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. 02Redshiftシェーダーのパラメーター抽出
  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

信頼できる唯一の情報源

このスキルは、Cinema 4D MCP の Vlad によるフォーク(vladmdgolam/cinema4d-mcp)を対象としています。

関連するフォークでの追加機能:

  • inspect_redshift_materials - マテリアルの割り当て、プレビューから取得した色、読み取り可能な説明/コンテナフィールド、ベストエフォートのグラフ調査に対応する読み取り専用の Redshift インスペクターです。注: このツールは RS マテリアルを not_redshift_like(タイプ 5703)としてスキップする場合があります。RS ノードグラフを完全に抽出するには、代わりに maxon API を使用した execute_python_script を使用してください(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 を使用してください。ただし、タイプ 5703 のラッパーを使っている場合、このツールは RS マテリアルを not_redshift_like としてスキップする可能性があります。RS ノードグラフの全データを取得するには、Redshift のセクションに記載されている maxon API パターンを使用して execute_python_script を実行してください。

ヘルスチェック(必ず最初に実施)

  1. get_scene_info - 接続を確認します
  2. print("ok") を指定して execute_python_script を実行 - 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~30 秒、フォークによっては 60 秒)。チャンクに分けてベイクしてください(例:フレーム 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 Object タグ、親階層からの継承、または Takes システムによって無効化されている場合があります。

アニメーションの完全なベイク手順

これは、最初から最後までの正式な手順です。順番に実行してください。各ステップの結果を確認してから次に進みます。

Step 1:ヘルスチェック
# Tool call: get_scene_info
# Then:
import c4d
print(doc.GetDocumentName(), doc.GetFps(), doc.GetMaxTime().GetFrame(doc.GetFps()))

確認事項:シーン名が取得できること、fps が正しいこと(通常は 24/25/30)、最大フレームが想定される終了フレームであること。

Step 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 がそれらを解決します。

Step 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)}")
Step 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))
Step 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() が想定される最終フレームを返す)
  • すべてのエフェクターが有効になっている(表示の信号機とタグを確認)
  • 干渉するテイクがない(doc.GetTakeData().GetCurrentTake() が正しいテイクを返す)
  • まず小さな範囲(フレーム 0~10)でテストする — フルベイクの実行前に、クローン数と位置が正しいことを確認する
ベイク後
  • 総フレーム数が end - start + 1 と一致する(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 マテリアルの抽出

RS ノードグラフには、Redshift がインストールされていれば maxon Python API 経由でアクセスできます。 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 を返し、GetEffectivePortValue() は GraphNode ラッパーに 存在しません。グラフの構造は GetInnerNodes(ALL_MASK) で 引き続き列挙できます。また、dir(port) が「expected generic datatype capsule」エラーを発生させることがあります。値を読み取れない場合は、 実測による抽出に切り替えてください。バッチキューで小さなフレームを 1 つレンダリングして(例: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 ファイルには 2 種類のシリアライズ形式があります。

形式ヘッダーマーカー値マーカー検出場所
新しい形式10 1c 10 1c(16 バイト)10 21 + タイププローブ/ベイクファイル
古い形式10 1f 10 1f(16 バイト)10 24 + タイプシーン _002.c4d ファイル

タイプバイト:0x11 = float64(IEEE 754 LE、8 バイト)、0x12 = int32 (LE、4 バイト)。値の後には、次のポートパス長を示す 2 バイトの ビッグエンディアン uint16 が続きます(アラインメントの検証に使用)。 vec3/カラー値の場合: タイプバイト 0x22 → float64 RGB × 3。

3 つ目の形式(10 20 10 20、16 バイトのヘッダー + 10 28 の値マーカー)は ノードグラフの接続(ポート間の配線)をエンコードします。ポート値では ありません — 現在もリバースエンジニアリング中です。

抽出できる内容
カテゴリ状態備考
スカラー float64/int32✅ 完全に動作brightness、contrast、animation_speed、coord_scale、seed、refr_weight、refl_roughness、rotate など
ランプのカラーストップ✅ デコード済み各ストップの type 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 の 2 つのノイズノード)
  • 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 マテリアル)

GetGraph() が None を返す古い RS シェーダーネットワークのマテリアルの場合:

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_name の代わりに material_index を使用してください。

