月別アーカイブ: 2013年11月

glTF を触ってみた

OpenGL(webGL)向けの、ランタイム用のアセットのフォーマットを見てみたいと思います。
[glTF – Runtime asset format for WebGL, OpenGL ES, and OpenGL]

glTFとは

もともとは、COLLADA2jSONとして作られたもののようですが、現在はglTFという名称になっています。KhronosのCOLLADAワーキンググループで、仕様の策定が行われているようです。
COLLADAフォーマットは、DCCツール間のアセット交換や出力を主目的として作られているため、ランタイムでの読み込みという意味では、抽象的過ぎて扱いづらい側面がありましたが、glTFは、ランタイムでの読み込みと再生を主眼に置いたものとなっているようです。
前身となったCOLLADA2jSONの名が示すとおり、JSON形式が出力フォーマットですが、コンバーターのwriterクラスを書き換えれば、任意の形式で格納可能だと思われますが、当面はWebGLがメインのプラットフォームだとみなすことができると思います。ちなみに、TFはTransmissionFormatの略だそうです。

glTFの中身

アニメーションのデータなど入るようですが、とりあえずはジオメトリのデータの格納構造を見てみたいと思います。glTFのgitを取得すると、サンプルのモデルとして、duckが入っているので、これを見てみます。
https://github.com/KhronosGroup/glTF

頂点配列の格納形式

頂点配列に限らず、おそらく大きな配列は以下の構造になると思われます。”buffers”内に、配列の実体となるファイルや、内容そのものの、配列が定義されるようです。
“bufferViews”には、”buffers”内に定義された実体に対し、各要素に対応するビューを定義します。
下記の例では、ducks.binファイルに格納されたbufferに、頂点データ配列と、頂点インデックス配列のためのbufferViewを定義しています。

"bufferViews": {
    "bufferView_24": {
        "buffer": "duck.bin",
        "byteLength": 76768,
        "byteOffset": 0,
        "target": "ARRAY_BUFFER"
    },
    "bufferView_25": {
        "buffer": "duck.bin",
        "byteLength": 25272,
        "byteOffset": 76768,
        "target": "ELEMENT_ARRAY_BUFFER"
    }
},

"buffers": {
    "duck.bin": {
        "byteLength": 102040,
        "path": "duck.bin"
    }
},

頂点アトリビュートの格納形式

頂点アトリビュートは、頂点バッファのbufferViewを参照し、offset,stride,countとtypeを定義します。
下記の例では、いずれも上記で定義した、”bufferView_24″のARRAY_BUFFERを参照し、
頂点位置、法線、UVと思われる3つのアトリビュートを定義しているようです。

"attributes": {
    "attribute_18": {
        "bufferView": "bufferView_24",
        "byteOffset": 0,
        "byteStride": 12,
        "count": 2399,
        "max": [
            96.1799,
            163.97,
            53.9252
        ],
        "min": [
            -69.2985,
            9.92937,
            -61.3282
        ],
        "type": "FLOAT_VEC3"
    },
    "attribute_20": {
        "bufferView": "bufferView_24",
        "byteOffset": 28788,
        "byteStride": 12,
        "count": 2399,
        "max": [
            0.999599,
            0.999581,
            0.998436
        ],
        "min": [
            -0.999084,
            -1,
            -0.999832
        ],
        "type": "FLOAT_VEC3"
    },
    "attribute_22": {
        "bufferView": "bufferView_24",
        "byteOffset": 57576,
        "byteStride": 8,
        "count": 2399,
        "max": [
            0.983346,
            0.980037
        ],
        "min": [
            0.026409,
            0.019963
        ],
        "type": "FLOAT_VEC2"
    }
},

プリミティブの格納形式

プリミティブでは、最初に定義した、頂点インデックスバッファのbufferViewを参照し、加えて、頂点アトリビュートと、セマンティクスの対応付けと、プリミティブ形状を定義します。
とりあえずここまでで、頂点バッファの作成から、DrawCallまでを行うことができそうな情報が格納されているのが分かります。
マテリアルも参照されていますが、これは後ほど見てみます。

"primitives": [
    {
        "indices": {
            "bufferView": "bufferView_25",
            "byteOffset": 0,
            "count": 12636,
            "type": "UNSIGNED_SHORT"
        },
        "material": "material.0",
        "primitive": "TRIANGLES",
        "semantics": {
            "NORMAL": "attribute_20",
            "POSITION": "attribute_18",
            "TEXCOORD_0": "attribute_22"
        }
    }
]

メッシュの格納形式

