01−01、Direct3Dの描画方法



必要なデータ
OpenRDBはDirect3Dでオブジェクトを描画します。
オブジェクトは三角形の集まりとして描画します。
この三角形の集まりを描画するためには、大雑把に言うと頂点データとインデックスデータと頂点宣言データが必要です。
これらはdisp4/INCLUDE/DispObj.hで次のように宣言されています。

LPDIRECT3DVERTEXBUFFER9 m_VB;
LPDIRECT3DINDEXBUFFER9 m_IB;

m_VBが頂点データで、m_IBがインデックスデータです。
インデックスデータには頂点の順番の整数が格納されますが
頂点データに何を格納するかはプログラマが自由に決めることが出来ます。

頂点データの構成を決めるデータをVertexDeclarationといいます。
DispObj.hで次のように宣言されています。

IDirect3DVertexDeclaration9* m_dispvdecls;

まず頂点宣言の作成コードから見てみます。
disp4/CPP/DispObj.cppのCreateDeclで頂点宣言データを作成します。

D3DVERTEXELEMENT9 vdecl[] = {
    //pos[4]
    { 0, 0, D3DDECLTYPE_FLOAT4, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_POSITION, 0 },
    //normal[3]
    { 0, 16, D3DDECLTYPE_FLOAT3, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_NORMAL, 0 },
    //uv
    { 0, 28, D3DDECLTYPE_FLOAT2, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_TEXCOORD, 0 },
    D3DDECL_END()
 };
HRESULT hr;
hr = m_pdev->CreateVertexDeclaration( vdecl, &m_dispvdecl );

ここでは頂点の構成は4次元の位置座標(pos[4])と3次元の法線(normal[3])と2次元のuvにしています。
D3DDECLTYPE_FLOATの後の数字でfloatいくつ分のデータなのかを宣言します。
{}の中の2番目の数字は0, 16, 28となっていますが、これはデータの始まりのバイト数を表します。
pos[4]の始まりは先頭なので0。
normal[3]の始まりはpos[4]のデータの後なので4バイト(float1個分) X 4=16バイトで16となります。
uvの始まりはpos[4], normal[3]の後なので16 + 4バイト X 3 =28バイトで28となります。

描画用の頂点データとインデックスデータはビデオメモリに作成します。
ビデオメモリに作成すると描画が速くなりますが
プログラム側からデータにアクセスするのには、システムメモリへのアクセスの何倍も時間が掛かります。
ですので通常、システムメモリにも頂点データを用意しておきます。

システムメモリにはDeclで宣言した構成と同じ構成の構造体を作成します。
この構造体はdisp4/INCLUDE/coef.hでPM3DISPVで宣言しています。

typedef struct tag_pm3dispv{
    D3DXVECTOR4 pos;
    D3DXVECTOR3 normal;
    D3DXVECTOR2 uv;
}PM3DISPV;

頂点データとインデックスデータを作成している部分は
DispObj.cppのCreateVBandIB()です。

UINT elemleng;
    DWORD curFVF;
    elemleng = sizeof( PM3DISPV );
    curFVF = 0;
    hr = m_pdev->CreateVertexBuffer( m_pm3->m_optleng * elemleng,
        D3DUSAGE_WRITEONLY, curFVF,
        D3DPOOL_MANAGED,
        &m_VB, NULL );

    hr = m_pdev->CreateIndexBuffer( m_pm3->m_facenum * 3 * sizeof(int),
        0,
        D3DFMT_INDEX32, 
        D3DPOOL_MANAGED,
        &m_IB, NULL );

上のようにして作成します。
データのセットはLockしてからシステムメモリからビデオメモリにmemcpyし、Unlockします。


頂点データとインデックスデータの関係を説明します。
n番目の頂点データの要素をV(n)で表し、n番目のインデックスデータの頂点をI(n)で表すとします。

三角を描画するのでインデックスは3個ずつのセットでそれぞれの三角の頂点の順序を表します。

つまり1個目の三角の頂点データは
V( I(0) )とV( I(1) )とV( I(2) )です。
2個目の三角の頂点データは
V( I(3) )とV( I(4) )とV( I(5) )です。



描画命令

三角を描画するにはDrawIndexedPrimitiveというDirect3Dの関数を使います。
この関数はDispObj.cppのRenderNormal関数で呼んでいます。

m_pdev->SetVertexDeclaration( m_dispvdecl );
m_pdev->SetStreamSource( 0, m_VB, 0, sizeof(PM3DISPV) );
m_pdev->SetIndices( m_IB );
m_pdev->DrawIndexedPrimitive( D3DPT_TRIANGLELIST,
    0,
    0,
    m_pm3->m_optleng,
    currb->startface * 3,
    curnumprim
);

