前回
PMX ファイル読み込み
PMXモデルファイルをロードして、ミクさんを描画できるようにしましょう。
PMXフォーマットは元々MMD(MikuMikuDance)で使用されるフォーマットです。
以前はPMDを使用していましたが、PMDファイルフォーマットから改良されたファイル形式がPMXです。
使用するPMXファイルは自分で探して使用してください。
PMXファイルの構造
PMXファイルは以下のようなデータ構造を持っています。
- ヘッダー情報
- モデル情報(名前、コメント)
- 頂点情報
- フェイス情報(インデックスと考えてください)
- テクスチャ情報
- マテリアル情報
- ボーン情報
- モーフ情報
- ディスプレイフレーム情報(使用しません)
- リジッドボディ情報
- ジョイント情報
- 剛体情報
上記に列挙した順序で情報が並んでいます。
PMXファイルの読み込み
PMXフォーマットはバイナリデータです。
C++のifstreamを使用してPMXファイルを開き、readを通じて必要な情報をバイト単位で読み込むようにします。
std::ifstream pmxFile{ filePath, (std::ios::binary | std::ios::in) };
if (pmxFile.fail())
{
pmxFile.close();
return false;
}
filePathはpmxファイルのパスとファイル名の文字列です。ifstreamを使用してバイナリモード、読み取り専用モードでファイルを開きます。
PMXFileData 構造体を次のように定義しました。
struct PMXFileData
{
PMXHeader header;
PMXModelInfo modelInfo;
std::vector<PMXVertex> vertices;
std::vector<PMXFace> faces;
std::vector<PMXTexture> textures;
std::vector<PMXMaterial> materials;
std::vector<PMXBone> bones;
std::vector<PMXMorph> morphs;
std::vector<PMXDisplayFrame> displayFrames;
std::vector<PMXRigidBody> rigidBodies;
std::vector<PMXJoint> joints;
std::vector<PMXSoftBody> softBodies;
};
ヘッダーの読み込み
PMXファイルのヘッダー情報に関する構造体を次のように定義しました。
struct PMXHeader
{
std::array<unsigned char, 4> magic;
float version;
unsigned char dataLength;
unsigned char textEncoding; // 0 = utf-16, 1= utf - 8
unsigned char addUVNum; // 0 - 4
unsigned char vertexIndexSize; // 1 = byte, 2 = short, 4 = int
unsigned char textureIndexSize; // 1 = byte, 2 = short, 4 = int
unsigned char materialIndexSize;
unsigned char boneIndexSize;
unsigned char morphIndexSize;
unsigned char rigidBodyIndexSize;
};
ヘッダー情報は各パラメーターの型のサイズだけ読み込めば良いです。
ヘッダー情報を読み込むメソッドを私はこのように作成しました。
bool ReadHeader(PMXFileData& data, std::ifstream& file)
最初の4バイトはPMXファイルのマジックナンバーです。PMXファイルであれば、必ず決まった値が最初の4バイトに含まれています。
constexpr std::array<unsigned char, 4> PMX_MAGIC_NUMBER{ 0x50, 0x4d, 0x58, 0x20 };
この値と最初の4バイトの値を比較したとき、一致しない場合はPMXファイル形式ではないため、ファイルの読み込みを中止します。
file.read(reinterpret_cast<char*>(data.header.magic.data()), data.header.magic.size());
if (data.header.magic != PMX_MAGIC_NUMBER)
{
return false;
}
file.read(reinterpret_cast<char*>(&data.header.version), sizeof(data.header.version));
file.read(reinterpret_cast<char*>(&data.header.dataLength), sizeof(data.header.dataLength));
file.read(reinterpret_cast<char*>(&data.header.textEncoding), sizeof(data.header.textEncoding));
次の4バイトはファイルのバージョン
次の1バイトはデータのサイズ
次の1バイトはテキストエンコーディング
これらのデータは私たちにとって重要な情報ではありません。
file.read(reinterpret_cast<char*>(&data.header.addUVNum), sizeof(data.header.addUVNum));
次の1バイトは追加UVの数です。基本のUV値以外に追加で使用するUVの数です。
後で頂点情報を読み込む際に使用されます。
file.read(reinterpret_cast<char*>(&data.header.vertexIndexSize), sizeof(data.header.vertexIndexSize));
file.read(reinterpret_cast<char*>(&data.header.textureIndexSize), sizeof(data.header.textureIndexSize));
file.read(reinterpret_cast<char*>(&data.header.materialIndexSize), sizeof(data.header.materialIndexSize));
file.read(reinterpret_cast<char*>(&data.header.boneIndexSize), sizeof(data.header.boneIndexSize));
file.read(reinterpret_cast<char*>(&data.header.morphIndexSize), sizeof(data.header.morphIndexSize));
file.read(reinterpret_cast<char*>(&data.header.rigidBodyIndexSize), sizeof(data.header.rigidBodyIndexSize));
その後も順番に1バイトずつ読み込んでいきますが、これらはすべて後で読み込む情報のバイト数です。
モデル情報の読み込み
モデル情報を読み込みましょう。
モデル情報はモデルの名前とコメントの文字列です。
bool ReadModelInfo(PMXFileData& data, std::ifstream& file)
{
UnicodeUtil::GetPMXStringUTF16(file, data.modelInfo.modelName);
UnicodeUtil::GetPMXStringUTF8(file, data.modelInfo.englishModelName);
UnicodeUtil::GetPMXStringUTF16(file, data.modelInfo.comment);
UnicodeUtil::GetPMXStringUTF8(file, data.modelInfo.englishComment);
return true;
}
PMXファイルは名前とコメントのUTF16文字列とUTF8文字列を持っているため、それぞれ別々に読み込む必要があります。
UTF16の文字列をstd::wstringに変換するメソッドを作成する必要があります。
bool GetPMXStringUTF16(std::ifstream& _file, std::wstring& output)
{
std::array<wchar_t, 512> wBuffer{};
int textSize;
_file.read(reinterpret_cast<char*>(&textSize), 4);
_file.read(reinterpret_cast<char*>(&wBuffer), textSize);
output = std::wstring(&wBuffer[0], &wBuffer[0] + textSize / 2);
return true;
}
最初の4バイトは、その文字列のバイト数です。
文字列のバイト数を読み込んだ後、その分だけ読み込んでwbufferに保存しましょう。
wstring コンストラクタに開始と終了のポインタを渡して、読み込んだバイトをwstringに変換します。
texSizeを2で割る理由は、UTF16の文字が2バイトだからです。
例えば、texSizeが16の場合、文字数は8個ということになります。
UTF8の文字列はstd::stringに保存します。
bool GetPMXStringUTF8(std::ifstream& _file, std::string& output)
{
std::array<wchar_t, 512> wBuffer{};
int textSize;
_file.read(reinterpret_cast<char*>(&textSize), 4);
_file.read(reinterpret_cast<char*>(&wBuffer), textSize);
output = std::string(&wBuffer[0], &wBuffer[0] + textSize);
return true;
}
頂点情報の読み込み
今回は頂点情報を読み込みましょう。
PMXの頂点構造体は次のように構成されています。
struct PMXVertex
{
DirectX::XMFLOAT3 position;
DirectX::XMFLOAT3 normal;
DirectX::XMFLOAT2 uv;
DirectX::XMFLOAT4 additionalUV[4];
PMXVertexWeight weightType;
int boneIndices[4];
float boneWeights[4];
DirectX::XMFLOAT3 sdefC;
DirectX::XMFLOAT3 sdefR0;
DirectX::XMFLOAT3 sdefR1;
float edgeMag;
};
頂点情報を読み込むメソッドは次のとおりです。
bool ReadVertex(PMXFileData& data, std::ifstream& file)
最初の4バイトはこのモデルの頂点数です。
unsigned int vertexCount;
file.read(reinterpret_cast<char*>(&vertexCount), 4);
data.vertices.resize(vertexCount);
この頂点数で頂点Vectorをリサイズし、forループで頂点を読み込んでいきます。
for (auto& vertex : data.vertices)
{
}
最初の32バイトは位置、法線、UV値です。
file.read(reinterpret_cast<char*>(&vertex.position), 12);
file.read(reinterpret_cast<char*>(&vertex.normal), 12);
file.read(reinterpret_cast<char*>(&vertex.uv), 8);
次は追加UVですが、先ほどヘッダーを読み込む際にこの追加UVの数を読み込みました。
この数だけ16バイトずつ読み込みます。
for (int i = 0; i < data.header.addUVNum; i++)
{
file.read(reinterpret_cast<char*>(&vertex.additionalUV[i]), 16);
}
次にボーンのウェイトタイプと現在の頂点にリギングされたボーンのインデックスを読み込みます。
file.read(reinterpret_cast<char*>(&vertex.weightType), 1);
const unsigned char boneIndexSize = data.header.boneIndexSize;
switch (vertex.weightType)
{
case PMXVertexWeight::BDEF1:
file.read(reinterpret_cast<char*>(&vertex.boneIndices[0]), boneIndexSize);
break;
case PMXVertexWeight::BDEF2:
file.read(reinterpret_cast<char*>(&vertex.boneIndices[0]), boneIndexSize);
file.read(reinterpret_cast<char*>(&vertex.boneIndices[1]), boneIndexSize);
file.read(reinterpret_cast<char*>(&vertex.boneWeights[0]), 4);
break;
case PMXVertexWeight::BDEF4:
case PMXVertexWeight::QDEF:
file.read(reinterpret_cast<char*>(&vertex.boneIndices[0]), boneIndexSize);
file.read(reinterpret_cast<char*>(&vertex.boneIndices[1]), boneIndexSize);
file.read(reinterpret_cast<char*>(&vertex.boneIndices[2]), boneIndexSize);
file.read(reinterpret_cast<char*>(&vertex.boneIndices[3]), boneIndexSize);
file.read(reinterpret_cast<char*>(&vertex.boneWeights[0]), 4);
file.read(reinterpret_cast<char*>(&vertex.boneWeights[1]), 4);
file.read(reinterpret_cast<char*>(&vertex.boneWeights[2]), 4);
file.read(reinterpret_cast<char*>(&vertex.boneWeights[3]), 4);
break;
case PMXVertexWeight::SDEF:
file.read(reinterpret_cast<char*>(&vertex.boneIndices[0]), boneIndexSize);
file.read(reinterpret_cast<char*>(&vertex.boneIndices[1]), boneIndexSize);
file.read(reinterpret_cast<char*>(&vertex.boneWeights[0]), 4);
file.read(reinterpret_cast<char*>(&vertex.sdefC), 12);
file.read(reinterpret_cast<char*>(&vertex.sdefR0), 12);
file.read(reinterpret_cast<char*>(&vertex.sdefR1), 12);
break;
default:
return false;
}
ここでもヘッダーにあったboneIndexSizeの分だけリギングされたボーンのインデックス値を読み込みます。
スキニングアニメーションについて知らない方であれば、リギング、ウェイト、ボーンが何なのか分からないでしょうが、モデルのアニメーションのためのデータだと考えてください。
スキニングアニメーションについては後ほど扱うことにします。
最後にedgeMagを読み込むと、1つの頂点情報を全て読み込んだことになります。
forループを閉じてください。
file.read(reinterpret_cast<char*>(&vertex.edgeMag), 4);
}
フェイス情報の読み込み
フェイスは三角形1つのインデックスリストだと考えてください。
struct PMXFace
{
int vertices[3];
};
読み込むメソッドは次のとおりです。
bool ReadFace(PMXFileData& data, std::ifstream& file)
int faceCount = 0;
file.read(reinterpret_cast<char*>(&faceCount), 4);
faceCount /= 3;
data.faces.resize(faceCount);
最初の4バイトはインデックスの数です。
インデックス3つで1つのフェイスを構成するので、3で割るとフェイスの数になります。
switch (data.header.vertexIndexSize)
{
case 1:
{
std::vector<uint8_t> vertices(faceCount * 3);
file.read(reinterpret_cast<char*>(vertices.data()), sizeof(uint8_t) * vertices.size());
for (int32_t faceIdx = 0; faceIdx < faceCount; faceIdx++)
{
data.faces[faceIdx].vertices[0] = vertices[faceIdx * 3 + 0];
data.faces[faceIdx].vertices[1] = vertices[faceIdx * 3 + 1];
data.faces[faceIdx].vertices[2] = vertices[faceIdx * 3 + 2];
}
}
break;
case 2:
{
std::vector<uint16_t> vertices(faceCount * 3);
file.read(reinterpret_cast<char*>(vertices.data()), sizeof(uint16_t) * vertices.size());
for (int32_t faceIdx = 0; faceIdx < faceCount; faceIdx++)
{
data.faces[faceIdx].vertices[0] = vertices[faceIdx * 3 + 0];
data.faces[faceIdx].vertices[1] = vertices[faceIdx * 3 + 1];
data.faces[faceIdx].vertices[2] = vertices[faceIdx * 3 + 2];
}
}
break;
case 4:
{
std::vector<uint32_t> vertices(faceCount * 3);
file.read(reinterpret_cast<char*>(vertices.data()), sizeof(uint32_t) * vertices.size());
for (int32_t faceIdx = 0; faceIdx < faceCount; faceIdx++)
{
data.faces[faceIdx].vertices[0] = vertices[faceIdx * 3 + 0];
data.faces[faceIdx].vertices[1] = vertices[faceIdx * 3 + 1];
data.faces[faceIdx].vertices[2] = vertices[faceIdx * 3 + 2];
}
}
break;
default:
return false;
}
return true;
インデックス値のバイト数もヘッダーに含まれていました。そのサイズだけ読み込めるようにします。
PMXのインデックス値のバイト数は1、2、4バイトのいずれかです。
テクスチャ情報の読み込み
メソッドは次のとおりです。
bool ReadTextures(PMXFileData& data, std::ifstream& file)
{
unsigned int numOfTexture = 0;
file.read(reinterpret_cast<char*>(&numOfTexture), 4);
data.textures.resize(numOfTexture);
for (auto& texture : data.textures)
{
UnicodeUtil::GetPMXStringUTF16(file, texture.textureName);
}
return true;
}
最初の4バイトはテクスチャの数です。
テクスチャの数だけテクスチャの名前をstd::wstringとして読み込めるようにします。
マテリアル情報の読み込み
UnityやUnreal Engineを扱ったことがある方なら、マテリアルという言葉は馴染みがあるでしょう。
マテリアル構造体は次のとおりです。
struct PMXMaterial
{
std::wstring name;
std::string englishName;
DirectX::XMFLOAT4 diffuse;
DirectX::XMFLOAT3 specular;
float specularPower;
DirectX::XMFLOAT3 ambient;
PMXDrawModeFlags drawMode;
DirectX::XMFLOAT4 edgeColor;
float edgeSize;
unsigned int textureIndex;
unsigned int sphereTextureIndex;
PMXSphereMode sphereMode;
PMXToonMode toonMode;
unsigned int toonTextureIndex;
std::wstring memo;
unsigned int numFaceVertices;
};
メソッドは次のとおりです。
bool ReadMaterial(PMXFileData& data, std::ifstream& file)
{
int numOfMaterial = 0;
file.read(reinterpret_cast<char*>(&numOfMaterial), 4);
data.materials.resize(numOfMaterial);
for (auto& mat : data.materials)
{
UnicodeUtil::GetPMXStringUTF16(file, mat.name);
UnicodeUtil::GetPMXStringUTF8(file, mat.englishName);
file.read(reinterpret_cast<char*>(&mat.diffuse), 16);
file.read(reinterpret_cast<char*>(&mat.specular), 12);
file.read(reinterpret_cast<char*>(&mat.specularPower), 4);
file.read(reinterpret_cast<char*>(&mat.ambient), 12);
file.read(reinterpret_cast<char*>(&mat.drawMode), 1);
file.read(reinterpret_cast<char*>(&mat.edgeColor), 16);
file.read(reinterpret_cast<char*>(&mat.edgeSize), 4);
file.read(reinterpret_cast<char*>(&mat.textureIndex), data.header.textureIndexSize);
file.read(reinterpret_cast<char*>(&mat.sphereTextureIndex), data.header.textureIndexSize);
file.read(reinterpret_cast<char*>(&mat.sphereMode), 1);
file.read(reinterpret_cast<char*>(&mat.toonMode), 1);
if (mat.toonMode == PMXToonMode::Separate)
{
file.read(reinterpret_cast<char*>(&mat.toonTextureIndex), data.header.textureIndexSize);
}
else if (mat.toonMode == PMXToonMode::Common)
{
file.read(reinterpret_cast<char*>(&mat.toonTextureIndex), 1);
}
else
{
return false;
}
UnicodeUtil::GetPMXStringUTF16(file, mat.memo);
file.read(reinterpret_cast<char*>(&mat.numFaceVertices), 4);
}
return true;
}
この部分も実際に使用する際に各パラメータについて話します。
ボーン情報の読み込み
ボーンもかなり複雑です。
ボーンはさまざまなオプションを持つことができます。
PMXモデルのボーンのオプションは以下の通りです。
enum class PMXBoneFlags : uint16_t
{
TargetShowMode = 0x0001,
AllowRotate = 0x0002,
AllowTranslate = 0x0004,
Visible = 0x0008,
AllowControl = 0x0010,
IK = 0x0020,
AppendLocal = 0x0080,
AppendRotate = 0x0100,
AppendTranslate = 0x0200,
FixedAxis = 0x0400,
LocalAxis = 0x800,
DeformAfterPhysics = 0x1000,
DeformOuterParent = 0x2000,
};
そして、IKで使用するPMXIKLinkも定義します。
struct PMXIKLink
{
unsigned int ikBoneIndex;
unsigned char enableLimit;
DirectX::XMFLOAT3 limitMin;
DirectX::XMFLOAT3 limitMax;
};
ボーンの構造体は次のとおりです。
struct PMXBone
{
std::wstring name;
std::string englishName;
DirectX::XMFLOAT3 position;
unsigned int parentBoneIndex;
unsigned int deformDepth;
PMXBoneFlags boneFlag;
DirectX::XMFLOAT3 positionOffset;
unsigned int linkBoneIndex;
unsigned int appendBoneIndex;
float appendWeight;
DirectX::XMFLOAT3 fixedAxis;
DirectX::XMFLOAT3 localXAxis;
DirectX::XMFLOAT3 localZAxis;
unsigned int keyValue;
unsigned int ikTargetBoneIndex;
unsigned int ikIterationCount;
float ikLimit;
std::vector<PMXIKLink> ikLinks;
};
メソッド全体は次のとおりです。
bool ReadBone(PMXFileData& data, std::ifstream& file)
{
unsigned int numOfBone;
file.read(reinterpret_cast<char*>(&numOfBone), 4);
data.bones.resize(numOfBone);
for (auto& bone : data.bones)
{
UnicodeUtil::GetPMXStringUTF16(file, bone.name);
UnicodeUtil::GetPMXStringUTF8(file, bone.englishName);
file.read(reinterpret_cast<char*>(&bone.position), 12);
file.read(reinterpret_cast<char*>(&bone.parentBoneIndex), data.header.boneIndexSize);
file.read(reinterpret_cast<char*>(&bone.deformDepth), 4);
file.read(reinterpret_cast<char*>(&bone.boneFlag), 2);
if (((uint16_t)bone.boneFlag & (uint16_t)PMXBoneFlags::TargetShowMode) == 0)
{
file.read(reinterpret_cast<char*>(&bone.positionOffset), 12);
}
else
{
file.read(reinterpret_cast<char*>(&bone.linkBoneIndex), data.header.boneIndexSize);
}
if (((uint16_t)bone.boneFlag & (uint16_t)PMXBoneFlags::AppendRotate) ||
((uint16_t)bone.boneFlag & (uint16_t)PMXBoneFlags::AppendTranslate))
{
file.read(reinterpret_cast<char*>(&bone.appendBoneIndex), data.header.boneIndexSize);
file.read(reinterpret_cast<char*>(&bone.appendWeight), 4);
}
if ((uint16_t)bone.boneFlag & (uint16_t)PMXBoneFlags::FixedAxis)
{
file.read(reinterpret_cast<char*>(&bone.fixedAxis), 12);
}
if ((uint16_t)bone.boneFlag & (uint16_t)PMXBoneFlags::LocalAxis)
{
file.read(reinterpret_cast<char*>(&bone.localXAxis), 12);
file.read(reinterpret_cast<char*>(&bone.localZAxis), 12);
}
if ((uint16_t)bone.boneFlag & (uint16_t)PMXBoneFlags::DeformOuterParent)
{
file.read(reinterpret_cast<char*>(&bone.keyValue), 4);
}
if ((uint16_t)bone.boneFlag & (uint16_t)PMXBoneFlags::IK)
{
file.read(reinterpret_cast<char*>(&bone.ikTargetBoneIndex), data.header.boneIndexSize);
file.read(reinterpret_cast<char*>(&bone.ikIterationCount), 4);
file.read(reinterpret_cast<char*>(&bone.ikLimit), 4);
unsigned int linkCount = 0;
file.read(reinterpret_cast<char*>(&linkCount), 4);
bone.ikLinks.resize(linkCount);
for (auto& ikLink : bone.ikLinks)
{
file.read(reinterpret_cast<char*>(&ikLink.ikBoneIndex), data.header.boneIndexSize);
file.read(reinterpret_cast<char*>(&ikLink.enableLimit), 1);
if (ikLink.enableLimit != 0)
{
file.read(reinterpret_cast<char*>(&ikLink.limitMin), 12);
file.read(reinterpret_cast<char*>(&ikLink.limitMax), 12);
}
}
}
}
return true;
}
ボーンは設定されているオプションに応じて、読み込むか読み込まない情報が異なります。
オプション値を読み込み、ビット演算でオプションに該当するかどうかを検査します。
モーフ情報の読み込み
モーフは顔の表情などをコントロールするための情報です。アニメーションに関連していますね。
モーフには複数のタイプが存在します。
enum class PMXMorphType : uint8_t
{
Group,
Position,
Bone,
UV,
AddUV1,
AddUV2,
AddUV3,
AddUV4,
Material,
Flip,
Impluse,
};
そして、モーフはタイプによって使用される情報の構造が異なります。
struct PMXMorph
{
std::wstring name;
std::string englishName;
unsigned char controlPanel;
PMXMorphType morphType;
struct PositionMorph
{
unsigned int vertexIndex;
DirectX::XMFLOAT3 position;
};
struct UVMorph
{
unsigned int vertexIndex;
DirectX::XMFLOAT4 uv;
};
struct BoneMorph
{
unsigned int boneIndex;
DirectX::XMFLOAT3 position;
DirectX::XMFLOAT4 quaternion;
};
struct MaterialMorph
{
enum class OpType : uint8_t
{
Mul,
Add,
};
unsigned int materialIndex;
OpType opType;
DirectX::XMFLOAT4 diffuse;
DirectX::XMFLOAT3 specular;
float specularPower;
DirectX::XMFLOAT3 ambient;
DirectX::XMFLOAT4 edgeColor;
float edgeSize;
DirectX::XMFLOAT4 textureFactor;
DirectX::XMFLOAT4 sphereTextureFactor;
DirectX::XMFLOAT4 toonTextureFactor;
};
struct GroupMorph
{
unsigned int morphIndex;
float weight;
};
struct FlipMorph
{
unsigned int morphIndex;
float weight;
};
struct ImpulseMorph
{
unsigned int rigidBodyIndex;
unsigned char localFlag; //0:OFF 1:ON
DirectX::XMFLOAT3 translateVelocity;
DirectX::XMFLOAT3 rotateTorque;
};
std::vector<PositionMorph> positionMorph;
std::vector<UVMorph> uvMorph;
std::vector<BoneMorph> boneMorph;
std::vector<MaterialMorph> materialMorph;
std::vector<GroupMorph> groupMorph;
std::vector<FlipMorph> flipMorph;
std::vector<ImpulseMorph> impulseMorph;
};
読み込むメソッドは次のとおりです。
bool ReadMorph(PMXFileData& data, std::ifstream& file)
{
unsigned int numOfMorph = 0;
file.read(reinterpret_cast<char*>(&numOfMorph), 4);
data.morphs.resize(numOfMorph);
for (auto& morph : data.morphs)
{
UnicodeUtil::GetPMXStringUTF16(file, morph.name);
UnicodeUtil::GetPMXStringUTF8(file, morph.englishName);
file.read(reinterpret_cast<char*>(&morph.controlPanel), 1);
file.read(reinterpret_cast<char*>(&morph.morphType), 1);
unsigned int dataCount;
file.read(reinterpret_cast<char*>(&dataCount), 4);
if (morph.morphType == PMXMorphType::Position)
{
morph.positionMorph.resize(dataCount);
for (auto& morphData : morph.positionMorph)
{
file.read(reinterpret_cast<char*>(&morphData.vertexIndex), data.header.vertexIndexSize);
file.read(reinterpret_cast<char*>(&morphData.position), 12);
}
}
else if (morph.morphType == PMXMorphType::UV ||
morph.morphType == PMXMorphType::AddUV1 ||
morph.morphType == PMXMorphType::AddUV2 ||
morph.morphType == PMXMorphType::AddUV3 ||
morph.morphType == PMXMorphType::AddUV4)
{
morph.uvMorph.resize(dataCount);
for (auto& morphData : morph.uvMorph)
{
file.read(reinterpret_cast<char*>(&morphData.vertexIndex), data.header.vertexIndexSize);
file.read(reinterpret_cast<char*>(&morphData.uv), 16);
}
}
else if (morph.morphType == PMXMorphType::Bone)
{
morph.boneMorph.resize(dataCount);
for (auto& morphData : morph.boneMorph)
{
file.read(reinterpret_cast<char*>(&morphData.boneIndex), data.header.boneIndexSize);
file.read(reinterpret_cast<char*>(&morphData.position), 12);
file.read(reinterpret_cast<char*>(&morphData.quaternion), 16);
}
}
else if (morph.morphType == PMXMorphType::Material)
{
morph.materialMorph.resize(dataCount);
for (auto& morphData : morph.materialMorph)
{
file.read(reinterpret_cast<char*>(&morphData.materialIndex), data.header.materialIndexSize);
file.read(reinterpret_cast<char*>(&morphData.opType), 1);
file.read(reinterpret_cast<char*>(&morphData.diffuse), 16);
file.read(reinterpret_cast<char*>(&morphData.specular), 12);
file.read(reinterpret_cast<char*>(&morphData.specularPower), 4);
file.read(reinterpret_cast<char*>(&morphData.ambient), 12);
file.read(reinterpret_cast<char*>(&morphData.edgeColor), 16);
file.read(reinterpret_cast<char*>(&morphData.edgeSize), 4);
file.read(reinterpret_cast<char*>(&morphData.textureFactor), 16);
file.read(reinterpret_cast<char*>(&morphData.sphereTextureFactor), 16);
file.read(reinterpret_cast<char*>(&morphData.toonTextureFactor), 16);
}
}
else if (morph.morphType == PMXMorphType::Group)
{
morph.groupMorph.resize(dataCount);
for (auto& morphData : morph.groupMorph)
{
file.read(reinterpret_cast<char*>(&morphData.morphIndex), data.header.morphIndexSize);
file.read(reinterpret_cast<char*>(&morphData.weight), 4);
}
}
else if (morph.morphType == PMXMorphType::Flip)
{
morph.flipMorph.resize(dataCount);
for (auto& morphData : morph.flipMorph)
{
file.read(reinterpret_cast<char*>(&morphData.morphIndex), data.header.morphIndexSize);
file.read(reinterpret_cast<char*>(&morphData.weight), 4);
}
}
else if (morph.morphType == PMXMorphType::Impluse)
{
morph.impulseMorph.resize(dataCount);
for (auto& morphData : morph.impulseMorph)
{
file.read(reinterpret_cast<char*>(&morphData.rigidBodyIndex), data.header.rigidBodyIndexSize);
file.read(reinterpret_cast<char*>(&morphData.localFlag), 1);
file.read(reinterpret_cast<char*>(&morphData.translateVelocity), 12);
file.read(reinterpret_cast<char*>(&morphData.rotateTorque), 12);
}
}
else
{
return false;
}
}
return true;
}
ディスプレイフレーム情報の読み込み
この情報は使用しないため、特に説明しません。
struct PMXDisplayFrame
{
std::wstring name;
std::string englishName;
enum class TargetType : uint8_t
{
BoneIndex,
MorphIndex,
};
struct Target
{
TargetType type;
unsigned int index;
};
enum class FrameType : uint8_t
{
DefaultFrame,
SpecialFrame
};
FrameType flag;
std::vector<Target> targets;
};
bool ReadDisplayFrame(PMXFileData& data, std::ifstream& file)
{
unsigned int numOfDisplayFrame = 0;
file.read(reinterpret_cast<char*>(&numOfDisplayFrame), 4);
data.displayFrames.resize(numOfDisplayFrame);
for (auto& displayFrame : data.displayFrames)
{
UnicodeUtil::GetPMXStringUTF16(file, displayFrame.name);
UnicodeUtil::GetPMXStringUTF8(file, displayFrame.englishName);
file.read(reinterpret_cast<char*>(&displayFrame.flag), 1);
unsigned int targetCount = 0;
file.read(reinterpret_cast<char*>(&targetCount), 4);
displayFrame.targets.resize(targetCount);
for (auto& target : displayFrame.targets)
{
file.read(reinterpret_cast<char*>(&target.type), 1);
if (target.type == PMXDisplayFrame::TargetType::BoneIndex)
{
file.read(reinterpret_cast<char*>(&target.index), data.header.boneIndexSize);
}
else if (target.type == PMXDisplayFrame::TargetType::MorphIndex)
{
file.read(reinterpret_cast<char*>(&target.index), data.header.morphIndexSize);
}
else
{
return false;
}
}
}
return true;
}
剛体、ジョイント、ソフトボディ情報の読み込み
これらはすべて物理シミュレーションに関連する情報です。
今回はこれらが何であるかを説明すると長くなりすぎるため、後で物理エンジンを追加する際に詳しく見ていくことにします。今回は情報をどのように読み込むかだけを見ていきましょう。
剛体の構造体は次のとおりです。
struct PMXRigidBody
{
std::wstring name;
std::string englishName;
unsigned int boneIndex;
unsigned char group;
unsigned short collisionGroup;
enum class Shape : uint8_t
{
Sphere,
Box,
Capsule
};
Shape shape;
DirectX::XMFLOAT3 shapeSize;
DirectX::XMFLOAT3 translate;
DirectX::XMFLOAT3 rotate;
float mass;
float translateDimmer;
float rotateDimmer;
float repulsion;
float friction;
enum class Operation : uint8_t
{
Static,
Dynamic,
DynamicAndBoneMerge,
};
Operation op;
};
読み込むメソッドは次のとおりです。
bool ReadRigidBody(PMXFileData& data, std::ifstream& file)
{
unsigned int numOfRigidBody = 0;
file.read(reinterpret_cast<char*>(&numOfRigidBody), 4);
data.rigidBodies.resize(numOfRigidBody);
for (auto& rigidBody : data.rigidBodies)
{
UnicodeUtil::GetPMXStringUTF16(file, rigidBody.name);
UnicodeUtil::GetPMXStringUTF8(file, rigidBody.englishName);
file.read(reinterpret_cast<char*>(&rigidBody.boneIndex), data.header.boneIndexSize);
file.read(reinterpret_cast<char*>(&rigidBody.group), 1);
file.read(reinterpret_cast<char*>(&rigidBody.collisionGroup), 2);
file.read(reinterpret_cast<char*>(&rigidBody.shape), 1);
file.read(reinterpret_cast<char*>(&rigidBody.shapeSize), 12);
file.read(reinterpret_cast<char*>(&rigidBody.translate), 12);
file.read(reinterpret_cast<char*>(&rigidBody.rotate), 12);
file.read(reinterpret_cast<char*>(&rigidBody.mass), 4);
file.read(reinterpret_cast<char*>(&rigidBody.translateDimmer), 4);
file.read(reinterpret_cast<char*>(&rigidBody.rotateDimmer), 4);
file.read(reinterpret_cast<char*>(&rigidBody.repulsion), 4);
file.read(reinterpret_cast<char*>(&rigidBody.friction), 4);
file.read(reinterpret_cast<char*>(&rigidBody.op), 1);
}
return true;
}
ジョイントの構造体は次のとおりです。
struct PMXJoint
{
std::wstring name;
std::string englishName;
enum class JointType : uint8_t
{
SpringDOF6,
DOF6,
P2P,
ConeTwist,
Slider,
Hinge,
};
JointType type;
unsigned int rigidBodyAIndex;
unsigned int rigidBodyBIndex;
DirectX::XMFLOAT3 translate;
DirectX::XMFLOAT3 rotate;
DirectX::XMFLOAT3 translateLowerLimit;
DirectX::XMFLOAT3 translateUpperLimit;
DirectX::XMFLOAT3 rotateLowerLimit;
DirectX::XMFLOAT3 rotateUpperLimit;
DirectX::XMFLOAT3 springTranslateFactor;
DirectX::XMFLOAT3 springRotateFactor;
};
読み込むメソッドは次のとおりです。
bool ReadJoint(PMXFileData& data, std::ifstream& file)
{
unsigned int numOfJoint = 0;
file.read(reinterpret_cast<char*>(&numOfJoint), 4);
data.joints.resize(numOfJoint);
for (auto& joint : data.joints)
{
UnicodeUtil::GetPMXStringUTF16(file, joint.name);
UnicodeUtil::GetPMXStringUTF8(file, joint.englishName);
file.read(reinterpret_cast<char*>(&joint.type), 1);
file.read(reinterpret_cast<char*>(&joint.rigidBodyAIndex), data.header.rigidBodyIndexSize);
file.read(reinterpret_cast<char*>(&joint.rigidBodyBIndex), data.header.rigidBodyIndexSize);
file.read(reinterpret_cast<char*>(&joint.translate), 12);
file.read(reinterpret_cast<char*>(&joint.rotate), 12);
file.read(reinterpret_cast<char*>(&joint.translateLowerLimit), 12);
file.read(reinterpret_cast<char*>(&joint.translateUpperLimit), 12);
file.read(reinterpret_cast<char*>(&joint.rotateLowerLimit), 12);
file.read(reinterpret_cast<char*>(&joint.rotateUpperLimit), 12);
file.read(reinterpret_cast<char*>(&joint.springTranslateFactor), 12);
file.read(reinterpret_cast<char*>(&joint.springRotateFactor), 12);
}
return true;
}
ソフトボディの構造体は次のとおりです。
struct PMXSoftBody
{
std::wstring name;
std::string englishName;
enum class SoftBodyType : uint8_t
{
TriMesh,
Rope
};
SoftBodyType type;
unsigned int materialIndex;
unsigned char group;
unsigned short collisionGroup;
enum class SoftBodyMask : uint8_t
{
BLink = 0x01,
Cluster = 0x02,
HybridLink = 0x04,
};
SoftBodyMask flag;
unsigned int bLinkLength;
unsigned int numClusters;
float totalMass;
float collisionMargin;
enum class AeroModel : int32_t
{
kAeroModelV_TwoSided,
kAeroModelV_OneSided,
kAeroModelF_TwoSided,
kAeroModelF_OneSided,
};
unsigned int areoModel;
float vcf;
float dp;
float dg;
float lf;
float pr;
float vc;
float df;
float mt;
float chr;
float khr;
float shr;
float ahr;
float srhr_cl;
float skhr_cl;
float sshr_cl;
float sr_splt_cl;
float sk_splt_cl;
float ss_splt_cl;
unsigned int v_it;
unsigned int p_it;
unsigned int d_it;
unsigned int c_it;
float lst;
float ast;
float vst;
struct AnchorRigidBody
{
unsigned int rigidBodyIndex;
unsigned int vertexIndex;
unsigned char nearMode;
};
std::vector<AnchorRigidBody> anchorRigidBodies;
std::vector<unsigned int> pinVertexIndices;
};
読み込むメソッドは次のとおりです。
bool ReadSoftBody(PMXFileData& data, std::ifstream& file)
{
unsigned int numOfSoftBody = 0;
file.read(reinterpret_cast<char*>(&numOfSoftBody), 4);
data.softBodies.resize(numOfSoftBody);
for (auto& softBody : data.softBodies)
{
UnicodeUtil::GetPMXStringUTF16(file, softBody.name);
UnicodeUtil::GetPMXStringUTF8(file, softBody.englishName);
file.read(reinterpret_cast<char*>(&softBody.type), 1);
file.read(reinterpret_cast<char*>(&softBody.materialIndex), data.header.materialIndexSize);
file.read(reinterpret_cast<char*>(&softBody.group), 1);
file.read(reinterpret_cast<char*>(&softBody.collisionGroup), 2);
file.read(reinterpret_cast<char*>(&softBody.flag), 1);
file.read(reinterpret_cast<char*>(&softBody.bLinkLength), 4);
file.read(reinterpret_cast<char*>(&softBody.numClusters), 4);
file.read(reinterpret_cast<char*>(&softBody.totalMass), 4);
file.read(reinterpret_cast<char*>(&softBody.collisionMargin), 4);
file.read(reinterpret_cast<char*>(&softBody.areoModel), 4);
file.read(reinterpret_cast<char*>(&softBody.vcf), 4);
file.read(reinterpret_cast<char*>(&softBody.dp), 4);
file.read(reinterpret_cast<char*>(&softBody.dg), 4);
file.read(reinterpret_cast<char*>(&softBody.lf) , 4);
file.read(reinterpret_cast<char*>(&softBody.pr), 4);
file.read(reinterpret_cast<char*>(&softBody.vc), 4);
file.read(reinterpret_cast<char*>(&softBody.df), 4);
file.read(reinterpret_cast<char*>(&softBody.mt), 4);
file.read(reinterpret_cast<char*>(&softBody.chr), 4);
file.read(reinterpret_cast<char*>(&softBody.khr), 4);
file.read(reinterpret_cast<char*>(&softBody.shr), 4);
file.read(reinterpret_cast<char*>(&softBody.ahr), 4);
file.read(reinterpret_cast<char*>(&softBody.srhr_cl), 4);
file.read(reinterpret_cast<char*>(&softBody.skhr_cl), 4);
file.read(reinterpret_cast<char*>(&softBody.sshr_cl), 4);
file.read(reinterpret_cast<char*>(&softBody.sr_splt_cl), 4);
file.read(reinterpret_cast<char*>(&softBody.sk_splt_cl), 4);
file.read(reinterpret_cast<char*>(&softBody.ss_splt_cl), 4);
file.read(reinterpret_cast<char*>(&softBody.v_it), 4);
file.read(reinterpret_cast<char*>(&softBody.p_it), 4);
file.read(reinterpret_cast<char*>(&softBody.d_it), 4);
file.read(reinterpret_cast<char*>(&softBody.c_it), 4);
file.read(reinterpret_cast<char*>(&softBody.lst), 4);
file.read(reinterpret_cast<char*>(&softBody.ast), 4);
file.read(reinterpret_cast<char*>(&softBody.vst), 4);
unsigned int anchorCount = 0;
file.read(reinterpret_cast<char*>(&anchorCount), 4);
softBody.anchorRigidBodies.resize(anchorCount);
for (auto& anchor : softBody.anchorRigidBodies)
{
file.read(reinterpret_cast<char*>(&anchor.rigidBodyIndex), data.header.rigidBodyIndexSize);
file.read(reinterpret_cast<char*>(&anchor.vertexIndex), data.header.vertexIndexSize);
file.read(reinterpret_cast<char*>(&anchor.nearMode), 1);
}
unsigned int pinVertexCount = 0;
file.read(reinterpret_cast<char*>(&pinVertexCount), 4);
softBody.pinVertexIndices.resize(pinVertexCount);
for (auto& pinVertex : softBody.pinVertexIndices)
{
file.read(reinterpret_cast<char*>(&pinVertex), data.header.vertexIndexSize);
}
}
return true;
}
データの確認
ここまで準備ができたら、実際にファイルを読み込んでデータが正しく読み取れているか確認してください。
コード自体が長いため、見落としている箇所があるかもしれませんが、問題がなければデータが正しく読み込んでいることを確認できるはずです。
bool LoadPMXFile(const std::wstring& filePath, PMXFileData& fileData)
{
if (filePath.empty() == true)
{
return false;
}
std::ifstream pmxFile{ filePath, (std::ios::binary | std::ios::in) };
if (pmxFile.fail())
{
pmxFile.close();
return false;
}
bool result = ReadHeader(fileData, pmxFile);
if (result == false)
{
return false;
}
result = ReadModelInfo(fileData, pmxFile);
if (result == false)
{
return false;
}
result = ReadVertex(fileData, pmxFile);
if (result == false)
{
return false;
}
result = ReadFace(fileData, pmxFile);
if (result == false)
{
return false;
}
result = ReadTextures(fileData, pmxFile);
if (result == false)
{
return false;
}
result = ReadMaterial(fileData, pmxFile);
if (result == false)
{
return false;
}
result = ReadBone(fileData, pmxFile);
if (result == false)
{
return false;
}
result = ReadMorph(fileData, pmxFile);
if (result == false)
{
return false;
}
result = ReadDisplayFrame(fileData, pmxFile);
if (result == false)
{
return false;
}
result = ReadRigidBody(fileData, pmxFile);
if (result == false)
{
return false;
}
result = ReadJoint(fileData, pmxFile);
if (result == false)
{
return false;
}
result = ReadSoftBody(fileData, pmxFile);
if (result == false)
{
return false;
}
pmxFile.close();
return true;
}
まとめ
今回は、PMXモデルファイルがどのように構成されているか、そしてどのように読み込むべきかを学びました。
特に考えるべき点はなく、既存のフォーマットを読み込むだけなので、退屈な作業です。
後半に出てくる物理シミュレーションに関連するデータについては、特に説明しませんでした。内容が非常に多く、実際に適用するまでどのような役割を果たすデータなのか理解するのが難しいです。そのため、後で実際に使用する際にまた話すことにします。
次の記事では、読み込んだPMXデータを使ってミクさんを画面に描画してみましょう。
次回