エフェクト オーバーライド

MPxShaderOverride は、ビューポート 2.0 でプラグイン サーフェス シェーダのすべてのシェーディングとライティングをオーバーライドする API エントリ ポイントです。MPxGeometryOverride と同様に、このクラスでは、シェーダの Maya ノードを定義しません。このクラスは完全な描画をオーバーライドするため、オーバーライドが、テクスチャ、ライト、ジオメトリなどのリソースの定義とバインドを行います。MDrawContext(描画コンテキスト)のインスタンスが適切なポイントで提供されるので、デバイス情報にアクセスしてこれらのタスクを容易に実行できます。このクラスは未処理の描画コールが必要であるため、描画 API に依存します。両方の API へのサポートが必要な場合は、DirectX および OpenGL 用に個別のコード パスを作成する必要があります。Maya CgFX および dx11Shader プラグインでのビューポート 2.0 のサポートは、このインタフェースを使用して実装されます。

MPxShaderOverride の実装は、特定のタイプのシェーディング ノードと関連付ける必要があります。ほとんどの場合は、プラグインでシェーディング ノードを定義し、シェーダを使用するオブジェクト用の描画コードを提供するために別の MPxShaderOverride が作成されます。CgFX プラグインでは MPxHwShaderNode インタフェースを使用して CgFX ノードを定義し、ビューポート 2.0 での描画をサポートする別の MPxShaderOverride が存在します。MPxShaderOverride の実装は、分類文字列を使用して MDrawRegistry に登録する必要があります。オーバーライドの分類を満たす分類文字列を使用するシェーダは、オーバーライドを使用して描画されます。分類文字列は、システムが認識できるように「drawdb/shader」で始まる必要があります。Maya では、シーンでアクティブに使用されている関連付けられたシェーダ タイプの各インスタンスに対して登録されたシェーダ オーバーライドの 1 つのインスタンスを作成します。

図 40

上位レベルでは、MPxShaderOverride に主なタスクが 2 つあります。描画する必要があるジオメトリ ストリームを指定し(ジオメトリ要件)、割り当て先のオブジェクトを描画することです。これは、新しいレンダリング モデルの producer-consumer (生成者-使用者)リレーションシップを示します。このクラスは、ジオメトリ要件(MGeometryRequirements)を生成します。この要件は、ジオメトリ ストリームを生成するためにジオメトリ システム(MPxGeometryOverride の内部クラスまたはプラグイン実装)で使用されます。次に、このクラスの描画メソッドが、描画するためにジオメトリ ストリームを使用します。

図 41: 2 つの異なる DAG オブジェクトに割り当てられたシェーダ オーバーライドのサンプル構成です。一方の DAG オブジェクトは、ジオメトリ オーバーライドを使用しています。もう一方は、内部ジオメトリ アップデータを使用しています。この構成は、NURBS サーフェス アップデータの例になります。シェーダ オーバーライドは、各アップデータの要件を「生成」します。各オブジェクトは、レンダリングする新しいジオメトリを「生成」します。このジオメトリは、シェーダ オーバーライドによって「使用」されるまでは、関連付けられたレンダー項目の下位のパイプラインに渡されます。

オーバーライドで実装する必要のある 3 つの主なフェーズは、初期化、更新、および描画です。

MPxShaderOverride のフェーズは、関連付けられたシェーダ タイプのインスタンスがオブジェクトにバインドされている場合、またはシェーダそれ自体の入力アトリビュートが変更された場合にトリガーされて実行されます。更新をトリガーするために、特別なロジックを追加する必要はありません。コールされる更新メソッドは、発生した変更のタイプによって異なります。シェーダの新しい割り当てによって、完全なリビルドがトリガーされます。同様に、rebuildAlways() が true を返す場合は、アトリビュートの変更でも完全なリビルドがトリガーされます。その他の場合はすべて、初期化はスキップされ、更新のみが行われます。描画フェーズは、シェーダでオブジェクトを描画する必要があるすべての更新時に発生します。

