mcpField
C
cinema4d-mcp

cinema4d-mcp

github.com/vladmdgolam创意与媒体9103 次安装5 周前更新
代码仓库

关于 cinema4d-mcp

cinema4d-mcp 是用于从 Cinema 4D 提取并验证场景、动画和 Redshift 材质数据的技能,指导用户选择 MCP 工具或 Python API,并处理逐帧烘焙与 API 差异。

主要功能

  1. 01场景连接与 Python 健康检查
  2. 02MoGraph 动画烘焙和导出
  3. 03Redshift 材质及节点图提取
  4. 04逐帧数据验证指导

适用场景

  1. 01将克隆器动画烘焙为逐帧数据
  2. 02提取 Redshift 着色器参数
  3. 03检查导出动画中的缺帧或无效值

MCP 相关

此技能针对 Vlad 的 Cinema 4D MCP 分支,说明何时使用结构化 MCP 工具或 `execute_python_script`。技能本身不提供 MCP 服务端。

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/vladmdgolam/agent-skills --skill cinema4d-mcp

安装后在 Claude Code 中输入/cinema4d-mcp确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
errors.md
mograph-baking-guide.md
redshift-workarounds.md

详细介绍

Cinema 4D MCP

信息来源

本技能面向 Vlad 对 Cinema 4D MCP 的分支:vladmdgolam/cinema4d-mcp。

该分支的重要新增内容:

  • inspect_redshift_materials - 只读 Redshift 检查工具,可检查分配情况、从预览中提取的颜色、可读的描述/容器字段,以及尽力进行图结构探测。注意: 如果 RS 材质使用类型 5703 封装器,此工具可能会将其跳过并标记为 not_redshift_like;若要完整提取 RS 节点图,请改用 execute_python_script 和 maxon API(参见 Redshift 材质提取部分)。
  • 支持安全处理重名材质 - 检查工具接受 material_index,并在名称冲突时返回稳定的场景索引和重名提示。
  • Redshift GraphView 回退方案 - 如果节点空间访问失败但 import redshift 成功,检查工具会回退到 redshift.GetRSMaterialNodeMaster(...),并报告 GraphView 节点及已解析的连接。
  • 已加载的 C4D 插件可能落后于仓库中的版本。修改插件后,请重启 Cinema 4D,再验证工具的新行为。

工具选择

简单操作请使用结构化 MCP 工具(get_scene_info、list_objects、add_primitive 等)。

对于非简单提取,请优先使用 execute_python_script。它可以避免包装器/架构不匹配,提供完整的 c4d + maxon API 访问权限,并支持妥善控制逐帧处理。这对于 Redshift 材质提取尤其重要——maxon 节点空间 API 可以完整访问 RS 节点图,而其他工具可能会遗漏部分内容。

使用 inspect_redshift_materials 快速查看材质分配和预览颜色,但请注意,如果 RS 材质使用类型 5703 封装器,该工具可能会将其跳过并标记为 not_redshift_like。若要获取完整的 RS 节点图数据,请使用 execute_python_script,并采用 Redshift 部分中记录的 maxon API 模式。

健康检查(始终先执行)

  1. get_scene_info - 验证连接
  2. 使用 execute_python_script 执行 print("ok") - 验证 Python 是否正常工作
  3. 如果两项都正常,即使其他工具不可用,仍可进行提取

关键规则

1. 世界坐标与局部坐标

GeGetMoData() 返回克隆器局部位置。务必应用全局矩阵:

mg = cloner.GetMg()
world_pos = mg * m.off  # LOCAL -> WORLD

如果遗漏这一步,所有内容都会相对于克隆器的全局偏移发生位移。

2. 可见性常量的值相反
  • MODE_OFF = 1(不是 0!)
  • MODE_ON = 0(不是 1!)
  • MODE_UNDEF = 2(默认/继承)

始终使用 c4d.MODE_OFF / c4d.MODE_ON,切勿使用裸整数。

3. 按顺序逐帧处理

MoGraph 效果器会累积状态。请按 0->N 的顺序迭代:

for frame in range(start, end + 1):
    doc.SetTime(c4d.BaseTime(frame, fps))
    doc.ExecutePasses(None, True, True, True, c4d.BUILDFLAGS_NONE)
    # NOW read data

切勿跳到任意帧。切勿跳过 ExecutePasses。