上記のコードのように説明した3つのデータを使用してDirect3Dの描画命令を呼び出します。



シェーダー

OpenRDBでは描画にプログラマブルシェーダーを使います。
頂点シェーダーとピクセルシェーダーを使います。

頂点シェーダーは三角を構成する頂点1個につき1回呼ばれます。
それぞれの呼び出しは独立していて、隣の頂点の情報などは参照できません。
渡された1個の頂点の座標変換とライティングをします。

ピクセルシェーダーは描画する三角の画面上でのピクセル1個に付き1回呼び出されます。
ピクセルシェーダーの入力引数は頂点シェーダーの出力です。
ピクセルシェーダーの入力に複数の頂点情報を与えない限り、渡されたピクセル1個のみの処理をします。
ただし、テクスチャ情報は渡されたUVの近接ピクセル情報を複数計算に使用することは出来ます。
ですが出力は1回の呼び出しに1ピクセル分です。

OpenRDB第1章のシェーダーは
Media/Shader/Ochakko.fxです。

fxファイルには定数と頂点シェーダーとピクセルシェーダーとテクニックが記述されています。
テクニックというのはどの頂点シェーダーとピクセルシェーダーを使うのかを指定しやすくするために
そのペアをまとめてそれに名前を付けられるようにしたものです。

以下にOchakko.fxのテクニックを記述します。

technique RenderL1
{
    pass P0
    {
        VertexShader = compile vs_3_0 RenderSceneVS( 1 );
        PixelShader  = compile ps_3_0 RenderScenePSTex();
    }
    pass P1
    {
        VertexShader = compile vs_3_0 RenderSceneVS( 1 );
        PixelShader  = compile ps_3_0 RenderScenePSNotex();
    }
}

technique RenderL2
{
    pass P0
    {
        VertexShader = compile vs_3_0 RenderSceneVS( 2 );
        PixelShader  = compile ps_3_0 RenderScenePSTex();
    }
  pass P1
    {
        VertexShader = compile vs_3_0 RenderSceneVS( 2 );
        PixelShader  = compile ps_3_0 RenderScenePSNotex();
    }
}

technique RenderL3
{
    pass P0
    {
        VertexShader = compile vs_3_0 RenderSceneVS( 3 );
        PixelShader  = compile ps_3_0 RenderScenePSTex();
    }
    pass P1
    {
        VertexShader = compile vs_3_0 RenderSceneVS( 3 );
        PixelShader  = compile ps_3_0 RenderScenePSNotex();
    }
}

techniqueの次に書かれている「RenderL1」「RenderL2」「RenderL3」がそれぞれのテクニック名です。
それぞれのテクニックの中にはpass(パス)が2つあります。
パスの中には頂点シェーダーとピクセルシェーダーの関数名のペアが書いてあります。

Ochakko.fxではライトの数でテクニックを分けます。
3つのライトまで対応するのでテクニックは3つです。

各テクニックの中にパスが2つありますが
良く見るとこれらのパスの頂点シェーダーは同じでピクセルシェーダーだけが違います。

テクスチャが貼ってあるポリゴンの描画用とテクスチャの貼っていないポリゴンの描画用で2つあります。

なぜ分けるかというと、テクスチャが設定されていない状態でテクスチャを参照する命令を呼ぶと
環境によってはエラーで固まってしまうためです。

まとめると、ライトの数でテクニックを分け、テクスチャの有無でパスを分けています。


次にシェーダーの定数部分を見てみましょう。

texture g_MeshTexture;// Color texture for mesh
float4 g_diffuse;
float3 g_ambient;
float3 g_specular;
float g_power;
float3 g_emissive;

int g_nNumLights;
float3 g_LightDir[3];// Light's direction in world space
float4 g_LightDiffuse[3];// Light's diffuse color
float3 g_LightAmbient;// Light's ambient color

float g_fTime;// App's time in seconds
float4x4 g_mWorld;// World matrix for object
float4x4 g_mWVP;// World * View * Projection matrix
float3 g_EyePos;

シェーダーにおける定数とは、シェーダー内では参照しかせずC++側から数値を設定可能なものです。
ここではテクスチャとマテリアルとライトと座標変換行列、視点の位置を定数にしています。

次にいよいよ頂点シェーダーを見てみましょう。