初期化フェーズ中に、addGeometryRequirement() を使用してジオメトリ ストリーム要件を指定することができます。この要件では、特定のシェーディング エフェクトが割り当てられるオブジェクトから、必要なジオメトリ ストリームを指定します。要件を指定しない場合、1 つの位置ストリーム要件が使用されます。initialize() メソッドは、シェーダ キーを表す文字列を返す必要があります。MPxShaderOverride の異なるインスタンスが本質的に同じシェーダを表していても、別のシェーダ パラメータを使用していることがよくあります。シェーダ キーは、同じシェーダを表す MPxShaderOverride インスタンスを識別するために使用されます。レンダリングを最適化するために、レンダラは、MPxShaderOverride から返されるプロパティに基づいて、オブジェクトのレンダリングを統合しようとします。これには、シェーダ キーが含まれますが、その他の要素も含まれることがあります。統合およびジオメトリの処理の詳細については、下記の「統合に関する考慮事項」を参照してください。これにより、プラグインはシーケンス全体で 1 回セットアップを実行するだけで済むようになります。各プラグインが、同じシェーダを表す目的を決定します。

図 42: シェーダ オーバーライドによって提供されるシェーダ キーを 2 つのレンダー アイテムで同じにすることができます。この場合、統合によって、描画が行われる前にこれらの項目を 1 つのレンダー項目に「マージ」できます。

初期化中、現在の表示モードが非テクスチャ表示モードである場合は、指定されたシェーダ オーバーライドに関連付けられているシェーディング ノードを使用するすべてのレンダー アイテムで、内部的に定義された静的シェーダ インスタンスが使用されます。これは、追加のノード モニタを回避すると同時に、このシェーダ インスタンスを共有するレンダー アイテムを統合できるようにするための、パフォーマンス最適化です。

この動作をオーバーライドするために、既定の共有インスタンスの代わりにカスタム シェーダ インスタンスを戻すように MPxShaderOverride::nonTexturedShaderInstance()メソッドをオーバーライドすることができます。ノードのアトリビュート変更時にこのシェーダ インスタンスを更新する必要があるかどうかを示すように、戻りパラメータを設定することができます。モニタが不要な場合は、Maya は非テクスチャ モードの間、更新フェーズをスキップしようとします。テクスチャ モード表示に使用されるシェーダで、更新が必要になることがあります。たとえば、MPxShaderOverride::rebuildAlways()が true を返す場合は、このメソッドで設定したオプションにかかわらず、更新フェーズが呼び出されます。

更新フェーズ中に、シェーディングに必要なすべてのデータ値が更新されます。インタフェースでは、ディペンデンシー グラフにアクセスできるポイント(updateDG())と、描画 API (OpenGL または DirectX)にアクセスできるポイント(updateDevice())間が明確に分割されています。中間データは、EndUpdate() がコールされるときにクリーンアップできます。たとえば、オーバーライドに、指定されたノードのアトリビュートからの入力が必要な場合があります。

オーバーライドは、シェーディングに半透明が含まれるどうかに関するヒントを提供することができます。このヒントは、updateDevice()endUpdate() の間で呼びだされる isTransparent() メソッドをオーバーライドして提供することができます。

ディスプレイスメントなどの高度なシェーディング エフェクトによって、シェーディングされるオブジェクトのサイズが変更されることがあります。この場合は、誤ったタイミングでフラスタム カリングされないように、オブジェクトのバウンディング ボックスを調整することをお勧めします。これは、boundingBoxExtraScale() メソッドをオーバーライドして実現することができます。これによって、オーバーライドでオブジェクトの絶対値バウンディング ボックスを提供することはできなくなります。代わりに、オーバーライドは、計算されたバウンディング ボックスに適用されるスケール係数を提供します。これは、多数のシェーダが同じオブジェクトに影響を与える可能性があり、各シェーダは他のすべての要件を把握できないからです。