4. 拆分大型烘焙任务

MCP 脚本处理较大帧范围时会超时(默认约 20-30s,某些分支为 60s)。请分块烘焙(例如,先处理帧 0-200,再处理 200-400),之后再合并。使用 print() 记录进度。

5. 仅使用迭代遍历

使用基于栈的遍历。递归遍历会触发 Python 递归限制:

def find_obj(name):
    stack = [doc.GetFirstObject()]
    while stack:
        obj = stack.pop()
        while obj:
            if obj.GetName() == name:
                return obj
            if obj.GetDown():
                stack.append(obj.GetDown())
            obj = obj.GetNext()
    return None
6. API 版本兼容性

不同 C4D 版本中的常量可能不同。请使用防御性检查:

if hasattr(c4d, "SCENEFILTER_ANIMATION"):
    ...
7. 检查渲染可见性

对象可能会通过交通灯(GetRenderMode())、RS 对象标签、父级层级继承或 Takes 系统禁用。

完整动画烘焙工作流

这是权威的端到端流程。请按顺序执行——每一步都是下一步的前置条件。

步骤 1:健康检查
# Tool call: get_scene_info
# Then:
import c4d
print(doc.GetDocumentName(), doc.GetFps(), doc.GetMaxTime().GetFrame(doc.GetFps()))

确认:场景名称可以解析,fps 正确(通常为 24/25/30),最大帧数符合预期的结束帧。

步骤 2:查找动画轨道

烘焙前,请确认克隆器确实包含已制作动画的效果器:

import c4d

def find_obj(name):
    stack = [doc.GetFirstObject()]
    while stack:
        obj = stack.pop()
        while obj:
            if obj.GetName() == name:
                return obj
            if obj.GetDown():
                stack.append(obj.GetDown())
            obj = obj.GetNext()
    return None

fps = doc.GetFps()
cloner = find_obj("MyClonerName")  # replace with actual name

# Check direct tracks on cloner
for t in cloner.GetCTracks():
    did = t.GetDescriptionID()
    ids = [int(did[i].id) for i in range(did.GetDepth())]
    curve = t.GetCurve()
    key_count = curve.GetKeyCount() if curve else 0
    print(f"Track IDs: {ids}, keys: {key_count}")

# Check effector children for their own tracks
child = cloner.GetDown()
while child:
    for t in child.GetCTracks():
        did = t.GetDescriptionID()
        ids = [int(did[i].id) for i in range(did.GetDepth())]
        print(f"  Effector '{child.GetName()}' track IDs: {ids}")
    child = child.GetNext()

如果没有发现轨道,动画可能由场或表达式驱动——仍请继续烘焙;ExecutePasses 会解析这些内容。

步骤 3:烘焙 MoGraph(按顺序逐帧处理)
import c4d
from c4d.modules import mograph as mo
import json

def find_obj(name):
    stack = [doc.GetFirstObject()]
    while stack:
        obj = stack.pop()
        while obj:
            if obj.GetName() == name:
                return obj
            if obj.GetDown():
                stack.append(obj.GetDown())
            obj = obj.GetNext()
    return None

def vec(v):
    return [float(v.x), float(v.y), float(v.z)]

fps = doc.GetFps()
start = 0
end = int(doc.GetMaxTime().GetFrame(fps))
cloner = find_obj("MyClonerName")
mg = cloner.GetMg()  # global matrix for LOCAL->WORLD

frames_data = {}

for frame in range(start, end + 1):
    doc.SetTime(c4d.BaseTime(frame, fps))
    doc.ExecutePasses(None, True, True, True, c4d.BUILDFLAGS_NONE)

    md = mo.GeGetMoData(cloner)
    if md is None:
        print(f"Frame {frame}: no MoData")
        continue

    matrices = md.GetArray(c4d.MODATA_MATRIX)
    clone_indices = md.GetArray(c4d.MODATA_CLONE)

    frame_clones = []
    for i, m in enumerate(matrices):
        world_pos = mg * m.off
        scale = (m.v1.GetLength() + m.v2.GetLength() + m.v3.GetLength()) / 3.0
        frame_clones.append({
            "index": i,
            "position": vec(world_pos),
            "scale": float(scale),
            "clone_index": float(clone_indices[i]) if clone_indices else None
        })

    frames_data[frame] = frame_clones
    if frame % 50 == 0:
        print(f"Baked frame {frame}/{end}")

