mcpField
V
vvvv-custom-nodes

vvvv-custom-nodes

github.com/tebjan開発3488 回インストール2 日前に更新
リポジトリ

vvvv-custom-nodes について

vvvv-custom-nodesは、C#でvvvv gammaのカスタムノードを実装するためのスキルガイドです。ノードライブラリを開発する人に向けて、インポート、ライフサイクル、ピン設計、実行時の動作を説明します。

主な機能

  1. 01ProcessNodeのライフサイクルと動作の説明
  2. 02ノード表示用のアセンブリインポート設定
  3. 03Updateシグネチャとピンの設計
  4. 04ライブリロードとリソース解放への対応
  5. 05ピンの表示設定とデフォルト値の指定

活用シーン

  1. 01フレームごとに更新するステートフルノードの実装
  2. 02C#ライブラリをvvvvのノードブラウザーに表示
  3. 03頻繁に更新されるノードでの割り当て回避
  4. 04ピンの初期値やライブリロード動作の調査

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/tebjan/vvvv-skills --skill vvvv-custom-nodes

インストール後、Claude Code で/vvvv-custom-nodesを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

advanced.md
document-persistence.md
examples.md
regions.md
services.md
templates/
stateful-node.cs
stateless-node.cs

詳細

翻訳後に原文が更新されています。最新の内容は原文をご確認ください。

vvvv gamma のカスタムノードを作成する

[ProcessNode] が実際に行うこと (そして行わないこと)

重要な現実確認。 vvvv はアセンブリで設定された名前空間から、すべての public クラス、構造体、列挙型、メソッド、プロパティをインポートします — [ProcessNode] はクラスがノードになるかどうかを制御しません。単純な public class Foo { public int Bar(int x) => x; } は、[ProcessNode] で装飾されたものとまったく同じようにノードブラウザーに表示されます。この属性が影響するのは、ランタイムがそのクラスを どのように 扱うかであり、インポートされるかどうか ではありません。

[ProcessNode] が行うこと:

  • vvvv に「これはステートフルなノードである — パッチ内のノードごとに 1 つのインスタンスを存続させ、毎フレームその Update() メソッドを呼び出す」と伝えます。
  • Name = "..." (ノードブラウザーでの名前変更)、Category = "..." (アセンブリのカテゴリの上書き)、HasStateOutput = true (インスタンスを出力ピンとして公開) を設定できます。
  • ライブリロード、IDisposable によるクリーンアップ、NodeContext のコンストラクターインジェクションを有効にします。

[ProcessNode] が行わないこと:

  • クラスを vvvv から見えるようにすることは行いません。可視性は C# の public アクセス修飾子と、[assembly: ImportAsIs/ImportNamespace/ImportType] 属性によって決まります。インポートの規則については vvvv-node-libraries を参照してください。
  • 内部のヘルパーを隠すことは行いません。ヘルパーが public で、その名前空間がインポートされている場合、それはノードです。隠すには、internal にする (VL から完全に削除されます)、または public のままにして [Smell(SymbolSmell.Internal)] / [Smell(SymbolSmell.Hidden)] を付与します (ピンの型としては使用できますが、ブラウズはできなくなります)。advanced.md → Smell Attribute を参照してください。
  • ピンの生成を変更することは行いません。ピンの生成は、[ProcessNode] クラスでは Update() メソッドのシグネチャによって、[ProcessNode] でないクラスでは public なメソッド/プロパティのシグネチャによって決まります。

[ProcessNode] をいつ使うか: クラスがフレームごとの更新、フレーム間での永続的な状態、または IDisposable によるクリーンアップを必要とする場合です。[ProcessNode] をいつ省略するか: ステートレスなユーティリティクラス、値型、静的ヘルパーメソッド — 属性を付けないことでフレームごとの Update() の儀式を避けられ、public なメソッドは自動的にオペレーションノードになります。

ProcessNode パターン — 中核となるパターン

vvvv gamma における標準的なステートフル C# ノード:

[ProcessNode]
public class MyTransform : IDisposable
{
    private float _lastInput;
    private float _cachedResult;