描画フェーズは、純粋な仮想 draw() メソッドによって実行されます。このメソッドは、正常に描画できる場合は true を返します。false を返す場合、描画はサポートされていないマテリアルで使用される既定のシェーダを使用して行われます。描画は意図的にデータ更新と混合されません。描画がコールされる時点で、すべての評価が完了している必要があります。更新フェーズと描画フェーズ間で受け渡す必要があるユーザ データがある場合は、オーバーライドでそのデータそのものをキャッシュする必要があります。描画中に Maya のディペンデンシー グラフにアクセスするとエラーになり、この操作によって不安定になることがあります。すべてのシェーダの設定とジオメトリの描画を処理する draw() メソッドの実装は可能ですが、draw() メソッドの使用が必要なのは、シェーダの設定に対してのみです。この後に、drawGeometry() がコールされて、Maya でジオメトリの描画を処理することができます。手動でのジオメトリのバインドが必要な場合は、draw() メソッドに渡されるレンダー項目リスト内の各レンダー項目のジオメトリを使用して、ハードウェア リソース ハンドルを照会できます。

また、activateKey() および terminateKey() メソッドは、レンダー項目が別のシェーダ キーを使用して描画されるたびに描画フェーズでも呼び出されます。activateKey() および terminateKey() メソッドは、同じシェーダ キーを共有している draw() コールのバッチに対して一度だけレンダリング状態を構成してレンダリングを最適化するために使用できます。すべてが同じシェーダ キーを返す 3 つのシェーダ オーバーライド(A、B、C)の場合、起動のサンプル シーケンスは次のようになります。

shaderOverrideA->activateKey(...)
shaderOverrideA->draw(...)
shaderOverrideB->draw(...)
shaderOverrideC->draw(...)
shaderOverrideA->terminateKey(...)
注:terminateKey() コールバックは、activateKey() コールバックの起動に使用されるコールバックとして、常に同じ MPxShaderOverride インスタンスで起動されます。

すべての描画メソッドは、MDrawContext パラメータを使用して描画コンテキストにアクセスします。これは、テクスチャ マネージャとともに、可能な限り、状態およびテクスチャを管理するために使用する必要があります。これらのインタフェースを使用すると(未処理の描画 API コールを行うのとは対照的に)、パフォーマンスが向上し、デバイス状態の破損に関する問題を回避できます。

handlesDraw() メソッドは、activateKey()draw() および terminateKey() の前にコールされます。このメソッドを使用して、オーバーライドは、現在の描画コンテキストに基づいて描画を処理するかどうかを決定できます。たとえば、カラー パスの描画は処理しても、シャドウ マップ作成パスの描画は処理しないことを選択できます。このメソッドから false が返された場合、activateKey()draw() および terminateKey() はコールされません。

Maya SDK の例「hwPhongShader」は、MPxShaderOverride の簡単な使用例を示しています。プラグインは、MPxHwShaderNode を使用するノードを定義し、クラス 「hwPhongShaderOverride」に MPxShaderOverride を実装してビューポート 2.0 のサポートを提供します。オーバーライドの draw() メソッドでは、(コンパイル時の定数によって制御される)描画の両方のメソッドが示されます。最初の例では、シェーダを設定してから、drawGeometry() をコールして描画を実行します。2 番目の例では、シェーダの設定後にすべてのジオメトリ バインドを手動で実行し、手動で OpenGL コール glDrawElements() を行って描画を実行します。

MPxShaderOverride の使用法の詳細な例については、CgFX プラグイン(cgFxShaderNode.h/.cpp)および dx11Shader プラグイン(dx11ShaderOverride.h/.cpp)を参照してください。dxShaderOverride には、handlesDraw() の使用例が含まれています。

統合に関する考慮事項

MShaderInstance の使用

オーバーライドでシェーダ インスタンスを使用している場合、オーバーライドごとに 1 つのシェーダ インスタンスを保持するのではなく、固有のグローバル セットを使用する必要があります。詳細については、「シェーダ インスタンス」の「統合に関する考慮事項」を参照してください。