print(f"Done. Total frames baked: {len(frames_data)}")
步骤 4:提取关键帧(可选——适用于稀疏数据)

如果需要仅包含关键帧的数据,而不是逐帧烘焙的数据:

import c4d

fps = doc.GetFps()
obj = find_obj("MyObject")

keyframe_data = {}
for t in obj.GetCTracks():
    did = t.GetDescriptionID()
    ids = [int(did[i].id) for i in range(did.GetDepth())]
    curve = t.GetCurve()
    if not curve:
        continue
    keys = []
    for k in range(curve.GetKeyCount()):
        key = curve.GetKey(k)
        keys.append({
            "frame": key.GetTime().GetFrame(fps),
            "value": float(key.GetValue())
        })
    keyframe_data[str(ids)] = keys

print(json.dumps(keyframe_data))
步骤 5:导出 JSON
import json

output = {
    "scene": doc.GetDocumentName(),
    "fps": fps,
    "start_frame": start,
    "end_frame": end,
    "clone_count": len(frames_data.get(start, [])),
    "frames": frames_data
}

import tempfile, os
export_path = os.path.join(tempfile.gettempdir(), "mograph_export.json")
with open(export_path, "w") as f:
    json.dump(output, f)

print(f"Exported {len(frames_data)} frames to {export_path}")
步骤 6:验证

导出后运行此项,以便在将数据交给下游之前发现隐蔽错误:

import json, math

with open(export_path) as f:
    data = json.load(f)

fps = data["fps"]
start = data["start_frame"]
end = data["end_frame"]
expected_frames = end - start + 1
actual_frames = len(data["frames"])

errors = []

if actual_frames != expected_frames:
    errors.append(f"Frame count mismatch: expected {expected_frames}, got {actual_frames}")

nan_count = 0
for frame_key, clones in data["frames"].items():
    for clone in clones:
        for coord in clone["position"]:
            if math.isnan(coord) or math.isinf(coord):
                nan_count += 1
        if clone.get("clone_index") is not None:
            if not (0.0 <= clone["clone_index"] <= 1.0):
                errors.append(f"Frame {frame_key} clone {clone['index']}: clone_index out of range: {clone['clone_index']}")

if nan_count > 0:
    errors.append(f"NaN/Inf found in {nan_count} position coordinates")

if errors:
    for e in errors:
        print("ERROR:", e)
else:
    print("Validation passed.")
    print(f"  Frames: {actual_frames}, Clones per frame: {data['clone_count']}")

验证清单

烘焙前
  • 在场景中设置帧范围(doc.GetMaxTime() 返回预期的结束帧)
  • 所有效应器均已启用(检查可见性指示灯和标签)
  • 没有干扰的 Take(doc.GetTakeData().GetCurrentTake() 是正确的 Take)
  • 先在较小范围内测试(帧 0-10)——确认克隆数量和位置正确后,再运行完整烘焙
烘焙后
  • 总帧数等于 end - start + 1(没有差一错误,也没有间隙)
  • 位置坐标中没有 NaN 或 Inf 值
  • 克隆索引位于 0.0–1.0 范围内(如果使用 MODATA_CLONE)
  • 世界位置合理——对照视口抽查第 0 帧和最后一帧

MoGraph 提取模式

import c4d
from c4d.modules import mograph as mo

def vec(v):
    return [float(v.x), float(v.y), float(v.z)]

cloner = find_obj("ClonerName")
mg = cloner.GetMg()

for frame in range(start, end + 1, step):
    doc.SetTime(c4d.BaseTime(frame, fps))
    doc.ExecutePasses(None, True, True, True, c4d.BUILDFLAGS_NONE)
    md = mo.GeGetMoData(cloner)
    matrices = md.GetArray(c4d.MODATA_MATRIX)
    for i, m in enumerate(matrices):
        world_pos = mg * m.off
        scale = (m.v1.GetLength() + m.v2.GetLength() + m.v3.GetLength()) / 3.0

动画轨道发现

for t in obj.GetCTracks():
    did = t.GetDescriptionID()
    ids = [int(did[i].id) for i in range(did.GetDepth())]
    curve = t.GetCurve()
    keys = []
    if curve:
        for k in range(curve.GetKeyCount()):
            key = curve.GetKey(k)
            keys.append({"frame": key.GetTime().GetFrame(fps), "value": float(key.GetValue())})