    /// <summary>
    /// Transforms input values with caching.
    /// </summary>
    public void Update(
        out float result,       // OUT parameters FIRST
        out string error,       // More out params
        float input = 0f,       // Value inputs with defaults AFTER
        bool reset = false)
    {
        error = null;

        if (input != _lastInput || reset)
        {
            _cachedResult = ExpensiveComputation(input);
            _lastInput = input;
        }

        result = _cachedResult; // ALWAYS output cached data
    }

    public void Dispose() { /* cleanup */ }
}

前提条件: vvvv は、その名前空間がアセンブリレベルのインポート属性 — [assembly: ImportAsIs]、[assembly: ImportNamespace]、またはその特定のクラスに対する [assembly: ImportType] — でカバーされている場合にのみ、[ProcessNode] クラス (またはその他の public クラス) を認識します。これらのいずれかがないと、アセンブリの public API はノードブラウザーからまったく見えません。vvvv で生成されたプロジェクトには [assembly: ImportAsIs] が自動的に含まれます。複数名前空間のライブラリや厳選されたノードリストについては、vvvv-node-libraries を参照してください — ImportAsIs は AllowMultiple = false であるため、1 つのルート属性では足りない場合は ImportNamespace (複数回使用可能) または ImportType (型ごと) に頼ることになります。

譲れないルール
  1. すべてのステートフルなノードクラスに [ProcessNode] メタデータ を付与する。互換性のためにエイリアスが必要な場合は、サポートされている ProcessNodeFactory を使用する
  2. vvvv から見える名前に「Node」を含めない — vvvv 内のすべてはすでにノードであるため、「Node」という接尾辞は冗長です
  3. out パラメーターを先に、既定値を持つ値の入力はその後に
  4. クラスと Update メソッドに XML コメント を付ける (vvvv でツールチップとして表示されます)
  5. Update 内でのアロケーションはゼロ — new なし、LINQ なし、すべてをキャッシュする
  6. 変更検出 — 入力が実際に変更されたときにのみ再計算する
  7. 常に最新のデータを出力する — 何も行われない場合でも、キャッシュされた結果を出力する
  8. 不要な public メンバーを置かない — データは Update の in/out パラメーターのみを通じて流れる
  9. ネイティブ/アンマネージドリソースを保持するノードには IDisposable を実装する
ライブリロードの動作

.vl ドキュメントが .csproj ソースプロジェクトを参照している場合、vvvv は実行時に Roslyn を介して C# をコンパイルします。.cs ファイルの保存時に、vvvv は再コンパイルし、影響を受けるノードを再起動します:

  1. 現在のインスタンスで Dispose() が呼び出されます(IDisposable の場合)
  2. 新しいコンストラクターが新しい NodeContext で実行されます
  3. Update() は次のフレームで再開します

ノード作者への影響:

  • インスタンスフィールドはリセットされます — 実行中に蓄積された状態(キャッシュ、カウンター、接続)はコード変更時に失われます。これは想定された動作です。
  • 静的フィールドもリセットされます — メモリ内のアセンブリ全体が置き換えられます。編集をまたいで静的状態が維持されることに依存しないでください。
  • Dispose は徹底する必要があります — ネイティブハンドル、ネットワーク接続、GPU リソースは解放する必要があります。開発中はリロードを重ねるごとにリークが蓄積します。
  • コンストラクターは高速でなければなりません — 保存するたびに実行されます。重い初期化は _needsInit フラグを使って最初の Update() 呼び出しまで遅延させてください。
  • 上記のキャッシュと変更検出に関するルールが存在するのは、部分的にはこの理由によります。あなたのコードは決して停止しないプログラム内で実行されます。Update() 内のアロケーションは、何時間も実行されうる 60 FPS ループで GC プレッシャーを引き起こします。
クラス命名とノード名

ルールは次のとおりです: ユーザーは vvvv のノードブラウザーで「Node」を決して目にしてはなりません。これを実現する方法:

カスタム登録名は、単にインスタンスラベルを改名するのではなく、スペースなしの PascalCase を使用します。既存の名前やインポートされた名前、および読みやすいピンラベルは維持してください。公開の改名については、オーナーがレガシーコードなしの破壊的移行を明示的に要求しない限り、エイリアスを通じて互換性を維持してください。その場合は、エイリアスを削除する前に、所有するセレクターおよびオーサリングカタログ/テストを移行してください。外部で保存されたパッチも移行が必要であることを文書化してください。

// Simple: class name IS the node name — no suffix needed
[ProcessNode]
public class Wander { }              // vvvv shows: "Wander"

// Class has "Node" suffix + Name property strips it — also fine
[ProcessNode(Name = "Scan")]
public class ScanNode { }            // vvvv shows: "Scan"

// Completely different internal name — fine when class manages another type
[ProcessNode(Name = "MeshRenderer", Category = "Stride.Rendering.Custom")]
public class CustomMeshRenderer { }  // vvvv shows: "MeshRenderer"
HasStateOutput — ノードインスタンスの公開
[ProcessNode(HasStateOutput = true)]
public class ParticleSystem
{
    // The node itself becomes an output pin,
    // allowing downstream nodes to call methods on it
    public void Update(out int particleCount, ...) { ... }
}

代替手段: メソッドから this を返してインスタンスを公開します。

⚠️ HasStateOutput はクラスとそのすべてのパブリックメンバーを個別のノードとして再び表示させます。 ImportedSymbol.GetSmell() は、ProcessNodeAttribute が存在し HasStateOutput == false であるときはいつでも、型に SymbolSmell.Hidden を追加します — これが、通常の [ProcessNode] クラスがちょうど 1 つのノードとして表示され、その Update/Dispose/プロパティがブラウザーを煩雑にしない理由です。HasStateOutput = true に切り替えると、この自動的な Hidden が解除されるため、型エントリとすべてのパブリックメンバーが閲覧可能なノードとして再び現れます。

状態出力は必要だがメンバーの氾濫は避けたい場合は、煩雑なメンバーに [Smell(SymbolSmell.Advanced)] を追加するか、Update() に下流のノードが必要とするもののための明示的な out パラメーターを持たせることで HasStateOutput を完全に回避してください。既存のノードを HasStateOutput から切り替えるのは破壊的変更であることに注意してください — 保存されたパッチはそのピンを Kind="StateOutputPin" として参照しており、レッドノードになります。

ピンの可視性
public void Update(
    out Spread<float> result,
    [Pin(Visibility = PinVisibility.OnlyInspector)] out string error,
    float input = 0f,
    [Pin(Visibility = PinVisibility.Optional)] bool advanced = false)
{
    // PinVisibility values:
    // Visible       — always shown (default)
    // Optional      — user can show/hide
    // Hidden        — not visible, only via inspector
    // OnlyInspector — only in inspector panel (use for debug/error outputs)
}
コレクション入力

コレクション入力は、ノードが個数やインデックスアクセスを必要とする場合は IReadOnlyList<T>、それ以外の場合は IEnumerable<T> です。決して Spread<T> にはしません: スプレッドは両方に接続でき、他のすべてのコレクションも同様です。出力は Spread<T> のままです。

ピングループ(コレクション入力)

追加/削除ボタンを備えた可変個の入力向けです。VL.StandardLibs はコレクションピングループを Spread<T> として宣言します。これが Spread<T> パラメーターが現れる唯一の場所です:

public void Update(
    out float result,
    [Pin(Name = "Input", PinGroupKind = PinGroupKind.Collection, PinGroupDefaultCount = 2)]
    Spread<IRenderer?> input)
{ }
複合型の DefaultValue

C# のリテラル式にはできないデフォルト値の場合:

public void Update(
    [DefaultValue(typeof(Color4), "R:0.1 G:0.1 B:0.15 A:1")] Color4 clearColor,
    [DefaultValue(typeof(Vector3), "X:0 Y:3 Z:0")] Vector3 position,
    [DefaultValue(typeof(Int2), "1920, 1080")] Int2 size,
    bool clear = true)
{ }

