关于 cinema4d-mcp
cinema4d-mcp 是用于从 Cinema 4D 提取并验证场景、动画和 Redshift 材质数据的技能,指导用户选择 MCP 工具或 Python API,并处理逐帧烘焙与 API 差异。
主要功能
- 01场景连接与 Python 健康检查
- 02MoGraph 动画烘焙和导出
- 03Redshift 材质及节点图提取
- 04逐帧数据验证指导
适用场景
- 01将克隆器动画烘焙为逐帧数据
- 02提取 Redshift 着色器参数
- 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.mdmograph-baking-guide.mdredshift-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和maxonAPI(参见 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 模式。
健康检查(始终先执行)
get_scene_info- 验证连接- 使用
execute_python_script执行print("ok")- 验证 Python 是否正常工作 - 如果两项都正常,即使其他工具不可用,仍可进行提取
关键规则
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.336level(颜色校正):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/挂起故障排查行,了解与之相伴的渲染端症状。
节点种类常量
| 种类 | 值 | 含义 |
|---|---|---|
| NODE | 1 | 着色器节点(standardmaterial、incandescent 等) |
| INPUTS | 2 | 输入端口容器 |
| OUTPUTS | 4 | 输出端口容器 |
| INPORT | 8 | 单个输入端口(具有值) |
| OUTPORT | 16 | 单个输出端口 |
重名
如果场景中包含多个可见名称相同的材质,请在 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 中播放。
步骤:
-
健康检查——确认
get_scene_info返回场景名称,且doc.GetFps()返回 30。 -
从
list_objects或get_scene_info中识别克隆器名称。假设名称为"SphereCloner"。 -
运行小规模测试烘焙(帧 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]}")
-
确认输出:每帧 50 个克隆体,位置随帧变化,且没有 NaN 值。
-
使用完整动画烘焙工作流(上文步骤 3-5)运行完整烘焙。导出的 JSON 将保存到系统临时目录。
-
转换为 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 重新实现失败时,可以将渲染器用作预言机:
- 克隆材质;将
<node>.outcolor -> standardmaterial .emission_color重新接线(权重设为 1);将基础色、反射和折射权重归零。解码时撤销任何节点着色(例如,黑色到绿色渐变:取 G 通道的值,并进行 sRGB 线性化)。 - 将材质赋给位于已知世界坐标的平面;长焦透视相机即可——固定深度的平面投影恰好是仿射变换,无需正交投影(RS + 平行相机无论如何都可能不稳定)。
- 每个渲染像素都是已知坐标处的场采样。扫描平面的 z 值以获取体积切片;扫描场景时间以获取时间轴。
- 即使使用高负载渲染预设,仅发射颜色的渲染也能在数秒内收敛。
促成此方法的验证结果(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() - 同时保留原始提取数据和派生模型
补充参考资料
- references/errors.md — Python API 和 MCP 工具错误表、安全限制、关键参数 ID
- references/mograph-baking-guide.md — 逐帧顺序推进的详细示例、分块烘焙策略、内存管理
- references/redshift-workarounds.md — Redshift 颜色采样、材质验证模式、节点图限制