統合

統合が必要でない場合、次をオーバーライドすることによってこれを示すことができます。

MPxShaderOverride::handlesConsolidatedGeometry() 仮想メソッド

これを設定しない場合、項目を統合できるかどうかの判断に、シェーダ オプションが使用されます。

dx11ShaderglslShader、および hwPhongShader Developer Kit プラグインを参照してください。また、最初の 2 つの例では、セマンティックとして公開されています。「ビューポート 2.0 の dx11Shader および glslShader プラグインでサポートされるセマンティックと注釈」を参照してください。

シェーダ キーの独自性以外に、カスタム データの使用も、MRenderItem の独自性を決めるため、レンダー項目の統合に影響します。つまり、異なるデータ ポインタを持つレンダー項目は、項目の統合が起こらないことを意味します。統合のサポートが必要な場合、これを考慮する必要があります。

統合情報を抽出する

シェーダでは、描画時に Maya オブジェクトごとの情報抽出が必要になることがあります。

これを実現する方法の 1 つは、シェーダを使用するレンダー項目が統合されないように強制することです。その結果、シェーダ セットアップと描画の呼び出しが、オブジェクト単位でセットになります。この動作は、シェーダ キー、カスタム データの使用、または MPxShaderOverride::handlesConsolidatedGeometry() オーバーライドによって、統合されないものとして項目にマークを付けることで実現できます。

統合時に分離を強制する代わりの方法は、レンダー項目が統合されることは許可して、描画時にパスおよびジオメトリの情報を抽出するというものです。

シャドウ マップ パスのように、オブジェクトごとの抽出を必要としないパスでは、統合されたジオメトリ全体を描画する既定の動作は維持できます。

抽出を必要としないパスでは、まず MRenderItem::isConsolidated() メソッドを使用して、統合されたジオメトリがレンダー項目に含まれているかどうかを確認する必要があります。含まれている場合、MRenderItem::sourceIndexMapping() を使用して、Maya DAG パスのリスト、および関連するインデックス範囲を取得できます。インデックス範囲は、描画時に使用できる、統合されたジオメトリ データ バッファ内で、パスごとのジオメトリがある場所を示します。

MRenderItem::sourceDagPath() メソッドでは 1 つのパスしか返すことができないので、統合されたジオメトリ パスの抽出には適していません。

hwPhongShader プラグインからの次のサンプル コードでは、基本的なロジックを示しています。

const MHWRender::MRenderItem* renderItem = renderItemList.itemAt(renderItemIdx);
const MHWRender::MGeometry* geometry = renderItem->geometry();

MHWRender::MGeometryIndexMapping geometryIndexMapping;
if (renderItem->isConsolidated())
{
    renderItem->sourceIndexMapping(geometryIndexMapping);

    // Extract information if the geometry was consolidated
    for (int i=0; i<geometryIndexMapping.geometryCount(); i++)
    {
        // Pull out the dag path, any component, and the start and end   
        // indexing into the geometry buffers.
        MDagPath path = geometryIndexMapping.dagPath(i);
        MObject comp = geometryIndexMapping.component(i);
        int indexStart = geometryIndexMapping.indexStart(i);
        int indexLength = geometryIndexMapping.indexLength(i);
    }
}
else
{
    // Have unconsolidated geometry. The indexing thus spans the entire
    // range of the buffers returned.
    MDagPath sourceDagPath = renderItem->sourceDagPath();
}

具体的な例として、pPlaneShape1 および pPlaneShape2 の 2 つのオブジェクトがあるシーンを考えます。これらには同じプラグイン シェーダ(hwPhong)が割り当てられています。次のイメージでは、各オブジェクトのトポロジ(頂点およびフェースの ID)が示されています。三角形分割は点線で示されています。

オブジェクトごとのジオメトリ データにアクセスするには、最初にインデックス情報を抽出する必要があります。インデックス情報は、描画時に返されるレンダー項目(MRenderItem)のジオメトリ(MGeometry)を調べることにより返されます。