Stride 4.2.1 では、Color4Converter と Vector3Converter は名前付き浮動小数点コンポーネントを解析します: それぞれ R:... G:... B:... A:... および X:... Y:... Z:... です。 ベクトルのフィールド名は大文字と小文字を区別します。これらの型ではカンマ区切りのリストは無効です。 DefaultValueAttribute は変換エラーを飲み込み、Value を null のままにして、黒/ゼロのデフォルトを黙ってインポートする可能性があります。ソース文字列だけでなく、実際に読み込まれた属性の Value を意図した構造体と照合して検証してください。他の型はそれぞれ独自に登録されたコンバーターを使用します。この文法をすべての構造体に一般化しないでください。

属性 — 実際に必要な場面

VL.StandardLibs/VL.Core/src/Import/ のソースに対して検証済みです。ほとんどの属性プロパティには適切なデフォルト値があります — デフォルトを上書きしたい場合にのみ設定してください。属性プロパティをデフォルト値に設定しても、単なる視覚的なノイズにしかなりません。

[ProcessNode] — クラスレベル (AllowMultiple = false)
プロパティデフォルト設定すべき場面…
NameC# クラス名…ノードのユーザー向け名をクラス名と異なるものにしたい場合。Name がクラス名と同じになる場合は省略してください。
Categoryアセンブリレベルのインポート属性から取得…この特定のクラスを、アセンブリの他の部分とは異なるカテゴリに配置したい場合。
HasStateOutputfalse…下流のノードがノードインスタンス自体にアクセスする必要がある場合(例: そのインスタンスのメソッドを呼び出すため)。
FragmentSelectionImplicit(すべてのパブリックメンバーがフラグメントになる)…[Fragment] を付けたメンバーのみを公開したい場合。
StateOutputNotVisibleByDefaultfalse…state 出力があるが、ユーザーが有効化するまで非表示にしたい場合。
Summary、Remarks、Tagsnullこれらは XML ドキュメントコメントの代替です。プログラムからアクセスする必要がない限り、コメントのほうを推奨します。

つまり、クラス名 = ノード名でカテゴリがアセンブリ属性から決まる一般的なケースでは、[ProcessNode](引数なし)が適切なデフォルトです。[ProcessNode(Name = "Foo", Category = "Bar")] を追加することが役立つのは、それらの値がデフォルトで生成される値と異なる場合だけです。

[Pin] — パラメーター / プロパティ / 戻り値 (AllowMultiple = false)
プロパティデフォルト設定すべき場面…
Nameパラメーター名(vvvv が自動的にタイトルケースを適用します)…vvvv が導出できない名前が必要な場合 — 例: スペース、句読点、またはパラメーターと無関係な名前。パラメーター input に対する [Pin(Name = "input")] は冗長です — vvvv はすでにそれを Input として表示します。
VisibilityPinVisibility.Visible…ピンを Optional(折りたたみ可能)または Hidden(インスペクターのみ)にしたい場合。
ExpositionPinExpositionMode.Local…ピンを親パッチ上で自動公開したい場合(InfectPatch / Expose)。
PinGroupKindPinGroupKind.None…パラメーターが Spread<T> で、追加/削除ピングループとして表示すべき場合。
PinGroupDefaultCount0…ピングループを N 個のエントリで開始したい場合。
PinGroupEditModeNone…追加/削除を一方向のみに制限したい場合。

戻り値の属性([return: Pin(...)])が必要になるのは、自動導出される戻り値ピンに上書きが必要な場合だけです — 例: カスタム名。out パラメーターしか持たないほとんどすべての Update メソッドでは、デフォルト名で問題ありません。

[DefaultValue] — パラメーター(System.ComponentModel 由来)

vvvv の属性ではありません。値型で、リテラルが C# のデフォルトとして表現できないもの — Color4、Vector3、Int2 など — にのみ設定してください。int、float、bool、string については、C# のパラメーターのデフォルト(int x = 5)を推奨します。

