FMX 3D ノーマルマップに挑戦

Delphi Advent Calender 2014 の23日を担当します。p_katoです。

中山雅紀さんが第29回デベロッパーキャンプで発表された【A4】Delphi/C++テクニカルセッション「FireMonkeyの仕組み」を使って，ノーマルマップの作成に挑戦します。

ですので，Windows版のみのHLSLによる実装になります。

ノーマルマップは，接空間に展開された法線情報をノーマルマップテクスチャからピクセル単位に取り出して，オブジェクトに適用するシェーダです。

詳しくはIKDさんの「○×つくろ～どっとコム」の以下のページを参考にしました。
http://marupeke296.com/DXPS_S_No5_NormalMap.html

ですので実装は以下のような流れになります。
1)TMaterialSource
　ノーマルマップテクスチャを読み込んでシェーダに適用する。
2)TMaterial
　ピクセルスシェーダにノーマルマップを渡す。
3)バーテックスシェーダ(HLSL)
　接空間をグローバル空間に変換する行列を生成する
4)ピクセルシェーダ(HLSL)
　バーテックスシェーダから必要な情報を受け取り法線マップの切空間上の法線ベクトルを変換して利用可能なものにして適用する。

では作業に入ってみましょう。

中山雅紀さんのTMaterialSourceにノーマルマップを追加します。

```Delphi:
type
   TMtlSrcNormalMap = class(TMaterialSource)
   private
     FNormalMap:TBitmap;
     ・・・・
     procedure SetNormalMap(const Value: TBitmap);
     // イベント
     procedure DoNormalMapChanged(Sender:TObject);
   protected
     ・・・・
   public
     // プロパティ
     property NormalMap:TBitmap     read FNormalMap   write SetNormalMap;
   end;

constructor TMtlSrcNormalMap.Create(AOwner: TComponent);
var
  NBitmap:TBitmap;
begin
  inherited;
  FNormalMap         :=TTextureBitmap.Create;
  FNormalMap.OnChange:=DoNormalMapChanged;
  NBitmap:=TBitmap.Create(128,128); // ノーマルマップのデフォルトの値を最初に入れておく
  NBitmap.Clear($FF7F7FFF);
	FNormalMap.Assign(NBitmap);
  NBitmap.Free;
end;

procedure TMtlSrcNormalMap.SetNormalMap(const Value: TBitmap);
begin
  FNormalMap.Assign(Value);
end;

procedure TMtlSrcNormalMap.DoNormalMapChanged(Sender: TObject);
begin
  if not FNormalmap.IsEmpty then begin
    TMtlNormalMap(Material).NormalMap:=TTextureBitmap(FNormalMap).Texture;
  end;
end;

destructor TMtlSrcNormalMap.Destroy;
begin
  ・・・・
  FreeAndNil(FNormalMap);
  inherited;
end;
```

次に TMaterialに以下のコードを追加します。

```Delphi:
type
  TMtlNomalMap = class(TCustomMaterial)
  private
    ・・・・
  protected
    ・・・・
    [Weak] _NormalMap   :TTexture;
    ///// アクセス
    ・・・・
    procedure SetNormalMap(const Value:TTexture);
    ・・・・
     procedure DoNormalMapChanged(Sender:TObject);
  public
    ・・・・
    property Texture   :TTexture    read FTexture   write SetTexture;
  end;

procedure TMtlNomalMap.SetNormalMap(const Value: TTexture);
begin
  _NormalMap:= Value;
  DoChange;
end;

procedure TMtlNomalMap.InitShaderP; // ピクセルシェーダ
var
  CSS:array[0..1] of TContextShaderSource;
begin
  CSS[0] := TContextShaderSource.Create(
    TContextShaderArch.DX9,
    ResourceToBytes('・・・・'),
    [
      ・・・・
      // ノーマルマップを渡すことを宣言する
      TContextShaderVariable.Create('PS_NormalMap', TContextShaderVariableKind.Texture, 0, 0)
    ]
 );
  CSS[1] := TContextShaderSource.Create(
    TContextShaderArch.DX11_level_9,
    FileToBytes('・・・・・'),
    [
      ・・・・
      // ノーマルマップを渡すことを宣言する
      TContextShaderVariable.Create('PS_NormalMap', TContextShaderVariableKind.Texture, 0, 0)
    ]
 );
  ・・・・
end;

procedure TMyMaterial.DoApply(const AContext: TContext3D);
begin
  inherited;
  with AContext do begin
    ・・・・
    SetShaderVariable('PS_NormalMap', _NormalMap);
  end;
end;
```

次に，バーテックスシェーダを作ります。
コンパイルして，TMaterialクラスで読み込んで使います。
ピクセルシェーダで直接受け取れないTANGENTとBINORMALを受け渡します。