MRenderItem renderItem; // Passed at draw time.

// Get the geometry 
const MHWRender::MGeometry* geometry = renderItem->geometry();

// Get the index type
MHWRender::MGeometry::Primitive indexPrimType = renderItem->primitive();
MString indexPrimTypeName = MHWRender::MGeometry::primitiveString(indexPrimType);
const MHWRender::MIndexBuffer* buffer = geometry->indexBuffer(0);

// Get the data type
MString dataType = MHWRender::MGeometry::dataTypeString(buffer->dataType()); 

// Get the size of the index buffer
int indexCount = geometry->indexBufferCount();  

// Dump the indexing for debugging purposes. Note that when geometry is consolidated, the
// index can be unsigned short.
MHWRender::MIndexBuffer* nonConstIB = const_cast<MHWRender::MIndexBuffer*>(buffer);
if (buffer->dataType() == MHWRender::MGeometry::kUnsignedInt32)
{
    const unsigned int *ptr = (const unsigned int*)nonConstIB->map();
    for (unsigned int i=0; i<indexBufferCount; i++)
    {
        const unsigned int index = ptr[i];
        fprintf(stderr, "-- index[%d] = %d\n", i, index);
    }
    nonConstIB->unmap();
}
else
{
    const unsigned short *ptr = (const unsigned short*)nonConstIB->map();
    for (unsigned int i=0; i<indexBufferCount; i++)
    {
        const unsigned short index = ptr[i];
        fprintf(stderr, "-- index[%d] = %d\n", i, index);
    }
    nonConstIB->unmap();
}	

2 つのオブジェクトの間で統合が可能な場合、返される情報は次のようになります。プレーン テキストおよび太字は、インデックスが元のオブジェクトとどのように関係しているかを示すために使用されています。太字pPlaneShape1 を示すために使用されています。

Indexing Primitive Type = Triangles,

Index type = Unsigned Int 16,

Index count = 18

Index array:

index[0] = 0

index[1] = 1

index[2] = 3

index[3] = 3

index[4] = 1

index[5] = 2

index[6] = 4

index[7] = 5

index[8] = 7

index[9] = 7

index[10] = 5

index[11] = 6

index[12] = 8

index[13] = 9

index[14] = 11

index[15] = 11

index[16] = 9

index[17] = 10

ジオメトリについて提供される統合情報は次のとおりです。

  • 最初のジオメトリでは、パス = |pPlane2|pPlaneShape2、開始インデックス = 0、インデックスの長さ = 12 となっています。このジオメトリには 2 つの四角形(または 4 つの三角形)があるため、こちらのインデックスの方が長くなっています。

  • 2 番目のジオメトリでは、パス = |pPlane1|pPlaneShape1、開始インデックス = 12、インデックスの長さ = 6 となっています。このインデックスは、1 つの四角形(2 つの三角形)に対するものです。

注:ジオメトリが統合されている場合、32 ビット インデックスではなく、16 ビット インデックス(符号なしの short)が返されます。

次に示すのは、頂点およびフェースの識別子情報のダンプ結果です。

vertexid[0] = 0.000000

faceid[0] = 0.000000
vertexid[1] = 1.000000 faceid[1] = 0.000000
vertexid[2] = 4.000000 faceid[2] = 0.000000
vertexid[3] = 3.000000 faceid[3] = 0.000000
vertexid[4] = 1.000000 faceid[4] = 1.000000
vertexid[5] = 2.000000 faceid[5] = 1.000000
vertexid[6] = 5.000000 faceid[6] = 1.000000
vertexid[7] = 4.000000 faceid[7] = 1.000000
vertexid[8] = 0.000000 faceid[8] = 0.000000
vertexid[9] = 1.000000 faceid[9] = 0.000000
vertexid[10] = 3.000000 faceid[10] = 0.000000
vertexid[11] = 2.000000 faceid[11] = 0.000000