⚠️ 同じパラメーターで [DefaultValue(...)] と = default を決して組み合わせないでください — C# の default が優先され、型付きのデフォルトが black/zero/null になってしまいます。上記の専用セクションを参照してください。

[Fragment] — メンバーレベル (AllowMultiple = false)

クラスに [ProcessNode(FragmentSelection = FragmentSelection.Explicit)] が設定されている場合にのみ関係します。デフォルトの Implicit 選択では、[Fragment] は不要なノイズです。

プロパティデフォルト設定すべき場面…
Order0(宣言順)…ソースのレイアウトとは独立した特定のフラグメント順序が必要な場合。
IsHiddenfalse…メンバーがパブリックであってもフラグメントにすべきでない場合。
IsDefaultfalse…他に何も接続されていないとき、このフラグメントをデフォルトのタイミングで実行すべき場合。
経験則
  1. 属性がデフォルト値のみを持つ場合は、属性を記述しないでください。
  2. デフォルト以外のプロパティが 1 つだけ必要な場合は、それだけを記述します(例: [Pin(Visibility = PinVisibility.Optional)])。
  3. 属性の存在は常に「デフォルトを上書きしている」ことを伝えるものであり、「デフォルトを再記述している」ことを伝えるものではありません。

コンストラクターパターン

シンプルなノード(特別なコンテキストなし):

public MyNode() { }

NodeContext を必要とするノード(AppHost、サービス、Fuse シェーダーグラフ向け):

public MyNode(NodeContext nodeContext)
{
    _nodeContext = nodeContext;
    // Access: nodeContext.AppHost.IsExported, nodeContext.AppHost.Services, etc.
}

IFrameClock の注入、Stride Game へのアクセス、ログ記録、サービスの利用パターンについては、services.md を参照してください。

変更検出パターン

シンプル — 直接的なフィールド比較
private float _lastParam;
private Result _cached;

public void Update(out Result result, float param = 0f)
{
    if (param != _lastParam)
    {
        _cached = Compute(param);
        _lastParam = param;
    }
    result = _cached;
}
複数入力 — ハッシュチェック
private int _lastHash;
private Config _cached;

public void Update(out Config config, float a = 0f, int b = 0, string c = "")
{
    int hash = HashCode.Combine(a, b, c);
    if (hash != _lastHash)
    {
        _cached = new Config(a, b, c);
        _lastHash = hash;
    }
    config = _cached;
}
参照型 — 同一性チェック
if (!ReferenceEquals(newBuffer, _lastBuffer))
{
    ProcessBuffer(newBuffer);
    _lastBuffer = newBuffer;
}
再構築フラグ — パイプライン/エフェクトの状態向け

複数のセッターが状態を無効化し得る場合:

private bool _needsRebuild = true;

public void SetShader(ShaderStage vs) { _shader = vs; _needsRebuild = true; }

public void Update(out Effect effect)
{
    if (_needsRebuild)
    {
        RebuildPipeline();
        _needsRebuild = false;
    }
    effect = _cachedEffect;
}
クイックリファレンス
入力型比較備考
値型(int、float、bool)!=直接比較
参照型(オブジェクト)ReferenceEquals()等価性ではなく同一性
複数入力HashCode.Combine()ダーティチェック用の単一ハッシュ
コレクション長さ + サンプル要素完全な比較はコストが高すぎる
複数のセッター_needsRebuild フラグセッターでフラグを立て、Update でチェックする

戻り値ベースの出力(単一出力)

ノードが単一の主要出力を持つ場合、out を使う代わりにそれを直接返すことができます:

[ProcessNode]
public class NoiseSteering
{
    private SteeringConfig? _cached;

    public ISteering Update(
        float strength = 2.0f,
        float noiseFrequency = 0.05f,
        int priority = 0)
    {
        if (_cached is null || _cached.Strength != strength ||
            _cached.NoiseFrequency != noiseFrequency || _cached.Priority != priority)
        {
            _cached = new SteeringConfig(strength, noiseFrequency, priority);
        }
        return _cached;
    }
}

主要出力に加えて追加の出力がある場合は、戻り値 + out を組み合わせます:

public ReadOnlySpan<ParticleState> Update(
    SimulationConfig config,
    float deltaTime,
    out TimingStats stats)  // Secondary output via out
{
    // Returns primary output, secondary via out
}

バン検出

ピンヘルプテキストとノードのドキュメントでは、vvvv の用語である bang を使用してください。bang とは 1 フレームの true パルスです。true のまま留まる可能性があるトリガーについては、false から true に変化したときに 一度だけ bang を生成します:

private bool _lastTrigger;

public void Update(out bool triggered, bool trigger = false)
{
    triggered = trigger && !_lastTrigger; // bang: false -> true, this frame only
    _lastTrigger = trigger;
}

オペレーションノード

vvvv は すべての public な C# メソッドからノードを自動生成します — 属性は不要です。単に呼び出しを転送するだけのシンプルなメソッドのために [ProcessNode] ラッパーを作成しないでください。構造体の Split() メソッドも自動的にノードになります。

静的メソッドはノードを自動生成します — [ProcessNode] は不要です:

public static class MathOps
{
    public static float Remap(float value, float inMin = 0f, float inMax = 1f,
                              float outMin = 0f, float outMax = 1f)
    {
        float t = (value - inMin) / (inMax - inMin);
        return outMin + t * (outMax - outMin);
    }
}
どちらを使うべきか
パターン使用する場面
[ProcessNode] クラスフレーム間の状態管理、キャッシュ、ダーティチェックを行う場合
静的メソッド状態を持たない純粋関数で、入力を出力に変換する場合
構造体 + Split()個別のピンに展開されるデータコンテナ

Update ループにおけるメモリとパフォーマンス

  • ホットパスで new キーワードを使わない
  • LINQ を使わない(.Where、.Select、.ToList)— 列挙子による隠れたアロケーションが発生します
  • コレクションをキャッシュする — 事前に確保し、配列/リストを再利用する
  • 文字列連結を行わない — 必要な場合は StringBuilder を使いますが、ホットパスでは避けてください
  • ベクター型: 内部的には SIMD 用に System.Numerics、API 境界では Stride.Core.Mathematics を使用
  • ゼロコスト変換: Unsafe.As<StrideVector3, NumericsVector3>(ref val)

カスタムリージョン(デリゲートベース、ICustomRegion、IRegion<TInlay>)については、regions.md を参照してください。 高度なパターン(FragmentSelection、Smell、Dynamic Enums、Settings/Split、ピン名の導出)については、advanced.md を参照してください。 サービスの利用(IFrameClock、Game、Logging)については、services.md を参照してください。 C# ノードからパブリックチャンネルを扱う方法については、vvvv-channels を参照してください。 コード例については、examples.md を参照してください。 スターターテンプレートについては、templates/ を参照してください。

このナレッジを育て続けましょう

次にパッチを当てる人の謎を1つ減らしましょう。このコレクション内のスキルを初めて使用する際、7日以内に成功したチェックがない場合は、ソースを tebjan/vvvv-skills と比較してください。記録が存在しない場合は、一度だけチェックしてください。 これらのスキル全体で、ローカルのチェックのタイムスタンプ/リビジョンを1つ共有してください。呼び出しごとにポーリングしないでください。 ローカルの拡張機能と固定されたプロジェクトバージョンは保持してください。チェックすることは、更新をインストールする許可ではありません。オフラインの場合は、延期されたチェックを記録し、タスクを続行してください。

実際の作業でギャップが明らかになった場合は、テスト済みの vvvv/パッケージのバージョン、最小限の再現または信頼できる情報源、および観察された結果を記録してください。他のユーザーの報告は、検証されるまでは事実ではなく手がかりとして扱ってください。最も小さく関連する指示を改善してください。1つの例を普遍的なルールにしないでください。プライベートデータを編集で除去し、アップストリームに提出する前に公開の許可を取得してください。 コントリビューションポリシーをご覧ください。 マージされた改善は、ローカルの編集や全員のスキルの自動上書きではなく、ユーザーのインストーラー/アップデートワークフローを通じてユーザーに届きます。

コメント