ずっと板ポリもあれなので,今回はPMDフォーマットを表示させてみます。 モデルデータのプロ生ちゃんはプログラミング生放送様よりダウンロードして使用させて頂いております。
モデルデータは利用ガイドラインを読んだうえで,各自でダウンロードしてください。
今回はMikuMikuDance(MMD)で使用されているPolygon Model Data(PMD)フォーマットを読み込んでみます。
とりあえず表示だけやりたかったので,後でPBRに移行するというのもあるのでライティングはなしで,テクスチャカラーを表示しているだけの手抜きです。
さて,PMDフォーマットですが”通りすがりの記憶”さんのページに詳しいものがまとまっています。
まず,ファイルの構成は下記のようになっているようです。
PMD ファイル
* ヘッダ
* 頂点リスト
* 面リスト
* 材質リスト
* ボーンリスト
* IKリスト
* 表情リスト
* 表情枠用表示リスト
* ボーン枠用枠名リスト
* ボーン枠用表示リスト
— 拡張部分 —
* ヘッダ(英語)
* ボーンリスト(英語)
* 表情リスト(英語)
* ボーン枠用枠名リスト(英語)
* トゥーンテクスチャリスト
* 剛体リスト
* ジョイントリスト
モデルを表示するだけなら,拡張部分は全くいりませんが後でコリジョンをとりたいなどの場合に剛体リストとジョイントリストを使用することになるので,一応すべて読み込んでおくことにしました。
拡張部分というのが必ず存在するのかどうかわからなかったので,一応EOFになっているかどうかをチェックしながら読み取るようにプログラムを組んでみました。そのため最後の方はちょっとコードが汚いです。
データ構造は下記のように定義しました。
///////////////////////////////////////////////////////////////////////////////////////////////////
// PMD_EDGE_FLAG
///////////////////////////////////////////////////////////////////////////////////////////////////
enum PMD_EDGE_FLAG
{
PMD_EDGE_FLAG_DEFAULT = 0, //!< 通常.
PMD_EDGE_FLAG_DISABLE = 1 //!< エッジ無効.
};
///////////////////////////////////////////////////////////////////////////////////////////////////
// PMD_BONE_TYPE enum
///////////////////////////////////////////////////////////////////////////////////////////////////
enum PMD_BONE_TYPE
{
PMD_BONE_TYPE_ROTATE = 0, //!< 回転.
PMD_BONE_TYPE_ROTATE_AND_TARNSLATE = 1, //!< 回転と移動.
PMD_BONE_TYPE_IK = 2, //!< IK
PMD_BONE_TYPE_UNKNOWN = 3, //!< 不明.
PMD_BONE_TYPE_UNDER_INFLUENCE_OF_IK = 4, //!< IK影響下.
PMD_BONE_TYPE_UNDER_INFLUENCE_OF_ROTATE = 5, //!< 回転影響下.
PMD_BONE_TYPE_TARGET_IK = 6, //!< IK接続先.
PMD_BONE_TYPE_INVISIBLE = 7, //!< 非表示.
PMD_BONE_TYPE_TWIST = 8, //!< 捻り.
PMD_BONE_TYPE_ROTATRY_MOTION = 9, //!< 回転運動.
};
///////////////////////////////////////////////////////////////////////////////////////////////////
// PMD_MORPH_TYPE enum
///////////////////////////////////////////////////////////////////////////////////////////////////
enum PMD_MORPH_TYPE
{
PMD_MORPH_TYPE_BASE = 0, //!< base
PMD_MORPH_TYPE_EYEBROW = 1, //!< 眉.
PMD_MORPH_TYPE_EYE = 2, //!< 目.
PMD_MORPH_TYPE_LIP = 3, //!< 唇.
PMD_MORPT_TYPE_OTHER = 4, //!< その他.
};
///////////////////////////////////////////////////////////////////////////////////////////////////
// PMD_COLLISION_SHAPE_TYPE enum
///////////////////////////////////////////////////////////////////////////////////////////////////
enum PMD_COLLISION_SHAPE_TYPE
{
PMD_COLLISION_SHAPE_TYPE_SPHERE = 0, //!< 球.
PMD_COLLISION_SHAPE_TYPE_BOX = 1, //!< 箱.
PMD_COLLISION_SHAPE_TYPE_CAPSULE = 2, //!< カプセル.
};
///////////////////////////////////////////////////////////////////////////////////////////////////
// PMD_RIGIDBODY_TYPE
///////////////////////////////////////////////////////////////////////////////////////////////////
enum PMD_RIGIDBODY_TYPE
{
PMD_RIGIDBODY_TYPE_FLLOW_BONE = 0, //!< ボーン追従.
PMD_RIGIDBODY_TYPE_PHYSICS = 1, //!< 物理演算.
PMD_RIGIDBODY_TYPE_PHYSICS_FLLOW_POSITION = 2, //!< 物理演算(ボーン位置追従).
};
///////////////////////////////////////////////////////////////////////////////////////////////////
// PMD_HEADER strcture
///////////////////////////////////////////////////////////////////////////////////////////////////
#pragma pack( push, 1 )
struct PMD_HEADER
{
u8 Magic[3]; //!< ファイルマジック "Pmd"
f32 Version; //!< ファイルバージョン.
char8 ModelName[20]; //!< モデル名.
char8 Comment[256]; //!< コメント.
};
#pragma pack( pop )
///////////////////////////////////////////////////////////////////////////////////////////////////
// PMD_VERTEX structure
///////////////////////////////////////////////////////////////////////////////////////////////////
#pragma pack( push, 1 )
struct PMD_VERTEX
{
asdx::Vector3 Position; //!< 位置座標.
asdx::Vector3 Normal; //!< 法線ベクトル.
asdx::Vector2 TexCoord; //!< テクスチャ座標.
u16 BoneIndex[2]; //!< ボーン番号. モデル変形(頂点移動)時に影響.
u8 BoneWeight; //!< ボーン0へ与える影響度 min: 0, max : 100 (ボーン1への影響度は 100 - BoneWeight).
u8 EdgeFlag; //!< エッジフラグ.
};
#pragma pack( pop )
///////////////////////////////////////////////////////////////////////////////////////////////////
// PMD_MATERIAL structure
///////////////////////////////////////////////////////////////////////////////////////////////////
#pragma pack( push, 1 )
struct PMD_MATERIAL
{
asdx::Vector3 Diffuse; //!< 拡散反射
f32 Alpha; //!< 透過度.
f32 Power; //!< 鏡面反射強度.
asdx::Vector3 Specular; //!< 鏡面反射.
asdx::Vector3 Emissive; //!< 自己照明.
u8 ToonIndex; //!< トゥーンテクスチャ番号.
u8 VisualFlag; //!< 輪郭. 影
u32 VertexCount; //!< 頂点数.
char8 TextureName[20]; //!< テクスチャファイル名(非NULL終端であることに注意).
};
#pragma pack( pop )
///////////////////////////////////////////////////////////////////////////////////////////////////
// PMD_BONE structure
///////////////////////////////////////////////////////////////////////////////////////////////////
#pragma pack( push, 1 )
struct PMD_BONE
{
char8 Name[20]; //!< ボーン名.
u16 ParentIndex; //!< 親ボーンの番号(親がいない場合は 0xFFFF).
u16 TailIndex; //!< 末尾のボーン番号
u8 BoneType; //!< ボーンの種類.
u16 IKBoneIndex; //!< IKボーン番号(影響IKボーン。無い場合は0)
asdx::Vector3 Position; //!< ボーンのヘッド位置.
};
#pragma pack( pop )
///////////////////////////////////////////////////////////////////////////////////////////////////
// PMD_IK structure
///////////////////////////////////////////////////////////////////////////////////////////////////
#pragma pack( push, 1 )
struct PMD_IK
{
u16 BoneIndex; //!< IKボーン番号.
u16 TargetBoneIndex; //!< IKターゲットボーン番号(IKボーンが最初に接続するボーン)
u8 ChainCount; //!< IKチェーンの長さ(子供の数)
u16 RecursiveCount; //!< 再帰演算回数.
f32 ControlWeight; //!< 演算1回当たりの制限角度.
};
#pragma pack( pop )
///////////////////////////////////////////////////////////////////////////////////////////////////
// PMD_MORPH_VERTEX structure
///////////////////////////////////////////////////////////////////////////////////////////////////
struct PMD_MORPH_VERTEX
{
u32 Index; //!< 表情用の頂点番号.
asdx::Vector3 Position; //!< 表情用の頂点の位置座標(頂点自体の座標).
};
///////////////////////////////////////////////////////////////////////////////////////////////////
// PMD_MORPH structure
///////////////////////////////////////////////////////////////////////////////////////////////////
#pragma pack( push, 1 )
struct PMD_MORPH
{
char8 Name[20]; //!< 表情名.
u32 VertexCount; //!< 表情用の頂点数.
u8 Type; //!< 表情の種類.
};
#pragma pack( pop )
///////////////////////////////////////////////////////////////////////////////////////////////////
// PMD_BONE_LABEL_INDEX structure
///////////////////////////////////////////////////////////////////////////////////////////////////
#pragma pack( push, 1 )
struct PMD_BONE_LABEL_INDEX
{
u16 BoneIndex; //!< 表示枠用ボーン番号.
u8 FrameIndex; //!< 表示枠番号
};
#pragma pack( pop )
///////////////////////////////////////////////////////////////////////////////////////////////////
// PMD_LOCALIZE_HEADER structure
///////////////////////////////////////////////////////////////////////////////////////////////////
#pragma pack( push, 1 )
struct PMD_LOCALIZE_HEADER
{
u8 LocalizeFlag; //!< ローカライズフラグ(1 : 英名対応あり)
char8 ModelName[20]; //!< 英語名モデル.
char8 Comment[256]; //!< 英語コメント.
};
#pragma pack( pop )
///////////////////////////////////////////////////////////////////////////////////////////////////
// PMD_TOON_TEXTURE_LIST structure
///////////////////////////////////////////////////////////////////////////////////////////////////
#pragma pack( push, 1 )
struct PMD_TOON_TEXTURE_LIST
{
char8 FileName[10][100]; //!< トゥーンテクスチャファイル名.
};
#pragma pack( pop )
///////////////////////////////////////////////////////////////////////////////////////////////////
// PMD_RIGIDBODY structure
///////////////////////////////////////////////////////////////////////////////////////////////////
#pragma pack( push, 1 )
struct PMD_RIGIDBODY
{
char8 Name[20]; //!< 名前. (例:頭)
u16 RelationBoneIndex; //!< 関連ボーン番号.
u8 GroupIndex; //!< グループ番号.
u16 GroupTarget; //!< グループ対象.
u8 ShapeType; //!< 形状.
asdx::Vector3 ShapeSize; //!< 形状サイズ.
asdx::Vector3 Position; //!< 位置座標.
asdx::Vector3 Angle; //!< 回転角(ラジアン).
f32 Mass; //!< 質量.
f32 DampingTranslate; //!< 移動減衰.
f32 DampingRotation; //!< 回転減衰.
f32 Elastictiy; //!< 反発力.
f32 Friction; //!< 摩擦力.
u8 Type; //!< タイプ.
};
#pragma pack( pop )
///////////////////////////////////////////////////////////////////////////////////////////////////
// PMD_PHYSICS_JOINT structure
///////////////////////////////////////////////////////////////////////////////////////////////////
#pragma pack( push, 1 )
struct PMD_PHYSICS_JOINT
{
char8 Name[20]; //!< 名前.
u32 RigidBodyA; //!< 剛体A.
u32 RigidBodyB; //!< 剛体B.
asdx::Vector3 Position; //!< 位置座標.
asdx::Vector3 Angle; //!< 回転角.
asdx::Vector3 LimitPosition[2]; //!< 移動制限.
asdx::Vector3 LimitAngle[2]; //!< 回転制限.
asdx::Vector3 SpringPosition; //!< ばね 位置座標.
asdx::Vector3 SpringAngle; //!< ばな 回転角.
};
#pragma pack( pop )
///////////////////////////////////////////////////////////////////////////////////////////////////
// ResPmdIK structure
///////////////////////////////////////////////////////////////////////////////////////////////////
struct ResPmdIK
{
u16 BoneIndex; //!< IKボーン番号.
u16 TargetBoneIndex; //!< IKターゲットボーン番号.
u16 RecursiveCount; //!< 再帰演算回数.
f32 ControlWeight; //!< 演算1会当たりの制限角度.
std::vector<u16> ChildBoneIndices; //!< IK影響下のボーン番号.
};
///////////////////////////////////////////////////////////////////////////////////////////////////
// ResPmdMorph structure
///////////////////////////////////////////////////////////////////////////////////////////////////
struct ResPmdMorph
{
std::string Name; //!< 名前.
u8 Type; //!< 表情タイプ.
std::vector<PMD_MORPH_VERTEX> Vertices; //!< 表情用の頂点データ.
};
///////////////////////////////////////////////////////////////////////////////////////////////////
// ResPmd class
///////////////////////////////////////////////////////////////////////////////////////////////////
class ResPmd : public ILoadable, public IDisposable
{
//=============================================================================================
// list of friend classes and methods.
//=============================================================================================
/* NOTHING */
public:
//=============================================================================================
// public variables.
//=============================================================================================
std::string Name; //!< モデル名.
std::string Comment; //!< コメント.
f32 Version; //!< バージョン.
std::vector<PMD_VERTEX> Vertices; //!< 頂点データ.
std::vector<u16> Indices; //!< 頂点インデックスデータ.
std::vector<PMD_MATERIAL> Materials; //!< マテリアルデータ.
std::vector<PMD_BONE> Bones; //!< ボーンデータ.
std::vector<ResPmdIK> IKs; //!< IKデータ.
std::vector<ResPmdMorph> Morphes; //!< モーフデータ.
std::vector<u16> MorphLabelIndices; //!< 表情枠用表示リスト.
std::vector<std::string> BoneLabels; //!< ボーン枠用枠名リスト.
std::vector<PMD_BONE_LABEL_INDEX> BoneLabelIndices; //!< ボーン枠用表示リスおt.
PMD_TOON_TEXTURE_LIST ToonTextureList; //!< トゥーンテクスチャリスト.
std::vector<PMD_RIGIDBODY> RigidBodies; //!< 剛体データ.
std::vector<PMD_PHYSICS_JOINT> Joints; //!< ジョイントデータ.
//=============================================================================================
// public methods.
//=============================================================================================
//---------------------------------------------------------------------------------------------
//! @brief コンストラクタです
//---------------------------------------------------------------------------------------------
ResPmd();
//---------------------------------------------------------------------------------------------
//! @brief デストラクタです.
//---------------------------------------------------------------------------------------------
virtual ~ResPmd();
//---------------------------------------------------------------------------------------------
//! @brief ファイルから読み込みを行います.
//!
//! @param[in] filename ファイル名です.
//! @retval true 読み込みに成功.
//! @retval false 読み込みに失敗.
//---------------------------------------------------------------------------------------------
bool Load( const char16* filename ) override;
//---------------------------------------------------------------------------------------------
//! @brief 破棄処理を行います.
//---------------------------------------------------------------------------------------------
void Dispose() override;
};
非常に読み込みやすいフォーマットになっているので,プログラムの方はファイルを開いてデータ構造通りにfread()していけば簡単に読み込めます。
実装は下記のような感じです。
//-------------------------------------------------------------------------------------------------
// ファイルから読み込みを行います.
//-------------------------------------------------------------------------------------------------
bool ResPmd::Load( const char16* filename )
{
if ( filename == nullptr )
{
ELOG( "Error : Invalid Argument." );
return false;
}
FILE* pFile;
auto err = _wfopen_s( &pFile, filename, L"rb" );
if ( err != 0 )
{
ELOG( "Error : File Open Failed. filename = %s", filename );
return false;
}
// ヘッダ読み込み.
PMD_HEADER header;
fread( &header, sizeof(header), 1, pFile );
// ファイルマジックをチェック.
if ( header.Magic[0] != 'P' || header.Magic[1] != 'm' || header.Magic[2] != 'd' )
{
ELOG( "Error : Invalid File. filename = %s", filename );
return false;
}
Name = header.ModelName;
Version = header.Version;
Comment = header.Comment;
// 頂点リストを読み込み.
{
u32 vertexCount = 0;
fread( &vertexCount, sizeof(vertexCount), 1, pFile );
Vertices.resize( vertexCount );
for( u32 i=0; i<vertexCount; ++i )
{ fread( &Vertices[i], sizeof(PMD_VERTEX), 1, pFile ); }
}
// 頂点インデックスを読み込み.
{
u32 vertexCount = 0;
fread( &vertexCount, sizeof(vertexCount), 1, pFile );
Indices.resize(vertexCount);
for( u32 i=0; i<vertexCount; ++i )
{ fread( &Indices[i], sizeof(u16), 1, pFile ); }
}
// マテリアルを読み込み.
{
u32 materialCount = 0;
fread( &materialCount, sizeof(materialCount), 1, pFile );
Materials.resize( materialCount );
for( u32 i=0; i<materialCount; ++i )
{ fread( &Materials[i], sizeof(PMD_MATERIAL), 1, pFile ); }
}
// ボーンデータを読み込み.
{
u16 boneCount = 0;
fread( &boneCount, sizeof(boneCount), 1, pFile );
Bones.resize( boneCount );
for( u16 i=0; i<boneCount; ++i )
{ fread( &Bones[i], sizeof(PMD_BONE), 1, pFile ); }
}
// IKデータを読み込み.
{
u16 count = 0;
fread( &count, sizeof(count), 1, pFile );
IKs.resize( count );
for( u32 i=0; i<count; ++i )
{
PMD_IK data;
fread( &data, sizeof(data), 1, pFile );
IKs[i].BoneIndex = data.BoneIndex;
IKs[i].TargetBoneIndex = data.TargetBoneIndex;
IKs[i].RecursiveCount = data.RecursiveCount;
IKs[i].ControlWeight = data.ControlWeight;
IKs[i].ChildBoneIndices.resize( data.ChainCount );
for( u32 j=0; j<data.ChainCount; ++j )
{ fread( &IKs[i].ChildBoneIndices[j], sizeof(u16), 1, pFile ); }
}
}
// モーフデータを読み込み.
{
u16 morphCount = 0;
fread( &morphCount, sizeof(morphCount), 1, pFile );
Morphes.resize( morphCount );
for( u16 i=0; i<morphCount; ++i )
{
PMD_MORPH morph;
fread( &morph, sizeof(morph), 1, pFile );
Morphes[i].Name = morph.Name;
Morphes[i].Type = morph.Type;
Morphes[i].Vertices.resize( morph.VertexCount );
for( u32 j=0; j<morph.VertexCount; ++j )
{ fread( &Morphes[i].Vertices[j], sizeof(PMD_MORPH_VERTEX), 1, pFile ); }
}
}
// 表情枠用表示リスト.
{
u8 count = 0;
fread( &count, sizeof(count), 1, pFile );
MorphLabelIndices.resize( count );
for( u8 i=0; i<count; ++i )
{ fread( &MorphLabelIndices[i], sizeof(u16), 1, pFile ); }
}
// ボーン枠用枠名リスト.
{
u8 count = 0;
fread( &count, sizeof(count), 1, pFile );
BoneLabels.resize( count );
for( u32 i=0; i<count; ++i )
{
char name[50];
fread( name, sizeof(char), 50, pFile );
BoneLabels[i] = name;
}
}
// ボーン枠用表示リスト.
{
u32 count = 0;
fread( &count, sizeof(count), 1, pFile );
BoneLabelIndices.resize( count );
for( u32 i=0; i<count; ++i )
{ fread( &BoneLabelIndices[i], sizeof(PMD_BONE_LABEL_INDEX), 1, pFile ); }
}
// 拡張データ ローカライズデータ.
if ( feof( pFile ) == 0 )
{
PMD_LOCALIZE_HEADER headerEn;
fread( &headerEn, sizeof(headerEn), 1, pFile );
if ( headerEn.LocalizeFlag == 0x1 )
{
//std::string modelName = headerEn.ModelName;
//std::string comment = headerEn.Comment;
//ILOGA( "ModelName[EN] : %s", modelName.c_str() );
//ILOGA( "Comment[EN] : %s", comment.c_str() );
for( size_t i=0; i<Bones.size(); ++i )
{
char boneName[20];
fread( boneName, sizeof(char), 20, pFile );
//ILOGA( "BoneName[EN] : %s", boneName );
}
for( size_t i=0; i<Morphes.size() - 1; ++i )
{
char morphName[20];
fread( morphName, sizeof(char), 20, pFile );
//ILOGA( "MorphName[EN] : %s", morphName );
}
for( size_t i=0; i<BoneLabels.size(); ++i )
{
char label[50];
fread( label, sizeof(char), 50, pFile );
//ILOGA( "BoneLabel[EN] : %s", label );
}
}
}
// 拡張データ トゥーンテクスチャリスト.
if ( feof( pFile ) == 0 )
{
fread( &ToonTextureList, sizeof(ToonTextureList), 1, pFile );
}
// 拡張データ 剛体データ.
if ( feof( pFile ) == 0 )
{
u32 rigidBodyCount = 0;
fread( &rigidBodyCount, sizeof(rigidBodyCount), 1, pFile );
RigidBodies.resize( rigidBodyCount );
for( u32 i=0; i<rigidBodyCount; ++i )
{ fread( &RigidBodies[i], sizeof(PMD_RIGIDBODY), 1, pFile ); }
}
// 拡張データ ジョイントリスト.
if ( feof( pFile ) == 0 )
{
u32 jointCount = 0;
fread( &jointCount, sizeof(jointCount), 1, pFile );
Joints.resize( jointCount );
for( u32 i=0; i<jointCount; ++i )
{ fread( &Joints[i], sizeof(PMD_PHYSICS_JOINT), 1, pFile ); }
}
// ファイルを閉じる.
fclose( pFile );
// 正常終了.
return true;
}
色々なデータで読み込みを試していないので,拡張データのあたりで読み込み失敗とかエラーが発生するかもしれないので,その場合はうまく直してください。
一応これでPMDデータ自体は読み込みできるはずです。“初音ミク.pmd”と”プロ生ちゃん.pmd”でしか検証していませんが…。
さて実際に読み込んだデータを表示してみます。プロ生ちゃんのテクスチャデータがすべてpngで作られていますが,WICの読み込みを移植していない関係で読み込めません。 そこで,今回はpngデータをすべてddsに変換して読み込みを行いました。PMD EditorとDirectX Texture Toolを使って編集を行いました。
プログラムの解説に入ります。まずは先ほど実装した読み込みプログラムを使用して,PMDデータの読み込みを行い,テクスチャリストとテクスチャIDリストを構築します。
PMDのTextureNameには”test.dds*a.spa”のような感じで,テクスチャファイル名とスフィアファイル名が一緒に入る仕様のようなので,テクスチャファイル名のみを取り出します。これは505行目付近で行っています。
ピクセルシェーダでテクスチャマップがあるかどうかを判断すると条件分岐が出てきてしまうため,処理が重くなる可能性があります。そこで,PMDのマテリアルデータにテクスチャがない箇所はダミーのテクスチャを割り当てし,これをピクセルシェーダ内でフェッチするようにします。 もちろんダミーテクスチャをフェッチしても問題がないように,ダミーのテクスチャは全部白色(1.0f, 1.0f, 1.0f, 1.0f)を入れてあります。
{
std::wstring path;
if ( !asdx::SearchFilePathW( L"res/pronama/プロ生ちゃん.pmd", path ) )
{
ELOG( "Error : SearchFilePath() Failed. " );
return false;
}
if ( !m_ModelData.Load( path.c_str() ) )
{
ELOG( "Error : ResPmd::Load() Failed." );
return false;
}
}
std::vector<s32> textureIdList;
std::vector<std::wstring> textureNameList;
{
std::string path;
textureIdList.resize( m_ModelData.Materials.size() );
{
std::wstring wpath;
if ( asdx::SearchFilePathW( L"res/dummy.dds", wpath ) )
{
textureNameList.push_back( wpath.c_str() );
}
}
for( size_t i=0; i<m_ModelData.Materials.size(); ++i )
{
std::string temp = m_ModelData.Materials[i].TextureName;
if ( temp.empty() )
{ continue; }
// スフィアマップ名は取り除く.
size_t idx = temp.find('*');
if ( idx != std::string::npos )
{ temp = temp.substr(0, idx); }
std::string input = "res/pronama/" + temp;
// 存在確認.
if ( !asdx::SearchFilePathA( input.c_str(), path ) )
{
textureIdList[i] = 0; // 見つからなかったらダミーテクスチャを使用.
continue;
}
// ワイド文字に変換.
auto filePath = asdx::ToStringW( path );
// リストを検索.
auto isFind = false;
for( size_t j=0; j<textureNameList.size(); ++j )
{
if ( textureNameList[j] == filePath )
{
isFind = true;
textureIdList[i] = u32(j);
break;
}
}
// 検索に引っかからなかったら追加.
if ( !isFind )
{
textureNameList.push_back( filePath );
textureIdList[i] = u32( textureNameList.size() - 1 );
}
}
}
続いて,PMDデータから頂点バッファとインデックスバッファ,シェーダに送るための定数バッファなどを構築していきます。
// CBV・SRV・UAV用ディスクリプターヒープを生成.
{
D3D12_DESCRIPTOR_HEAP_DESC desc = {};
desc.NumDescriptors = 1
+ static_cast<u32>(m_ModelData.Materials.size())
+ static_cast<u32>(textureNameList.size());
desc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE;
desc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV;
if ( !m_Heap[DESC_HEAP_BUFFER].Init( m_pDevice.GetPtr(), &desc ) )
{
ELOG( "Error : DescHeap::Init() Failed." );
return false;
}
}
// 頂点バッファの生成.
{
if ( !m_ModelVB.Init(
m_pDevice.GetPtr(),
sizeof(asdx::PMD_VERTEX) * m_ModelData.Vertices.size(),
sizeof(asdx::PMD_VERTEX),
&m_ModelData.Vertices[0] ) )
{
ELOG( "Error : VertexBuffer::Init() Failed." );
return false;
}
}
// インデックスバッファの生成.
{
if ( !m_ModelIB.Init(
m_pDevice.GetPtr(),
sizeof(u16) * m_ModelData.Indices.size(),
DXGI_FORMAT_R16_UINT,
&m_ModelData.Indices[0] ) )
{
ELOG( "Error : IndexBuffer::Init() Faild." );
return false;
}
}
// マテリアルバッファの生成.
{
if ( !m_ModelMB.Init(
m_pDevice.GetPtr(),
sizeof(ResMaterialBuffer) * m_ModelData.Materials.size() ) )
{
ELOG( "Error : ConstantBuffer::Init() Failed." );
return false;
}
u32 size = static_cast<u32>(sizeof(ResMaterialBuffer));
// 定数バッファビューの設定.
D3D12_CONSTANT_BUFFER_VIEW_DESC bufferDesc = {};
bufferDesc.BufferLocation = m_ModelMB->GetGPUVirtualAddress();
bufferDesc.SizeInBytes = size;
u32 offset = 0;
for( size_t i=0; i<m_ModelData.Materials.size(); ++i )
{
m_ModelMB.Update( &m_ModelData.Materials[i], size, offset );
m_pDevice->CreateConstantBufferView( &bufferDesc, m_Heap[DESC_HEAP_BUFFER].GetHandleCPU(1 + u32(i)) );
bufferDesc.BufferLocation += size;
offset += size;
}
}
頂点バッファ・インデックスバッファ定数バッファはプログラム的にお決まりパターンになるのでラッパークラスを書きました。
頂点バッファの初期化処理は下記のようになります。
//-------------------------------------------------------------------------------------------------
// 初期化処理を行います.
//-------------------------------------------------------------------------------------------------
bool VertexBuffer::Init( ID3D12Device* pDevice, u64 size, u32 stride, const void* pVertices )
{
if ( pDevice == nullptr || pVertices == nullptr || size == 0 || stride == 0 )
{
ELOG( "Error : Invalid Arguments." );
return false;
}
D3D12_HEAP_PROPERTIES props = {
D3D12_HEAP_TYPE_UPLOAD,
D3D12_CPU_PAGE_PROPERTY_UNKNOWN,
D3D12_MEMORY_POOL_UNKNOWN,
1,
1
};
D3D12_RESOURCE_DESC desc = {
D3D12_RESOURCE_DIMENSION_BUFFER,
0,
size,
1,
1,
1,
DXGI_FORMAT_UNKNOWN,
{ 1, 0 },
D3D12_TEXTURE_LAYOUT_ROW_MAJOR,
D3D12_RESOURCE_FLAG_NONE
};
auto hr = pDevice->CreateCommittedResource(
&props,
D3D12_HEAP_FLAG_NONE,
&desc,
D3D12_RESOURCE_STATE_GENERIC_READ,
nullptr,
IID_PPV_ARGS(m_Resource.GetAddress()));
if ( FAILED( hr ) )
{
ELOG( "Error : ID3D12Device::CreateCommittedResource() Failed." );
return false;
}
u8* pDst;
hr = m_Resource->Map( 0, nullptr, reinterpret_cast<void**>( &pDst ) );
if ( FAILED( hr ) )
{
ELOG( "Error : ID3D12Resource::Map() Failed." );
return false;
}
memcpy( pDst, pVertices, size_t(size) );
m_Resource->Unmap( 0, nullptr );
m_View.BufferLocation = m_Resource->GetGPUVirtualAddress();
m_View.SizeInBytes = static_cast<u32>(size);
m_View.StrideInBytes = stride;
return true;
}
インデックスバッファの初期化処理は下記になります。
//-------------------------------------------------------------------------------------------------
// 初期化処理を行います.
//-------------------------------------------------------------------------------------------------
bool IndexBuffer::Init( ID3D12Device* pDevice, u64 size, DXGI_FORMAT format, const void* pIndices )
{
if ( pDevice == nullptr || pIndices == nullptr || size == 0 )
{
ELOG( "Error : Invalid Argument." );
return false;
}
D3D12_HEAP_PROPERTIES props = {
D3D12_HEAP_TYPE_UPLOAD,
D3D12_CPU_PAGE_PROPERTY_UNKNOWN,
D3D12_MEMORY_POOL_UNKNOWN,
1,
1
};
D3D12_RESOURCE_DESC desc = {
D3D12_RESOURCE_DIMENSION_BUFFER,
0,
size,
1,
1,
1,
DXGI_FORMAT_UNKNOWN,
{ 1, 0 },
D3D12_TEXTURE_LAYOUT_ROW_MAJOR,
D3D12_RESOURCE_FLAG_NONE
};
auto hr = pDevice->CreateCommittedResource(
&props,
D3D12_HEAP_FLAG_NONE,
&desc,
D3D12_RESOURCE_STATE_GENERIC_READ,
nullptr,
IID_PPV_ARGS(m_Resource.GetAddress()));
if ( FAILED( hr ) )
{
ELOG( "Error : ID3D12Device::CreateCommittedResource() Failed." );
return false;
}
u8* pDst;
hr = m_Resource->Map( 0, nullptr, reinterpret_cast<void**>( &pDst ) );
if ( FAILED( hr ) )
{
ELOG( "Error : ID3D12Resource::Map() Failed." );
return false;
}
memcpy( pDst, pIndices, size_t(size) );
m_Resource->Unmap( 0, nullptr );
m_View.BufferLocation = m_Resource->GetGPUVirtualAddress();
m_View.SizeInBytes = static_cast<u32>( size );
m_View.Format = format;
return true;
}
定数バッファの初期化処理は下記のようになります。
//-------------------------------------------------------------------------------------------------
// 初期化処理を行います.
//-------------------------------------------------------------------------------------------------
bool ConstantBuffer::Init( ID3D12Device* pDevice, u64 size )
{
if ( pDevice == nullptr || size == 0 )
{
ELOG( "Error : Invalid Argument." );
return false;
}
if ( (size % 256) != 0 )
{
ELOG( "Error : ConstantBuffer must be 256 byte alignment." );
return false;
}
D3D12_HEAP_PROPERTIES props = {
D3D12_HEAP_TYPE_UPLOAD,
D3D12_CPU_PAGE_PROPERTY_UNKNOWN,
D3D12_MEMORY_POOL_UNKNOWN,
1,
1
};
D3D12_RESOURCE_DESC desc = {
D3D12_RESOURCE_DIMENSION_BUFFER,
0,
size,
1,
1,
1,
DXGI_FORMAT_UNKNOWN,
{ 1, 0 },
D3D12_TEXTURE_LAYOUT_ROW_MAJOR,
D3D12_RESOURCE_FLAG_NONE
};
auto hr = pDevice->CreateCommittedResource(
&props,
D3D12_HEAP_FLAG_NONE,
&desc,
D3D12_RESOURCE_STATE_GENERIC_READ,
nullptr,
IID_PPV_ARGS(m_Resource.GetAddress()));
if ( FAILED( hr ) )
{
ELOG( "Error : ID3D12Device::CreateCommittedResource() Failed." );
return false;
}
hr = m_Resource->Map( 0, nullptr, reinterpret_cast<void**>( &m_pDst ) );
if ( FAILED( hr ) )
{
ELOG( "Error : ID3D12Resource::Map() Failed." );
return false;
}
return true;
}
続いて,先ほど構築したテクスチャリストを使ってシェーダに送るためにシェーダリソースビューを作っていきます。
// テクスチャ・シェーダリソースビューの生成.
{
m_ModelTexture.resize( textureNameList.size() );
auto idx = 0;
auto offset = 1 + static_cast<u32>(m_ModelData.Materials.size());
for( auto textureName : textureNameList )
{
auto ext = asdx::GetExtW( textureName.c_str() );
// DDSを読み込みリソースを生成.
if ( ext == L"dds" )
{
asdx::ResDDS dds;
if ( dds.Load( textureName.c_str() ) )
{
auto desc = asdx::Texture::Desc();
desc.ResourceDesc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D;
desc.ResourceDesc.Width = dds.GetWidth();
desc.ResourceDesc.Height = dds.GetHeight();
desc.ResourceDesc.MipLevels = 1;
desc.ResourceDesc.Format = static_cast<DXGI_FORMAT>(dds.GetFormat());
desc.HandleCPU = m_Heap[DESC_HEAP_BUFFER].GetHandleCPU( offset + idx );
if ( !m_ModelTexture[idx].Init( m_pDevice.GetPtr(), desc ) )
{
continue;
}
// サブリソースを設定.
D3D12_SUBRESOURCE_DATA res = {};
res.pData = dds.GetSurfaces()->pPixels;
res.RowPitch = dds.GetSurfaces()->Pitch;
res.SlicePitch = dds.GetSurfaces()->SlicePitch;
asdx::RefPtr<ID3D12Resource> intermediate;
m_Immediate.Clear( m_pPipelineState.GetPtr() );
// サブリソースを更新.
if ( !m_ModelTexture[idx].Upload(
m_Immediate.GetGfxCmdList(),
&res,
intermediate.GetAddress() ) )
{
continue;
}
// コマンドリストを閉じておく.
m_Immediate->Close();
// GPUにテクスチャ転送.
m_Immediate.Execute( m_pCmdQueue.GetPtr() );
// 実行完了を待機.
WaitForGpu();
}
}
idx++;
}
}
テクスチャの初期化処理やサブリソースの更新もちょっと面倒なので,クラス化してあります。
テクスチャの初期化処理は下記のようになります。
//-------------------------------------------------------------------------------------------------
// 初期化処理です.
//-------------------------------------------------------------------------------------------------
bool Texture::Init( ID3D12Device* pDevice, Desc& desc )
{
if ( pDevice == nullptr )
{
ELOG( "Error : Invalid Argument." );
return false;
}
if ( desc.ResourceDesc.Dimension != D3D12_RESOURCE_DIMENSION_TEXTURE1D &&
desc.ResourceDesc.Dimension != D3D12_RESOURCE_DIMENSION_TEXTURE2D &&
desc.ResourceDesc.Dimension != D3D12_RESOURCE_DIMENSION_TEXTURE3D)
{
ELOG( "Error : Invalid Resource Dimension." );
return false;
}
HRESULT hr = S_OK;
D3D12_HEAP_PROPERTIES props = {
D3D12_HEAP_TYPE_DEFAULT,
D3D12_CPU_PAGE_PROPERTY_UNKNOWN,
D3D12_MEMORY_POOL_UNKNOWN,
1,
1
};
m_State = D3D12_RESOURCE_STATE_COMMON;
hr = pDevice->CreateCommittedResource(
&props,
D3D12_HEAP_FLAG_NONE,
&desc.ResourceDesc,
D3D12_RESOURCE_STATE_COMMON,
nullptr,
IID_PPV_ARGS( m_Resource.GetAddress() ) );
if ( FAILED( hr ) )
{
ELOG( "Error : ID3D12Device::CreateCommittedResource() Failed." );
return false;
}
auto mipLevel = ( desc.ResourceDesc.MipLevels == 0 ) ? -1 : desc.ResourceDesc.MipLevels;
D3D12_SHADER_RESOURCE_VIEW_DESC viewDesc = {};
viewDesc.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING;
viewDesc.Format = desc.ResourceDesc.Format;
switch( desc.ResourceDesc.Dimension )
{
case D3D12_RESOURCE_DIMENSION_TEXTURE1D:
{
if ( desc.ResourceDesc.DepthOrArraySize > 1 )
{
viewDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE1DARRAY;
viewDesc.Texture1DArray.ArraySize = desc.ResourceDesc.DepthOrArraySize;
viewDesc.Texture1DArray.FirstArraySlice = 0;
viewDesc.Texture1DArray.MipLevels = mipLevel;
}
else
{
viewDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE1D;
viewDesc.Texture1D.MipLevels = mipLevel;
}
}
break;
case D3D12_RESOURCE_DIMENSION_TEXTURE2D:
{
if ( desc.IsCubeMap )
{
if ( desc.ResourceDesc.DepthOrArraySize > 6 )
{
viewDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURECUBEARRAY;
viewDesc.TextureCubeArray.MipLevels = mipLevel;
viewDesc.TextureCubeArray.NumCubes = ( desc.ResourceDesc.DepthOrArraySize / 6 );
}
else
{
viewDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURECUBE;
viewDesc.TextureCube.MipLevels = mipLevel;
}
}
else if ( desc.ResourceDesc.SampleDesc.Count > 1 )
{
if ( desc.ResourceDesc.DepthOrArraySize > 1 )
{
viewDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2DMSARRAY;
viewDesc.Texture2DMSArray.ArraySize = desc.ResourceDesc.DepthOrArraySize;
viewDesc.Texture2DMSArray.FirstArraySlice = 0;
}
else
{
viewDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2DMS;
}
}
else
{
if ( desc.ResourceDesc.DepthOrArraySize > 1 )
{
viewDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2DARRAY;
viewDesc.Texture2DArray.ArraySize = desc.ResourceDesc.DepthOrArraySize;
viewDesc.Texture2DArray.MipLevels = mipLevel;
}
else
{
viewDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D;
viewDesc.Texture2D.MipLevels = mipLevel;
viewDesc.Texture2D.MostDetailedMip = 0;
}
}
}
break;
case D3D12_RESOURCE_DIMENSION_TEXTURE3D:
{
viewDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE3D;
viewDesc.Texture3D.MipLevels = mipLevel;
viewDesc.Texture3D.MostDetailedMip = 0;
}
break;
}
pDevice->CreateShaderResourceView( m_Resource.GetPtr(), &viewDesc, desc.HandleCPU );
return true;
}
サブリソースの更新処理は下記のような感じです。
//-------------------------------------------------------------------------------------------------
// サブリソースを更新します.
//-------------------------------------------------------------------------------------------------
bool Texture::Upload
(
ID3D12GraphicsCommandList* pCmdList,
D3D12_SUBRESOURCE_DATA* pData,
ID3D12Resource** ppIntermediate
)
{
if ( pCmdList == nullptr || pData == nullptr )
{
ELOG( "Error : Invalid Argument." );
return false;
}
RefPtr<ID3D12Device> device;
m_Resource->GetDevice( IID_PPV_ARGS(device.GetAddress()));
{
// アップロード用リソースを生成.
D3D12_RESOURCE_DESC uploadDesc = {
D3D12_RESOURCE_DIMENSION_BUFFER,
0,
GetRequiredIntermediateSize( m_Resource.GetPtr(), 0, 1 ),
1,
1,
1,
DXGI_FORMAT_UNKNOWN,
{ 1, 0 },
D3D12_TEXTURE_LAYOUT_ROW_MAJOR,
D3D12_RESOURCE_FLAG_NONE
};
D3D12_HEAP_PROPERTIES props = {
D3D12_HEAP_TYPE_UPLOAD,
D3D12_CPU_PAGE_PROPERTY_UNKNOWN,
D3D12_MEMORY_POOL_UNKNOWN,
1,
1
};
auto hr = device->CreateCommittedResource(
&props,
D3D12_HEAP_FLAG_NONE,
&uploadDesc,
D3D12_RESOURCE_STATE_GENERIC_READ,
nullptr,
IID_PPV_ARGS(ppIntermediate));
if ( FAILED( hr ) )
{
ELOG( "Error : ID3D12Device::CreateCommittedResource() Failed." );
return false;
}
}
D3D12_RESOURCE_STATES oldState = m_State;
m_State = D3D12_RESOURCE_STATE_COPY_DEST;
D3D12_RESOURCE_BARRIER barrier = {};
barrier.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION;
barrier.Transition.pResource = m_Resource.GetPtr();
barrier.Transition.StateBefore = oldState;
barrier.Transition.StateAfter = m_State;
barrier.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES;
barrier.Flags = D3D12_RESOURCE_BARRIER_FLAG_NONE;
pCmdList->ResourceBarrier( 1, &barrier );
UpdateSubresources(
pCmdList,
m_Resource.GetPtr(),
(*ppIntermediate),
0,
0,
1,
pData );
oldState = m_State;
m_State = D3D12_RESOURCE_STATE_GENERIC_READ;
barrier.Transition.StateBefore = oldState;
barrier.Transition.StateAfter = m_State;
pCmdList->ResourceBarrier( 1, &barrier );
return true;
}
ちょっとUpload()メソッドの仕様がやっつけ感がありますが,とりあえず今回のサンプルは1回こっきりで使いまわさないことを前提にして作っているので,このままにしておきます。
これでようやくコマンドを作成するための下準備が整いました。モデルの描画は結構負荷が高いのでコマンドを全てBundleに積んでしまいます。前回やったBundleがこういうところで威力を発揮します。
// バンドルの初期化.
if ( !m_Bundle.Init( m_pDevice.GetPtr(), D3D12_COMMAND_LIST_TYPE_BUNDLE, m_pPipelineState.GetPtr() ) )
{
ELOG( "Error : GraphicsCmdList()::Init() Failed." );
return false;
}
// バンドルにコマンドを積む.
{
// ディスクリプタヒープを設定.
ID3D12DescriptorHeap* pHeap = m_Heap[DESC_HEAP_BUFFER].GetPtr();
m_Bundle->SetDescriptorHeaps( 1, &pHeap );
// ルートシグニチャを設定.
m_Bundle->SetGraphicsRootSignature( m_pRootSignature.GetPtr() );
// プリミティブトポロジーの設定.
m_Bundle->IASetPrimitiveTopology( D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST );
auto vbv = m_ModelVB.GetView();
auto ibv = m_ModelIB.GetView();
// 頂点バッファビューを設定.
m_Bundle->IASetVertexBuffers( 0, 1, &vbv );
m_Bundle->IASetIndexBuffer( &ibv );
// インデックスオフセット.
u32 offset = 0;
u32 materialCount = u32( m_ModelData.Materials.size() );
for( size_t i=0; i<materialCount; ++i )
{
// テクスチャとマテリアルを設定.
auto handleSRV = m_Heap[DESC_HEAP_BUFFER].GetHandleGPU( 1 + materialCount + textureIdList[i] );
auto handleMat = m_Heap[DESC_HEAP_BUFFER].GetHandleGPU( 1 + u32(i) );
m_Bundle->SetGraphicsRootDescriptorTable( 1, handleSRV );
m_Bundle->SetGraphicsRootDescriptorTable( 2, handleMat );
u32 count = m_ModelData.Materials[i].VertexCount;
// 描画コマンドを生成.
m_Bundle->DrawIndexedInstanced( count, 1, offset, 0, 0 );
offset += count;
}
// バンドルへの記録を終了.
m_Bundle->Close();
}
ちなみ,ここまでは全て初期化処理です。
あとは,フレームを描画する際にBundleを実行してやればよいので,フレーム描画の処理は前回とほとんど変わりません。
//-------------------------------------------------------------------------------------------------
// 描画時の処理です.
//-------------------------------------------------------------------------------------------------
void App::OnRender(FLOAT elapsedSec)
{
// 回転角を増やす.
m_RotateAngle += ( elapsedSec / 1.5f );
// ワールド行列を更新.
m_ModelParam.World = asdx::Matrix::CreateRotationY( m_RotateAngle );
// 定数バッファを更新.
m_ModelTB.Update( &m_ModelParam, sizeof(m_ModelParam), 0 );
// コマンドアロケータとコマンドリストをリセット.
m_Immediate.Clear( m_pPipelineState.GetPtr() );
ID3D12DescriptorHeap* pHeap = m_Heap[DESC_HEAP_BUFFER].GetPtr();
// ディスクリプタヒープを設定.
m_Immediate->SetDescriptorHeaps( 1, &pHeap );
// ルートシグニチャを設定.
m_Immediate->SetGraphicsRootSignature( m_pRootSignature.GetPtr() );
// ディスクリプタヒープテーブルを設定.
auto handleCBV = m_Heap[DESC_HEAP_BUFFER].GetHandleGPU(0);
m_Immediate->SetGraphicsRootDescriptorTable( 0, handleCBV );
// リソースバリアの設定.
m_Immediate.Transition(
m_pRenderTarget[m_FrameIndex].GetPtr(),
D3D12_RESOURCE_STATE_PRESENT,
D3D12_RESOURCE_STATE_RENDER_TARGET );
// ビューポートの設定.
m_Immediate->RSSetViewports( 1, &m_Viewport );
// シザー矩形の設定.
m_Immediate->RSSetScissorRects( 1, &m_ScissorRect );
// レンダーターゲットのハンドルを取得.
auto handleRTV = m_Heap[DESC_HEAP_RTV].GetHandleCPU(m_FrameIndex);
auto handleDSV = m_Heap[DESC_HEAP_DSV].GetHandleCPU(0);
// レンダーターゲットの設定.
m_Immediate->OMSetRenderTargets( 1, &handleRTV, FALSE, &handleDSV );
// レンダーターゲットビューをクリア.
const FLOAT clearColor[] = { 0.39f, 0.58f, 0.92f, 0.0f };
m_Immediate->ClearRenderTargetView( handleRTV, clearColor, 0, nullptr );
// 深度ステンシルビューをクリア.
m_Immediate->ClearDepthStencilView( handleDSV, D3D12_CLEAR_FLAG_DEPTH, 1.0f, 0, 0, nullptr );
// バンドルを実行.
m_Immediate->ExecuteBundle( m_Bundle.GetGfxCmdList() );
// リソースバリアの設定.
m_Immediate.Transition(
m_pRenderTarget[m_FrameIndex].GetPtr(),
D3D12_RESOURCE_STATE_RENDER_TARGET,
D3D12_RESOURCE_STATE_PRESENT );
// コマンドの記録を終了.
m_Immediate->Close();
// コマンド実行.
m_Immediate.Execute( m_pCmdQueue.GetPtr() );
// 表示する.
m_pSwapChain->Present( 1, 0 );
// コマンドの完了を待機.
WaitForGpu();
}
初期化処理はかなりごちゃごちゃしますが,フレーム描画処理はスッキリしてよいですね。
かなり長くなってしまいましたが,一応モデル表示について説明してみました。
頂点データ等を作るのはさほど手間ではないのですが,やっぱりテクスチャデータの取り扱いがDirect3D12は面倒くさすぎて,全くやる気が出ないですね。
次回はモーションデータを取り扱おうかなって思っています。
本ソースコードおよびプログラムはMIT Licenseに準じます。
プログラムの作成にはMicrosoft Visual Studio Community 2015, 及び Windows SDK 10.0.10240.0を用いています。
D3D12_PMD