各データ バッファの最初の 8 つのエントリは pPlaneShape2 に関するものであり、各データ バッファの最後の 4 つのエントリは pPlaneShape1 に関するものです。

データ バッファの検索用に提供されたインデックスをたどることにより、次のような三角形ごとの頂点 ID が得られます。

[0,1,3], [3,1,4], [1,2,4], [4,2,5], [0,1,2], [2,1,3]

最初の 4 つの三角形は pPlaneShape2 のものであり、最後の 2 つの三角形は pPlaneShape1 のものです。

対応するフェース ID の検索からは、次のように結果が得られます。

[0,0,0],[0,0,0],[1,1,1],[1,1,1], [0,0,0], [0,0,0]

最初の 2 つの三角形(pPlaneShape2)はフェース 0 に属し、次の 2 つの三角形(pPlaneShape2)はフェース 1 に属し、最後の 2 つの三角形は pPlaneShape1 のフェース 0 に属しています。

注:フェースおよび頂点の識別子情報は、シェーダの初期化中に適切な vertexid および faceid セマンティックを使用して頂点ストリームを照会することにより生成されます。

旧式の既定ビューポート(ビューポート 1)とビューポート 2.0 の描画ロジック

ビューポート 1 では、ジオメトリの統合と同等のものがありませんが、各シェーダの描画のために一連のジオメトリを送り返そうとすることがあります。これは、常にシェーダをバインドおよびバインド解除することを避けるための最適化です。

MPxHwShaderNode インタフェースの呼び出しパターンは、次のようになります。

  1. glBind() により、シェーダをバインド
  2. glGeometry()
    1. パス A のストリームをバインド
    2. パス A のインデックスをバインド
    3. 描画(Draw)
  3. glGeometry()
    1. パス B のストリームをバインド
    2. パス B のインデックスをバインド
    3. 描画(Draw)
  4. さらに、オブジェクトのインスタンスごとに glGeometry() の呼び出し
  5. glUnbind() により、シェーダをバインド解除

一方、ビューポート 2.0 の MPxShaderOverride インタフェースの呼び出しパターンは、次のようになります。

  1. activateKey() により、シェーダ インスタンスをバインド
  2. draw()
    1. 統合されたジオメトリのストリームをバインド
    2. インデックス範囲ごとの描画が必要な場合:
      1. インデックス範囲を設定
      2. 描画(Draw)
    3. それ以外の場合、カスタム描画ロジックを選択
  3. terminateKey() により、シェーダをバインド解除

ビューポート 2.0 では、すべての情報について描画は 1 回だけなので、次のようになります。

  • オブジェクトごとのバインドではなく、統合されたジオメトリのバインドが 1 回だけとなります。
  • これにより、ビューポート 1 のロジックとは対照的に、プラグインでジオメトリの描画方法を最終的に決定する際の柔軟性が得られます。ビューポート 1 のロジックでは、予め設定された、インデックス バッファとデータ バッファ ストリームのセットを使用して、一定回数プラグインを呼び出します。

フレームごとの作業

すべてのシェーダに共通するフレームごとの作業を存在させることが可能です。この場合、いくつかのオプションが使用できます。

  • MPxShaderOverride のコンテキスト内で、フレーム スタンプを取得するための呼び出しが使用できます(MDrawContext::getFrameStamp())。たとえば、シェーダを複数回呼び出すときの、コストがかかるライティングの再計算を避けるために、glslShader および dx11Shader プラグインでは、フレーム スタンプの変更時のみ計算を実行します。

  • ビューポート 2.0 の通知コールバックは、グローバル レベルで使用できます。(MRenderer::addNotification(MHWRender::MPassContext::kBeginRenderSemantic))。

  • ビューポートごとの MUiMessage コールバック(add3dViewPreRenderMsgCallback())は、引き続きビューポート 2.0 でグローバル レベルの使用が可能です。

フレームと描画コンテキスト

フレームと描画コンテキストの詳細については、「フレームと描画のコンテキスト」を参照してください。