Cloner 模式常量

c4d.ID_MG_MOTIONGENERATOR_MODE: 0=Grid, 1=Linear, 2=Radial, 3=Object, 4=Honeycomb
c4d.MG_GRID_MODE: 0=Endpoint (total span), 1=Per Step (spacing)

Redshift 材质提取

可通过 maxon Python API 访问 RS 节点图,前提是已安装 Redshift。inspect_redshift_materials MCP 工具可能会将材质报告为 not_redshift_like(类型 5703),但仍可通过 maxon 节点空间 API 读取节点图。

首选路径:maxon 节点空间 API(已验证可用)

RS 材质使用节点空间 com.redshift3d.redshift4c4d.class.nodespace。可通过以下方式访问:

import c4d
import maxon

RS_NODESPACE = "com.redshift3d.redshift4c4d.class.nodespace"

mat = doc.GetMaterials()[0]
nm = mat.GetNodeMaterialReference()
graph = nm.GetGraph(RS_NODESPACE)  # returns NodesGraphModelRef

# Traverse ALL nodes and ports:
root = graph.GetRoot()
inner = root.GetInnerNodes(maxon.NODE_KIND.ALL_MASK, False)

for n in inner:
    kind = n.GetKind()
    nid = str(n.GetId())

    if kind == 1:  # NODE — e.g. standardmaterial, incandescent, texturesampler
        print(f"Node: {nid}")
    elif kind == 8:  # INPORT — readable port with value
        short_name = nid.split('.')[-1]
        try:
            val = n.GetDefaultValue()
            if val is not None:
                print(f"  {short_name} = {val}")
        except:
            pass

可获取的内容:

  • 所有着色器节点(RS Standard Material、Incandescent、MaxonNoise、TextureSampler、RSRamp、RSColorCorrection 等)
  • 所有输入端口值(颜色、强度、粗糙度、折射权重、纹理路径、噪波参数、渐变色标等)
  • GetDefaultValue() 和 GetEffectivePortValue() 均可用

**⚠ API 变化(在 C4D 2024.2、2026-08 中观察到):**部分构建版本中, GetDefaultValue() 对所有端口(无论是否自定义)都返回 None,而且 GraphNode 包装器上不存在 GetEffectivePortValue();但仍可通过 GetInnerNodes(ALL_MASK) 枚举图结构。 此外,dir(port) 可能会引发“expected generic datatype capsule”错误。当值无法读取时,可改用经验提取:渲染一个 小帧(批处理队列,例如 295x640 单帧的 RS 渲染约需 7 分钟),再 从像素测量颜色;也可使用发射探针模式提取场值。相比与 API 纠缠, 这种方法能更快地回答“哪个节点承载预设颜色”。

**改为:先尝试二进制提取。**这些值确实存在于 .c4d 二进制文件中,也可以通过直接解析读取——参见 ~/cinema4d-mcp/scripts/extract_rs_binary.py(独立 Python 脚本,无需 C4D/RS 许可证)。支持两种 C4D 序列化格式(10_1c 和 10_1f)中的 float64 和 int32 标量端口值。这种方式比 API 更快、更省成本,也更可靠。

python3 ~/cinema4d-mcp/scripts/extract_rs_binary.py scene.c4d
已解析的二进制格式

.c4d 文件中存在两种序列化格式:

格式标头标记值标记所见文件
较新10 1c 10 1c(16 字节)10 21 + 类型探测/烘焙文件
较旧10 1f 10 1f(16 字节)10 24 + 类型场景 _002.c4d 文件

类型字节:0x11 = float64(IEEE 754 小端序,8 字节),0x12 = int32 (小端序,4 字节)。值之后是一个 2 字节大端序 uint16,表示下一个端口 路径长度(用于对齐验证)。对于 vec3/颜色值: 类型字节 0x22 → 3 个 float64 RGB 值。

第三种格式(10 20 10 20,16 字节标头 + 10 28 值标记) 编码的是节点图连接(端口之间的连线),而非端口 值——仍在逆向工程中。