struct VS_OUTPUT
{
    float4 Position : POSITION;// vertex position
    float4 Diffuse : COLOR0;// vertex diffuse color (note that COLOR0 is clamped from 0..1)
    float3 Specular : TEXCOORD0;
    float2 TextureUV : TEXCOORD1;// vertex texture coords
};


    VS_OUTPUT RenderSceneVS( float4 vPos : POSITION,
        float3 vNormal : NORMAL,
        float2 vTexCoord0 : TEXCOORD0,
        uniform int nNumLights )
{
    VS_OUTPUT Output;
    float3 wPos;
    wPos = mul( vPos, (float3x3)g_mWorld );

    Output.Position = mul(vPos, g_mWVP);
    float3 wNormal;
    wNormal = normalize(mul(vNormal, (float3x3)g_mWorld)); // normal (world space)

    float3 totaldiffuse = float3(0,0,0);
    float3 totalspecular = float3(0,0,0);
    float calcpower = g_power * 0.1f;

    for(int i=0; i<nNumLights; i++ ){
        float nl;
        float3 h;
        float nh;
        float4 tmplight;
        nl = dot( wNormal, g_LightDir[i] );
        h = normalize( ( g_LightDir[i] + g_EyePos - wPos ) * 0.5f );
        nh = dot( wNormal, h );

        totaldiffuse += g_LightDiffuse[i] * max(0,dot(wNormal, g_LightDir[i]));
        totalspecular +=  ((nl) < 0) || ((nh) < 0) ? 0 : ((nh) * calcpower);
    }

    Output.Diffuse.rgb = g_diffuse.rgb * totaldiffuse.rgb
        + g_ambient + g_emissive;
    Output.Diffuse.a = g_diffuse.a;
    Output.Specular = g_specular * totalspecular;
    Output.TextureUV = vTexCoord0;

    return Output;
}

まず頂点シェーダーが返す構造体を定義します。
見慣れないのは「:」とそのあとの文字列です。
「:」の後はセマンティクスと呼ばれるもので、その要素をどういう用途に使うのかを指定するものです。
「: Position」は位置座標ですということを宣言するものです。
「: COLOR0」はRDBAの色を表します。
float3 Specularの後は: Specularではありません。
Specularというセマンティクスは無いのです。
こういうときは代わりにテクスチャ座標用の「: TEXCOORDn」を使います。
TextureUVもテクスチャ用のセマンティクスを使用します。

RenderSceneVSが頂点シェーダーの関数名です。
その引数は上記の「必要なデータ」のところで説明した頂点宣言と構成が一致していないといけません。
uniform int nNumLightsはテクニックから指定する追加の引数で、ライトの数を表します。

このシェーダーでは頂点座標の変換とライティングを行っています。

座標変換はワールド変換行列g_mWorldによる変換と、ワールドビュープロジェクション変換行列g_mWVPによる変換を行っています。
頂点の画面上の座標を求めるだけならワールドビュープロジェクションの変換1回だけで済みます。
ライティングのためにワールドだけの変換もしています。
これは画面上の座標でライトの計算をするよりも、ワールド座標系でライトの計算をした方が簡単できれいに計算できるからです。

for文がライティングの部分です。
ライトの数だけ拡散光と反射光を計算して足し算しています。
そしてその結果にアンビエント(環境光)とエミッシブ(自己照明)を足しています。

UV座標は入力引数で受け取ったものをそのまま出力します。

この出力がピクセルシェーダーの入力に成ります。
頂点の数とピクセルの数は1:1ではないので
DirectXが自動的に、ピクセルの位置に相当する入力値を、複数の頂点の出力を補間して求めてピクセルシェーダーに渡します。

ピクセルシェーダを見てみましょう。

struct PS_OUTPUT
{
    float4 RGBColor : COLOR0;// Pixel color
};

PS_OUTPUT RenderScenePSTex( VS_OUTPUT In ) 
{
    PS_OUTPUT Output;
    Output.RGBColor = tex2D(MeshTextureSampler, In.TextureUV) * In.Diffuse + float4( In.Specular, 0 );
    return Output;
}

PS_OUTPUT RenderScenePSNotex( VS_OUTPUT In )
{
    PS_OUTPUT Output;
    Output.RGBColor = In.Diffuse + float4( In.Specular, 0 );
    return Output;
}

ピクセルシェーダは色だけを出力します。
RenderScenePSTexがテクスチャ有りのピクセルシェーダーで
RenderScenePSNoTexがテクスチャ無しのピクセルシェーダーです。

ピクセルシェーダーの入力は頂点シェーダーの出力ですが
気をつけないといけないのは、ピクセルシェーダで「: Position」の値を参照することは出来ないということです。
ピクセルシェーダーで位置が欲しいときは「: TEXCOORDn」を使って別に座標を渡さないといけません。

テクスチャが有るときはテクスチャの色に頂点シェーダーの色を掛け算します。
テクスチャが無いときは頂点シェーダーの色をピクセルの色とします。


01章のトップに戻る

オープンソースのトップに戻る

トップページに戻る