MyMaterial

MyMaterial.png


FireMonkey では、3Dコンポーネントへ色や模様などの質感を与えるために、TColorMaterialSource,TTextureMaterialSource,TLightMaterialSource というの3つのマテリアルコンポーネントが標準で用意されている(シェーディングの計算が間違っていて使い物にならない)が、鏡面や凸凹などのさらに複雑な質感を与えたい場合には、自分で独自のマテリアルを作らねばならない。

それには内部的に、シェーダ言語である HLSL(DIrect3D)と GLSL(OpenGL)を用いて、光の振る舞いを物理的に計算しなくてはならないが、変数の受け渡しなどの周辺のコーディングが煩雑すぎるというのは、前回のデブキャンで解明した通りである。

そこで、変数を型別にクラス化し、必要な変数オブジェクトを組み合わせるだけで、変数受け渡しのコーディングを自動化するフレームワークを提案する。

※ HLSL をGLSL へ変換する方法が分からないので、現状では Windows 環境のみでのサポート。

なお、HLSL は基本的に、Microsoft 提供の外部コンパイラによって、一旦バイナリへ変換してからプログラムへ取り込むのがセオリーであるが、今回はコンパイル関数を含む d3dcompiler_47.dll を内包することで、HLSL のテキストコードからの自動コンパイルを実現する。

▼ ソースは以下のリポジトリにて。


今回制作したフレームワークは、LUX.FireMonkey.Material.pas に含まれている。

※ HLSL のコンパイラ関数 D3DCompile を用いるために、d3d11shader.h,d3dcommon.h,d3dcompiler.h の3つのヘッダを移植した。

シェーダ本体を表すクラスが「TShaderSource」であり、その他の「TShaderVar*」というクラス群が、シェーダに渡す変数の型に対応するクラスである。

  • TShaderVar
    • TShaderVarPrim
      • TShaderVarFloat :Single型変数
      • TShaderVarFloat2 :Single型×2変数
      • TShaderVarFloat3 :Single型×3変数
      • TShaderVarVector :TVector3D 型変数
      • TShaderVarColor :TAlphaColor 型変数
      • TShaderVarMatrix :TMatrix3D 型変数
      • TShaderVarTexture :TLight 型変数
    • TShaderVarLights :TLight 型変数
  • TShaderSource
    • TShaderSourceV :バーテックスシェーダ
    • TShaderSourceP :ピクセルシェーダ

具体的にこれらのクラスを利用しているのが LIB.Material.pas である。

TMyMaterialSource は、実際に 3Dコンポーネントへ紐付けるマテリアルコンポーネント本体であるが、その実は CreateMaterial メソッドによって生成された TMaterial のラッパーであり、中身はほぼない。

     TMyMaterialSource = class( TMaterialSource )
     private
     protected
       _Texture :TBitmap;
       ///// アクセス
       function GetEmisColor :TAlphaColor;
       procedure SetEmisColor( const EmisColor_:TAlphaColor );
       function GetAmbiColor :TAlphaColor;
       procedure SetAmbiColor( const AmbiColor_:TAlphaColor );
       function GetDiffColor :TAlphaColor;
       procedure SetDiffColor( const DiffColor_:TAlphaColor );
       function GetSpecColor :TAlphaColor;
       procedure SetSpecColor( const SpecColor_:TAlphaColor );
       function GetSpecShiny :Single;
       procedure SetSpecShiny( const SpecShiny_:Single );
       procedure SetTexture( const Texture_:TBitmap );
       ///// メソッド
       function CreateMaterial: TMaterial; override;
       procedure DoTextureChanged( Sender_:TObject );
     public
       constructor Create( AOwner_:TComponent ); override;
       destructor Destroy; override;
       ///// プロパティ
       property EmisColor :TAlphaColor read GetEmisColor write SetEmisColor;
       property AmbiColor :TAlphaColor read GetAmbiColor write SetAmbiColor;
       property DiffColor :TAlphaColor read GetDiffColor write SetDiffColor;
       property SpecColor :TAlphaColor read GetSpecColor write SetSpecColor;
       property SpecShiny :Single      read GetSpecShiny write SetSpecShiny;
       property Texture   :TBitmap     read _Texture     write SetTexture;
     end;

重要なのは TMyMaterial である。

ここでは、TShaderVar* クラスを用いて、色などのシェーディングに必要な変数定義され、TShaderSource* クラスを用いて、バーテックス/ピクセルシェーダが定義されている。

TMyMaterial = class( TMaterial )
     private
     protected
       _FMatrixMVP :TShaderVarMatrix;
       _FMatrixMV  :TShaderVarMatrix;
       _TIMatrixMV :TShaderVarMatrix;
       _EyePos     :TShaderVarVector;
       _Opacity    :TShaderVarFloat;
       _EmisColor  :TShaderVarColor;
       _AmbiColor  :TShaderVarColor;
       _DiffColor  :TShaderVarColor;
       _SpecColor  :TShaderVarColor;
       _SpecShiny  :TShaderVarFloat;
       _Lights     :TShaderVarLights;
       _Texture    :TShaderVarTexture;
       _ShaderV    :TShaderSourceV;
       _ShaderP    :TShaderSourceP;
       ///// メソッド
       procedure DoInitialize; override;
       procedure DoApply( const Context_:TContext3D ); override;
     public
       constructor Create; override;
       destructor Destroy; override;
       ///// プロパティ
       property EmisColor :TShaderVarColor   read _EmisColor;
       property AmbiColor :TShaderVarColor   read _AmbiColor;
       property DiffColor :TShaderVarColor   read _DiffColor;
       property SpecColor :TShaderVarColor   read _SpecColor;
       property SpecShiny :TShaderVarFloat   read _SpecShiny;
       property Texture   :TShaderVarTexture read _Texture;
     end;