メッシュは、プリミティブの上位レイヤーに該当するもので、上記で説明した、頂点アトリビュートと、プリミティブの配列を内包します。

"meshes": {
    "LOD3spShape-lib": {
        "attributes": {.....},
        "name": "LOD3spShape",
        "primitives": [.....]
    }
},

ノードの格納形式

ノードは、メッシュや、カメラ、ライトなどの位置情報と親子関係を記述した、シーングラフに相当します。
ここまでの情報で、ModelViewマトリクスを設定して、DrawCallが行うことができそうな情報が格納されているのが分かります。

"nodes": {
    "node_0": {
        "children": [],
        "matrix": [1,  0,  0,  0,
                         0,  1,  0,  0,
                         0,  0,  1,  0,
                         0,  0,  0,  1],
        "meshes": [
            "LOD3spShape-lib"
        ],
        "name": "LOD3sp"
    },
},

シェーダーの格納形式

まず、使用するシェーダーコードを、”shaders”に定義し、使用するシェーダーの組み合わを”programs”内に定義します。さらに、シェーダーが外部参照するパラメーター(UniformやVertexAttribute,Texture)と、使用すべきRenderStateを”techniques”に定義します。

"shaders": {
    "technique1Fs": {
        "path": "technique1Fs.glsl"
    },
    "technique1Vs": {
        "path": "technique1Vs.glsl"
    }
},

"programs": {
    "program_0": {
        "fragmentShader": "technique1Fs",
        "vertexShader": "technique1Vs"
    }
},

"techniques": {
    "technique1": {
        "parameters": {},
        "pass": "defaultPass",
        "passes": {
            "defaultPass": {
                "instanceProgram": {
                    "attributes": [
                        {
                            "semantic": "NORMAL",
                            "symbol": "a_normal",
                            "type": "FLOAT_VEC3"
                        },
                        {
                            "semantic": "POSITION",
                            "symbol": "a_position",
                            "type": "FLOAT_VEC3"
                        },
                        {
                            "semantic": "TEXCOORD_0",
                            "symbol": "a_texcoord0",
                            "type": "FLOAT_VEC2"
                        }
                    ],
                    "program": "program_0",
                    "uniforms": [
                        {
                            "semantic": "WORLDVIEWINVERSETRANSPOSE",
                            "symbol": "u_normalMatrix",
                            "type": "FLOAT_MAT3"
                        },
                        {
                            "semantic": "WORLDVIEW",
                            "symbol": "u_worldviewMatrix",
                            "type": "FLOAT_MAT4"
                        },
                        {
                            "semantic": "PROJECTION",
                            "symbol": "u_projectionMatrix",
                            "type": "FLOAT_MAT4"
                        },
                        {
                            "parameter": "diffuse",
                            "symbol": "u_diffuseTexture",
                            "type": "SAMPLER_2D"
                        }
                    ]
                },
                "states": {
                    "blendEnable": false,
                    "cullFaceEnable": true,
                    "depthMask": true,
                    "depthTestEnable": true
                }
            }
        }
    }
},

マテリアルの格納形式

マテリアルは、自身が使用するtechniqueを参照し、シェーダーに対して設定すべき値が記述されています。
以下の例では、設定すべきテクスチャと、shininessの値があります。
shininessの値は、使用されているtechniqueに、Uniformの設定が無いので、おそらく無視されるのではないかと思われます。

"images": {
    "image_0": {
        "path": "duckCM.png"
    }
},

"materials": {
    "material.0": {
        "name": "blinn3",
        "technique": "technique1",
        "techniques": {
            "technique1": {
                "parameters": {
                    "diffuse": {
                        "image": "image_0",
                        "magFilter": "LINEAR",
                        "minFilter": "LINEAR_MIPMAP_LINEAR",
                        "type": "SAMPLER_2D",
                        "wrapS": "REPEAT",
                        "wrapT": "REPEAT"
                    },
                    "shininess": {
                        "type": "FLOAT",
                        "value": 38.4
                    }
                }
            }
        }
    }
},

とりあえず読み込んでみた

後学のため、自前でglTF-parser.jsを継承したオブジェクトを作って、サンプルにあるDuckのモデルを、読み込んでみました。(ちなみに、Duckのモデルは、model/duckと、loaders/threejs/duckに格納されているが、後者の方が新しいものと思われる。)
https://googledrive.com/host/0B79CBbJe5Ux1MF9yNFh5SlRZNlU/index.html
このプログラムは、汎用的には書いていないので、Duck以外のモデルは、おそらく正しくロードできないと思います。
ジオメトリやテクスチャ、シェーダーを読み込むあたりは比較的短時間で書けたのですが、マテリアル関連の部分が、少々面倒に感じました。
technique内のpassが配列である部分や、material内でtechniqueが配列である部分などは、少々冗長に感じます。その割には、samplerの設定がマテリアル内にあったりするので、このあたりは今後改善されていくのではないかと思います。