```
// TMaterialクラスからバーテックスシェーダに渡される外部変数
float4x4  FMatrixMVP: register( c00 );  static float4x4 _FMatrixMVP = transpose( FMatrixMVP );
float4x4 _FMatrixMV:  register( c04 );
float4x4  IMatrixMV:  register( c08 );  static float4x4 _IMatrixMV  = transpose( IMatrixMV  );

// バーテックスシェーダが内部的に受け取る引数の宣言
struct TSender
{
  float4 Pos: POSITION;
  float4 Nor: NORMAL;
  float4 Tex: TEXCOORD;
  float4 Tng: TANGENT;  // 接空間の計算に必要な引数
  float4 Bnl: BINORMAL; // 接空間の計算に必要な引数
};

// ピクセルシェーダに渡す引数
struct TResult
{
  float4 Scr: SV_Position;
  float4 Pos :TEXCOORD0;
  float4 Nor: NORMAL;
  float4 Tex: TEXCOORD1;
  float4 Tng: TEXCOORD2;
  float4 Bnl: TEXCOORD3;
};

TResult main( TSender _Sender )
{
  TResult _Result;
  ・・・・
  // 接空間に変換するための TANGENTとBINORMALを渡す
  _Result.Tng = mul(_Sender.Tng, _IMatrixMV);
  _Result.Bnl = mul(_Sender.Bnl, _IMatrixMV);

  return _Result;
}
```

ピクセルシェーダは以下のように追加・変更します。

```
// ライトの情報
struct TLight
{
  ・・・・
};

// TMaterialクラスから受け取る外部変数
float4x4  FMatrixMVP : register( c00 );  static float4x4 _FMatrixMVP = transpose( FMatrixMVP );
float4x4 _FMatrixMV  : register( c04 );
float4x4  IMatrixMV  : register( c08 );  static float4x4 _IMatrixMV  = transpose( IMatrixMV  );
・・・・
// ノーマルマップを外部変数として追加
Texture2D<float4> _NMap:    register( t1 );

// テクスチャの読み込み設定
SamplerState _SamplerState {};

// 接空間座標を通常空間に変換する
float4x4 TangentMatrix(
  float4 tangent,
  float4 binormal,
  float4 normal )
{
  float4x4 mat = {
    {normalize( tangent.xyz),   0},
    {normalize( binormal.xyz ), 0},
    {normalize( normal.xyz ),   0},
    {0,0,0,1}
  };
  return mat;
}
// バーテックスシェーダから受け取る引数
struct TSender
{
  float4 Scr: SV_Position;
  float4 Pos :TEXCOORD0;
  float4 Nor: NORMAL;
  float4 Tex: TEXCOORD1;
  float4 Tng: TEXCOORD2;
  float4 Bnl: TEXCOORD3;
};

// ピクセルシェーダの出力結果
struct TResult
{
  float4 Col :SV_Target;
};

TResult main( TSender _Sender )
{
  TResult _Result;
  float4 NC = float4(_NMap.Sample( _SamplerState, _Sender.Tex.xy).xyz, 0); // 法線の色を取得
  NC = normalize(2*NC-1.0f);                                               // ベクトルに変換
  NC = mul(NC, TangentMatrix(_Sender.Tng, _Sender.Bnl, _Sender.Nor));
  float3 N = normalize(NC.xyz);                                            // 法線ベクトルを計算
  ・・・・ // 法線ベクトルを使ってオブジェクトの色を決定する
  return _Result;
}
```

このマテリアルを3Dオブジェクトに適用してあげれば完成，，しません，，，

もう一つ大事なことがあります。それはバーテックスシェーダで出てきたTANGENTとBINORMALです。
これは頂点の法線ベクトルNORMALから計算できるのですが，3Dオブジェクト固有の情報なので，シェーダではなく3Dオブジェクト側に持たせる方か計算量が減ります。オブジェクトの法線ベクトルの計算をする度に計算するとよいです。
この計算はTMEshData.CalcTangentBinormalsという手続きで計算できます。
http://docwiki.embarcadero.com/Libraries/XE7/ja/FMX.Types3D.TMeshData.CalcTangentBinormals
しかし，TSphereなどの3Dオブジェクトでは隠蔽されて使えません。そこで以下のようなクラスヘルパを使いました。

```Delphi:
type
  HCustomMesh = class helper for TCustomMesh
  public
    procedure CalcTangentBinormals;
  end;

procedure HCustomMesh.CalcTangentBinormals;
begin
  Data.CalcTangentBinormals;
end;
```

これをTSphereで使うと，CalcFaceNormalsも実行されるので球の継ぎ目が目立ってしまいます。より深いソースコードから，この部分を取り除いた手続きを作った方が良いかもしれません。

まとめ：法線マップは以下のような手順でDelphi XE7で実装することが可能です。
1)ノーマルマップを適用する3Dオブジェクトは法線ベクトルが変化するごとにTangentBinormalsをする。
2)ノーマルマップテクスチャを用意して，TMaterialSourceクラスからテクスチャと同じ方法でTMaterialクラスに渡す
3)TMaterialクラスでは，ピクセルシェーダの初期化にテクスチャと同じ方法でノーマルマップの引数を追加する。
4)バーテックスシェーダで，TANGENTとBINORMALを受け取り，オブジェクトの位置情報と同様の座標変換を行ってからピクセルシェーダに渡す。
5)テクスチャと同じ方法でノーマルマップの情報を取り出し，取り出した接空間上の法線ベクトルを変換して，シェーダの処理に渡す

駆け足でしたが，法線マップの実装方法についてご説明しました。皆様のお役に立てば幸いです。
まだ良い方法があると思いますので，お気づきになられたら是非教えてください！！