クローンからマテリアルへのマッピング

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. Complete Animation Bake Workflow(上記の手順 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 マテリアルを使用しています。クローンとマテリアルの対応付けのために各マテリアルの色を特定する必要がありますが、mat[c4d.MATERIAL_COLOR_COLOR] を実行すると、すべての RS マテリアルで黒またはゼロが返されます。

失敗する理由: Redshift マテリアルの色は、標準の C4D マテリアルコンテナではなく、RS ノードグラフに保存されています。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 ライセンスの有効期限が切れていても使用できます(レンダリングせず、キャッシュを読み取ります)。上記の「Fallback: Preview Bitmap Sampling」の注記を参照してください。

ゼロクリックのバックエンドワークフロー

MCP プラグインは、Cinema の起動時にソケットサーバーを自動で開始します(ヘッドレスの MessageData ポンプ — ダイアログは不要です。プラグインリポジトリのコミット 94f9cbb)。 シェルからバックエンドとして 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)に配線し直します。ベース/反射/屈折のウェイトは 0 にします。デコード時には、ノードによる色付けを解除します(例:黒から緑へのランプの場合、値は 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 のバリアントを 1 つ保存し(SaveDocument はディレクトリを作成しないため、先に mkdir を実行します)、すべてを AddFile で追加して、SetRendering(BR_START) を実行します。エミッションレンダリング 360 枚に約 25 分かかりました。

Picture Viewer による正解データのレンダリング(Redshift)

スクリプトから c4d.documents.RenderDocument() を実行すると、一部の Redshift ビルドでは 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 へ自動保存します。人に「ファイル」→「名前を付けて保存」を頼む前に、ここを確認してください。(2 回のセッションで手動保存した後に気づきました。)
  • C4D のメインスレッド(プラグインのディスパッチキュー)上ですでに実行されているコードから、RenderDocument() を呼び出してはいけません。デッドロックします。レンダリングにはイベントループが必要ですが、ブロックされた呼び出しがそれを保持しているためです。

トラブルシューティングのクイックリファレンス

症状考えられる原因対処方法
ExecutePasses() が失敗する、または誤ったデータを返すSetTime() を ExecutePasses() の前ではなく後に呼び出している必ず SetTime → ExecutePasses → データの読み取りの順に実行します
すべてのフレームで MoGraph の行列が同一になる順番にステップせずにフレームへジャンプしているフレーム 0→N の順に進め、決してスキップしないでください
ワールド座標がビューポートと大きく異なるグローバル行列を適用していないworld_pos = cloner.GetMg() * m.off
GeGetMoData() が None を返すそのフレームで Cloner がまだ評価されていないExecutePasses が実行されたことを確認し、Cloner がミュートされていないか確認します
フレームごとにクローン数が変わる子の表示状態がアニメーションするオブジェクト Clonerフレームごとに md.GetCount() を読み取り、クローン数が一定だと仮定しないでください
広い範囲を処理するとスクリプトがタイムアウトする1 回の MCP 呼び出しで処理するにはフレーム範囲が大きすぎる100~200 フレーム単位に分割して処理し、結果を統合します
MODATA_CLONE がすべて 0.0 になる子が 1 つの Cloner、または子の切り替えがない想定どおりの動作です。すべてのクローンが同じ子を共有します
エフェクターのキーフレームが出力に反映されないTakes システムが Take を上書きしているdoc.GetTakeData().GetCurrentTake() を確認します
すべてのバッチジョブが Redshift の「Out Of Memory」で即座にエラーになる、またはインタラクティブな PV レンダー(CallCommand(12099))ではエラーにならないものの、CheckIsRunning(CHECKISRUNNING_EXTERNALRENDERING) が単純なシーンの収束時間をはるかに超えて 1 のままになる実際のメモリ不足と決めつけないでください。Redshift ライセンスの期限切れや失効で、この 2 つの症状が正確に発生します(バッチジョブでは誤解を招く即時の「OOM」、PV ではライセンス確認の停止による無限ハングのような状態)切り分けるには、シーンを 1 つ直接読み込み(バッチキューは使わず)、単純なシングルフレームの PV レンダー(理想的には、上記のとおり数秒で収束するはずの発光プローブシーン)を実行し、CHECKISRUNNING_EXTERNALRENDERING をポーリングします。エラーなしでその時間を大幅に超えてハングする場合は、RAM ではなく RS ライセンスを確認してください。システムメモリを解放しても、ライセンスの問題は解決しません。

既知のエラーと回避策

Python API と MCP ツールのエラー一覧については、references/errors.md を参照してください。

高度なデバッグ

Raw Socket フォールバック

MCP ツールが完全に失敗しても、C4D ソケットサーバーが 127.0.0.1:5555 で稼働している場合は、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)

注意事項:

  • MCP プラグインを読み込まずに C4D を起動した場合、ソケットサーバーが稼働していないことがあります。
  • デフォルトのポートは 5555 です。フォーク版や設定によっては、別のポートが使われる場合があります。
  • 応答は改行区切りの JSON です。大きな応答(例:シーン全体のデータ)は分割されるため、完全な JSON オブジェクトを受け取るまで recv を繰り返してください。

データ出力

  • メタデータ(シーン名、fps、フレーム範囲、サンプリングステップ)を含めて JSON に保存します
  • 小さな結果には json.dumps() と print()、大きなデータには tempfile.gettempdir() を使います
  • 生の抽出データと派生モデルの両方を保持します

追加リファレンス

コメント