可提取的内容
类别状态备注
标量 float64/int32✅ 完全可用brightness、contrast、animation_speed、coord_scale、seed、refr_weight、refl_roughness、rotate 等
渐变颜色节点✅ 已解码每个节点的类型 0x22 vec3 颜色 + 位置 + 插值 + 偏移
coord_offset⚠️ 始终为默认值所有测试场景中均为 (0, 0, ~0.054) — 未显式存储
材质颜色❌ 非直接值base_color 是节点连接,而不是已存储的 RGB 值;请使用 GetPreview(0) 或发射探针
连接布线❌ 10_20 格式序列化哪个节点的输出连接到哪个节点的输入
从全部 4 个场景文件提取的值

完整表格请参见 music-shader 中的 docs/c4d-binary-reverse-engineering.md。关键共用值:

  • brightness:0.054(所有场景)
  • contrast:0.428 或 0.238(取决于噪波节点)
  • animation_speed:2.85 或 1.1(Skriptonit 中的两个噪波节点)
  • coord_scale_global:~26-27(所有场景)
  • refr_weight:0.581,refl_roughness:0.336
  • level(颜色校正):1.0-1.5

对于二进制读取器尚无法处理的颜色/vec3/渐变类型,请使用 发射探针方法或 GetPreview(0)(缓存缩略图)来获取粗略 颜色数据。

查找特定节点类型,请使用:

result = maxon.GraphModelHelper.FindNodesByAssetId(
    graph, "com.redshift3d.redshift4c4d.nodes.core.standardmaterial", True
)
次选路径:旧版 GraphView(较旧的 RS 材质)

对于较旧的 RS 着色器网络材质,如果 GetGraph() 返回 None:

import redshift
gv = redshift.GetRSMaterialNodeMaster(mat)
if gv:
    root = gv.GetRoot()
    child = root.GetDown()
    while child:
        print(f"Node: {child.GetName()} op={child.GetOperatorID()}")
        child = child.GetNext()
回退方案:预览位图采样

两种路径都不可用时(未安装 RS),可对预览位图采样以获取近似颜色:

bmp = mat.GetPreview(0)
if bmp:
    r, g, b = bmp.GetPixel(bmp.GetBw() // 2, bmp.GetBh() // 2)

⚠ Redshift 许可证已过期时同样适用,并非仅在未安装 RS 时适用。 GetPreview() 会返回 C4D 从上一次渲染/交互式预览中缓存的位图——它不会触发新渲染,因此不受阻止 RenderDocument() / 批处理 / PV 渲染的许可证检查影响。已通过实时测试确认(2026-08):许可证过期时,CallCommand(12099)(PV 渲染)会无限期挂起,CHECKISRUNNING_EXTERNALRENDERING 卡在 1;而同一文档上的 mat.GetPreview(0) 会立即返回真实(非占位符)像素数据。几乎瞬间的响应本身就表明你读取的是缓存,而不是在执行渲染——如果“预览”调用需要几秒,那就是另一条代码路径。项目进行过程中许可证过期时,这是在无需恢复许可证的情况下继续提取一些逐材质数据(通过 3x3 或更密集的像素网格采样获取粗略平均颜色)的最快方式——请参见下方的 OOM/挂起故障排查行,了解与之相伴的渲染端症状。

节点种类常量
种类值含义
NODE1着色器节点(standardmaterial、incandescent 等)
INPUTS2输入端口容器
OUTPUTS4输出端口容器
INPORT8单个输入端口(具有值)
OUTPORT16单个输出端口
重名

如果场景中包含多个可见名称相同的材质,请在 MCP 检查器中使用 material_index,而不是 material_name。

克隆体到材质的映射

使用 GeGetMoData() 中的 MODATA_CLONE 数组,获取映射到子对象的归一化克隆索引(0.0–1.0):

md = mo.GeGetMoData(cloner)
clone_indices = md.GetArray(c4d.MODATA_CLONE)  # float array, 0.0–1.0

这些值对应克隆器的子对象循环。请进行视觉验证——不要假设循环顺序与层级顺序一致。

示例

示例 1:将 MoGraph 克隆器动画提取为 Three.js 的 JSON

场景: 一个由 Random 效果器驱动、包含 50 个球体的克隆器,需要导出逐帧位置数据,以便在 Three.js 中播放。

步骤:

  1. 健康检查——确认 get_scene_info 返回场景名称,且 doc.GetFps() 返回 30。

  2. 从 list_objects 或 get_scene_info 中识别克隆器名称。假设名称为 "SphereCloner"。

  3. 运行小规模测试烘焙(帧 0-10),确认数据格式:

import c4d
from c4d.modules import mograph as mo

def find_obj(name):
    stack = [doc.GetFirstObject()]
    while stack:
        obj = stack.pop()
        while obj:
            if obj.GetName() == name:
                return obj
            if obj.GetDown():
                stack.append(obj.GetDown())
            obj = obj.GetNext()
    return None

fps = doc.GetFps()
cloner = find_obj("SphereCloner")
mg = cloner.GetMg()

for frame in range(0, 11):
    doc.SetTime(c4d.BaseTime(frame, fps))
    doc.ExecutePasses(None, True, True, True, c4d.BUILDFLAGS_NONE)
    md = mo.GeGetMoData(cloner)
    matrices = md.GetArray(c4d.MODATA_MATRIX)
    world_positions = [list(mg * m.off) for m in matrices]
    print(f"Frame {frame}: {len(world_positions)} clones, first pos: {world_positions[0]}")
  1. 确认输出:每帧 50 个克隆体,位置随帧变化,且没有 NaN 值。

  2. 使用完整动画烘焙工作流(上文步骤 3-5)运行完整烘焙。导出的 JSON 将保存到系统临时目录。

  3. 转换为 Three.js 兼容格式——JSON 结构 frames[frame][clone_index].position 可直接映射到由 AnimationMixer 驱动的循环中,每帧更新 BufferAttribute。

Three.js 使用模式:

// Load the exported JSON
const data = await fetch('/data/spherecloner_export.json').then(r => r.json());
const fps = data.fps;

// On each animation frame:
function updateClones(currentTime) {
    const frame = Math.floor(currentTime * fps);
    const frameData = data.frames[frame];
    if (!frameData) return;
    frameData.forEach((clone, i) => {
        meshes[i].position.set(...clone.position);
    });
}

示例 2:使用预览位图变通方案排查 Redshift 颜色缺失问题

场景: 场景使用 Redshift 材质。你需要确定每种材质的颜色,以建立克隆体到材质的映射,但对所有 RS 材质调用 mat[c4d.MATERIAL_COLOR_COLOR] 都会返回黑色或 0。

失败原因: Redshift 材质将颜色存储在 RS 节点图中,而非标准 C4D 材质容器中。mat[c4d.MATERIAL_COLOR_COLOR] 读取的是旧版 C4D 通道,而 RS 材质的该通道为空。

变通方案——预览位图采样:

import c4d

doc_mats = doc.GetMaterials()
material_colors = []

for mat in doc_mats:
    name = mat.GetName()
    bmp = mat.GetPreview(0)  # 0 = default preview size
    if bmp is None:
        material_colors.append({"name": name, "color": None, "error": "no preview"})
        continue

    w = bmp.GetBw()
    h = bmp.GetBh()

    # Sample a 3x3 grid of pixels from the center region to get a representative color
    samples = []
    for sx in [w // 3, w // 2, 2 * w // 3]:
        for sy in [h // 3, h // 2, 2 * h // 3]:
            r, g, b = bmp.GetPixel(sx, sy)
            samples.append((r, g, b))

    avg_r = sum(s[0] for s in samples) // len(samples)
    avg_g = sum(s[1] for s in samples) // len(samples)
    avg_b = sum(s[2] for s in samples) // len(samples)

    material_colors.append({
        "name": name,
        "color_rgb_0_255": [avg_r, avg_g, avg_b],
        "color_hex": f"#{avg_r:02x}{avg_g:02x}{avg_b:02x}"
    })

import json
print(json.dumps(material_colors, indent=2))

注意事项:

  • 预览位图由上次渲染或交互式预览生成。如果 C4D 尚未为某种材质渲染预览,GetPreview() 可能会返回 None 或灰色占位图。
  • 运行此脚本前,打开材质管理器并等待缩略图重新生成,以强制渲染预览。
  • 对于多层或金属质感的 RS 材质,采样颜色只是近似值——可用于识别材质(大致判断哪种材质偏红或偏蓝),而非精确匹配颜色。
  • 与 MODATA_CLONE 索引交叉参照,建立克隆体 -> 材质 -> 颜色查找表。
  • 即使 RS 许可证过期,此方法仍然有效(它读取缓存,不会进行渲染)——参见上文“回退方案:预览位图采样”下的说明。

零点击后端工作流

MCP 插件会在 Cinema 启动时自动启动其套接字服务(无须对话框的无头 MessageData 泵;插件仓库中的提交 94f9cbb)。 要从 shell 中将 Cinema 作为后端(重新)启动,music-shader 使用以下 模式:

./scripts/c4d-backend.sh --restart [scene.c4d]   # kill -> open -> wait for port 5555 -> READY

死锁警告:不要从 MCP 脚本中调用 c4d.documents.InsertBaseDocument / SetActiveDocument——这会导致 Cinema 在插件分派线程中挂起(与 RenderDocument 死锁属于同一类问题)。应在已激活的文档中创建探测对象,隐藏原对象,并在之后清理。

发射颜色探针法:采样渲染器的程序化场

当你需要获取渲染器的精确程序化场(噪声、渐变、任意着色器节点),而 CPU 重新实现失败时,可以将渲染器用作预言机:

  1. 克隆材质;将 <node>.outcolor -> standardmaterial .emission_color 重新接线(权重设为 1);将基础色、反射和折射权重归零。解码时撤销任何节点着色(例如,黑色到绿色渐变:取 G 通道的值,并进行 sRGB 线性化)。
  2. 将材质赋给位于已知世界坐标的平面;长焦透视相机即可——固定深度的平面投影恰好是仿射变换,无需正交投影(RS + 平行相机无论如何都可能不稳定)。
  3. 每个渲染像素都是已知坐标处的场采样。扫描平面的 z 值以获取体积切片;扫描场景时间以获取时间轴。
  4. 即使使用高负载渲染预设,仅发射颜色的渲染也能在数秒内收敛。

促成此方法的验证结果(YM 着色器项目):Redshift 的“Maxon noise”与 c4d.utils.noise.C4DNoise 并不兼容格点(在不同缩放、倍频和方向的扫描中,场的最高相关性为 0.23)——CPU 烘焙结果最多只能在统计特性上相似。相机注意事项:位于 -z 并朝 +z 方向拍摄的相机会使平面相对于世界坐标轴沿 X 轴镜像——在信任解码结果之前,请用已知的不对称参考物验证方向。

批量渲染扫描结果:Picture Viewer 的单帧自动保存不可靠,但 Batch Render 队列(c4d.documents.GetBatchRender())可以可靠地保存手动设置帧范围的作业中的每一帧。每种配置保存一个 .c4d 变体(SaveDocument 不会创建目录——请先 mkdir),将它们全部 AddFile,然后执行 SetRendering(BR_START)。360 次发射颜色渲染耗时约 25 分钟。

通过 Picture Viewer 获取渲染真值(Redshift)

在某些 Redshift 版本中,脚本化的 c4d.documents.RenderDocument() 可能返回 RENDERRESULT_OK,但位图却全黑(各种方式均如此:主线程、克隆、自动保存、BatchRender)。可靠的方法是异步 Picture Viewer 渲染:

import c4d
doc = c4d.documents.GetActiveDocument()
rd = doc.GetActiveRenderData()
# CRITICAL: default frame sequence may be "All Frames" — firing PV then
# renders the entire timeline (an accidental 148-frame overnight job).
rd.GetDataInstance()[c4d.RDATA_FRAMESEQUENCE] = c4d.RDATA_FRAMESEQUENCE_CURRENTFRAME
doc.SetTime(c4d.BaseTime(FRAME, doc.GetFps()))
doc.ExecutePasses(None, True, True, True, c4d.BUILDFLAGS_NONE)
c4d.CallCommand(12099)   # Render to Picture Viewer — ASYNC, returns immediately
  • 使用 c4d.CheckIsRunning(c4d.CHECKISRUNNING_EXTERNALRENDERING) 轮询渲染是否完成(渲染中为 1,完成后为 0)。
  • 以编程方式停止正在运行的 PV 渲染:c4d.CallCommand(430000731)。
  • 无需手动保存:如果渲染设置中设有保存路径,Picture Viewer 会自动保存每个已完成的帧至 <save-path>/<scene-name>/<scene-name>_NNNN.png——在要求用户执行“文件→另存为”之前,先检查此处。(这是在经历两轮手动保存后才发现的。)
  • 切勿在已位于 C4D 主线程(插件分发队列)的代码中调用 RenderDocument()——这会导致死锁:渲染需要事件循环,而被阻塞的调用正占用着它。

故障排查速查表

症状可能原因解决方法
ExecutePasses() 失败或返回错误数据在 ExecutePasses() 之后而不是之前调用了 SetTime()始终按此顺序操作:SetTime → ExecutePasses → 读取数据
不同帧的 MoGraph 矩阵完全相同跳转到了某一帧,没有按顺序逐帧推进按 0→N 的顺序逐帧推进,绝不跳帧
世界位置与视口中的位置相差很远未应用全局矩阵world_pos = cloner.GetMg() * m.off
GeGetMoData() 返回 None该帧的 Cloner 尚未完成求值确保已运行 ExecutePasses;检查 Cloner 是否未静音
每帧的克隆数量不同对象 Cloner 的子对象可见性带有动画每帧读取 md.GetCount(),不要假设数量固定
处理较大范围时脚本超时单次 MCP 调用的帧范围过大分成每批 100-200 帧处理,再合并结果
MODATA_CLONE 全部为 0.0单子对象 Cloner,或未循环切换子对象这是预期行为——所有克隆共用一个子对象
效果器上的关键帧无法驱动输出动画Takes 系统覆盖了当前 Take检查 doc.GetTakeData().GetCurrentTake()
每个批处理作业都会立即报 Redshift "Out Of Memory" 错误,或者交互式 PV 渲染(CallCommand(12099))一直不报错,但 CheckIsRunning(CHECKISRUNNING_EXTERNALRENDERING) 长时间保持 1,远超简单场景应有的收敛时间不要直接认定是内存争用——Redshift 许可证过期或失效恰好会导致这两种症状(批处理作业立即出现误导性的 "OOM";PV 渲染中的许可证检查停滞,看起来像是无限挂起)排查方法:直接加载一个场景(而不是使用批处理队列),启动一个简单的单帧 PV 渲染(理想情况下使用发光探针场景,按上述说明,应在几秒内收敛),并轮询 CHECKISRUNNING_EXTERNALRENDERING。如果远超这个时间仍然挂起且没有报错,就检查 RS 许可证,而不是 RAM。释放系统内存无法解决许可证问题。

已知错误与解决方法

完整的 Python API 和 MCP 工具错误表请参阅 references/errors.md。

高级调试

原始套接字回退方案

如果 MCP 工具完全无法使用,但 127.0.0.1:5555 上的 C4D 套接字服务仍在运行,你可以绕过 MCP 层,直接发送命令。这是最后的诊断手段,不属于常规工作流。

适用场景:

  • 所有 MCP 工具调用都返回连接错误
  • execute_python_script 在传输层失败(不是 Python 错误)
  • 你需要确认 C4D 服务进程是否仍在运行

可用示例:

import json
import socket

def c4d_raw(command_dict, host="127.0.0.1", port=5555, timeout=10):
    """
    Send a raw command to the C4D socket server and return the parsed response.
    Commands mirror the MCP tool names: get_scene_info, list_objects, execute_python_script, etc.
    """
    payload = json.dumps(command_dict) + "\n"
    s = socket.create_connection((host, port), timeout=timeout)
    try:
        s.sendall(payload.encode("utf-8"))
        # Read until newline (server responds with a single JSON line)
        response = b""
        while True:
            chunk = s.recv(4096)
            if not chunk:
                break
            response += chunk
            if b"\n" in response:
                break
    finally:
        s.close()
    return json.loads(response.decode("utf-8").strip())

# Example: check connection
result = c4d_raw({"command": "get_scene_info"})
print(result)

# Example: run a Python expression
result = c4d_raw({
    "command": "execute_python_script",
    "params": {"script": "print(doc.GetDocumentName())"}
})
print(result)

注意:

  • 如果启动 C4D 时未加载 MCP 插件,套接字服务可能不会运行。
  • 默认端口为 5555。部分分支或配置可能使用不同端口。
  • 响应采用以换行符分隔的 JSON。较大的响应(例如完整场景数据)会被分块——循环调用 recv,直到收到完整的 JSON 对象。

数据输出

  • 保存为带元数据的 JSON(场景名称、fps、帧范围、采样步长)
  • 小型结果使用 json.dumps() + print(),大型数据使用 tempfile.gettempdir()
  • 同时保留原始提取数据和派生模型

补充参考资料

评论

支持 Markdown