広告

フルスクリーン描画時のリフレッシュレート

DirectXで、フルスクリーン描画をする際のディスプレイのリフレッシュレートについてです。

最近のPC向けディスプレイ

近年のPC向けディスプレイは、さまざまな垂直同期周波数を持っているケースがあります。具体的には、60Hzのほかに、120Hzや、144Hzなどです。
これ等のリフレッシュレートは、垂直同期を有効にした場合でも、60Hzのディスプレイに比べ、高頻度に画面を書き換える機会を有しています。また、垂直同期を無効にした場合でも、同様に、高頻度に画面を書き換える機会を有しているため、ティアリングによる不快感を軽減(分断された絵の差分を最小限に)できる可能性があります。

ゲーム製作者側の留意点

ゲーム製作者側が留意しなくてはならないのは、当然ながら、垂直同期:有効=60Hzという前提でゲームを製作しないということです。これに関しては、殆どのPC向けのゲームでは、フレーム間の時間間隔は可変となっているので、問題になることは無いと思います。また、垂直同期を無効にしたケースではプログラム側は特に留意することは無いように思えます。
ただし一点だけ気をつけたほうが良いことがあります。それは、フルスクリーン描画を行う際の、RefreshRateの設定値です。デバイス作成時に設定するRefreshRateは、そのままディスプレイの垂直同期周波数として設定され、D3DPRESENT_INTERVALがどのように設定されても、その値が使用されます。
つまり、120Hzの垂直同期周波数をサポートするディスプレイでは、60Hz/120Hzいずれにも設定して、垂直同期を無効にしてフルスクリーン描画を行うことができます。その際に観測されるFPSは、基本的には全く差が無い状態となりますが、ディスプレイの駆動周波数は異なるため、プレイヤーの体験としては違うものになってしまいます。
では、どうすれば良いかですが、現在のディスプレイのRefreshRateを取得し、同一のRefreshRateで使用可能なスクリーン解像度を使用すると良いと思います。また、ゲーム内のメニューから明示的に設定できるようにしても良いと思います。

初期化の例

以下の例では、現在設定されているRefreshRateを尊重しつつ、19×10解像度のフルスクリーン描画の初期化を行うコードです。解像度が存在しない場合の処理などは省略されています。

if( NULL == ( g_pD3D = Direct3DCreate9( D3D_SDK_VERSION ) ) )
  return E_FAIL;

D3DDISPLAYMODE curDM;
g_pD3D->GetAdapterDisplayMode(D3DADAPTER_DEFAULT, &curDM);

D3DDISPLAYMODE targetDM;
UINT targetRes[2] = {1920, 1080};

ZeroMemory(&targetDM, sizeof(targetDM));

// check if there is 19x10 resolution with current refresh rate and color depth.
UINT cnt = g_pD3D->GetAdapterModeCount(D3DADAPTER_DEFAULT,
      curDM.Format);
for (UINT i=0; iEnumAdapterModes(D3DADAPTER_DEFAULT,
  curDM.Format, i, &dm);

  if (curDM.Format == dm.Format &&
      curDM.RefreshRate == dm.RefreshRate &&
      targetRes[0] == dm.Width &&
      targetRes[1] == dm.Height) {
    targetDM = dm;
    break;
  }
}

// you'll need proper handler...
if (targetDM.Width != targetRes[0])
  return E_FAIL;

static D3DPRESENT_PARAMETERS d3dpp = {
  targetDM.Width, 
  targetDM.Height,
  targetDM.Format,
  2,
  D3DMULTISAMPLE_NONE,
  0,
  D3DSWAPEFFECT_DISCARD,
  hWnd,
  0, //Full screen
  0,
  D3DFMT_UNKNOWN,
  0,
  targetDM.RefreshRate, // set the refresh rate that have been set.
   //D3DPRESENT_INTERVAL_ONE // enable v-sync
  D3DPRESENT_INTERVAL_IMMEDIATE // disable v-sync
};

// create a device.
if( FAILED( g_pD3D->CreateDevice( D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hWnd,
 D3DCREATE_SOFTWARE_VERTEXPROCESSING,
 &d3dpp, &g_pd3dDevice ) ) )