TMyMaterial.Create では、それぞれのクラスを生成しているが、注目すべきは、シェーダクラスへ変数クラスを登録している部分である(★1)。これだけで、変数の受け渡し準備が完了し、レジスタ番号の指定などの面倒なコーディングが不要となる。ちなみにシェーダ自体のコードは、LoadFromFile メソッドにより、外部のテキストファイルから読み込む(★2)。

constructor TMyMaterial.Create;
begin
     inherited;

     _FMatrixMVP := TShaderVarMatrix .Create( 'FMatrixMVP'  );
     _FMatrixMV  := TShaderVarMatrix .Create( 'FMatrixMV'   );
     _TIMatrixMV := TShaderVarMatrix .Create( 'IMatrixMV'   );
     _EyePos     := TShaderVarVector .Create( '_EyePos'     );
     _Opacity    := TShaderVarFloat  .Create( '_Opacity'    );
     _EmisColor  := TShaderVarColor  .Create( '_EmisColor'  );
     _AmbiColor  := TShaderVarColor  .Create( '_AmbiColor'  );
     _DiffColor  := TShaderVarColor  .Create( '_DiffColor'  );
     _SpecColor  := TShaderVarColor  .Create( '_SpecColor'  );
     _SpecShiny  := TShaderVarFloat  .Create( '_SpecShiny'  );
     _Lights     := TShaderVarLights .Create( '_Light'      );
     _Texture    := TShaderVarTexture.Create( '_Texture'    );

     _ShaderV := TShaderSourceV.Create;
     _ShaderP := TShaderSourceP.Create;

     _EmisColor.Value := TAlphaColors.Null;
     _AmbiColor.Value := $FF202020;
     _DiffColor.Value := $FFFFFFFF;
     _SpecColor.Value := $FF606060;
     _SpecShiny.Value := 30;

     with _ShaderV do
     begin
          Vars := [ _FMatrixMVP,
                    _FMatrixMV ,
                    _TIMatrixMV ]; ★1

          LoadFromFile( '..\..\_SHADER\MyMaterial.hvs' ); ★2
     end;

     with _ShaderP do
     begin
          Vars := [ _FMatrixMVP,
                    _FMatrixMV ,
                    _TIMatrixMV,
                    _EyePos    ,
                    _Opacity   ,
                    _EmisColor ,
                    _AmbiColor ,
                    _DiffColor ,
                    _SpecColor ,
                    _SpecShiny ,
                    _Lights    ,
                    _Texture    ]; ★1

          LoadFromFile( '..\..\_SHADER\MyMaterial.hps' ); ★2
     end;
end;

なお、自動的に変数宣言の HLSL コードを生成し組み込むため、シェーダコード側で外部変数を記述する必要はない。

// float4x4  FMatrixMVP : register( c00 );
// float4x4  FMatrixMV  : register( c04 );
// float4x4  IMatrixMV  : register( c08 ); 

static float4x4 _FMatrixMVP = transpose( FMatrixMVP );
static float4x4 _FMatrixMV  = transpose( FMatrixMV  );
static float4x4 _IMatrixMV  = transpose( IMatrixMV  );

struct TSenderV               //頂点の変数型
{
    float4 Pos :POSITION;     //位置(ローカル)
    float4 Nor :NORMAL;       //法線(ローカル)
    float4 Tex :TEXCOORD;     //テクスチャ座標
};

struct TResultV               //フラグメントの変数型
{
    float4 Scr :SV_Position;  //位置(スクリーン)
    float4 Pos :TEXCOORD0;    //位置(グローバル)
    float4 Nor :NORMAL;       //法線(グローバル)
    float4 Tex :TEXCOORD1;    //テクスチャ座標
};

TResultV MainV( TSenderV _Sender )
{
    TResultV _Result;

    _Result.Scr = mul( _Sender.Pos, _FMatrixMVP );
    _Result.Pos = mul( _Sender.Pos, _FMatrixMV  );
    _Result.Nor = mul( _Sender.Nor, _IMatrixMV  );
    _Result.Tex =      _Sender.Tex;

    return _Result;
}
    

そして、シェーダへ変数を送り込んでいるのが DoApply メソッドである。

座標変換行列などの実行時に必要な値は、その場で変数クラスへ代入する。

その上で、TShaderSource.SendVars メソッドにより、紐付けられたすべての変数クラスのデータがシェーダ側へ転送される。

procedure TMyMaterial.DoApply( const Context_:TContext3D );
begin
     inherited;

     with Context_ do
     begin
          SetShaders( _ShaderV.Shader, _ShaderP.Shader );

          _FMatrixMVP.Value := CurrentModelViewProjectionMatrix;
          _FMatrixMV .Value := CurrentMatrix;
          _TIMatrixMV.Value := CurrentMatrix.Inverse.Transpose;
          _EyePos    .Value := CurrentCameraInvMatrix.M[ 3 ];
          _Opacity   .Value := CurrentOpacity;
          _Lights    .Value := Lights[ 0 ];
     end;

     _ShaderV.SendVars( Context_ );
     _ShaderP.SendVars( Context_ );
end;