スマートフォンでお手軽3D
今回は3D関係について書きたいと思います。
なお、サンプル画像と動画が記事の最後にあります!!
概要
iPhoneのOpenGL用にさまざまなライブラリおよびミドルウェアは出ていますけれど、今回は、iPhoneの3Dチップメーカーが出しているライブラリの紹介をしたいと思います。一応公式のチップメーカーがだしているし、便利でお手軽なライブラリなのにいまいち検索しても日本語のサイトが出てこないので、開発の方に広く知ってもらいたいと思い、書こうと思いました。
iPhoneやAndroidのOpenGL Viewの使い方は一部紹介しますが、検索すればいくらでも出てくるのでそこは調べてもらうものとします。また、スキンドメッシュの原理に関しては、ちょこっとだけ解説しますが、詳しくは長くなるので、グラフィックスの専門のサイトにまかせてライブラリそのものの使い方について書きたいと思います。
ライブラリを出しているメーカーについて
ライブラリを出しているメーカーはImagination社という会社で、古くはゲーム機のDreamCastの3DチップメーカーとしてPowerVRを開発したメーカーです。そのメーカーがiPhone用の3Dチップを作っているというのも面白いですね。そこがiPhone用、Android用にスキンメッシュ(ポリゴンにつなぎ目がなく曲げることができる技術)まで対応したライブラリを開発してくれています。
ライブラリについて
車輪の再発明(時には大事ですが)をしなくとも、お手軽に3Dのデータを表示できるのでありがたく使わせてもらいましょう。もちろんチップメーカーが開発しているので、速度面に関してもいうことはなく最適化されていますし、MAYA、3DStudioMax、Blenderという商用のお高いソフトからフリーの3Dソフトまで対応していて、3Dのデータを出力してくれるプラグインまで開発してくれているので、フリーの環境でそろえたいという方から、商用のソフトはあるんだけどプラグインまで開発したくないなぁという方まで、便利に使えるSDKのセットになっています。
また、ある一部のゲームメーカーのプラットフォームで標準になっている3DのXML形式のColladaからも、ライブラリで使用できるPodという形式(アニメーション付き3Dフォーマット)に変換できるようになっています。
スキンメッシュの原理の簡単な説明
3Dオブジェクトとボーン(関節のような頂点を変形するもの)の関係は下の図のようになっています。
![]() |
この図をみると、3Dのオブジェクトに対してピンク色の菱形の物体が内部にはいっていると思います。
そのピンク色のボーンと呼ばれる物体が3Dオブジェクトの頂点をどの程度移動するかそれぞれの頂点に保存されており、この値をウェイトと呼びますが、このウェイト値とボーンの回転とか移動とかのマトリックスを乗算して頂点をどの程度まげるか決め、頂点に乗算し3Dポリゴンを変形します。
このライブラリを使う利点と欠点
このライブラリを使う利点は、お手軽に他のミドルウェアにありがちな、細かい処理が見えにくいということがなく、シンプルに3Dのアニメーションオブジェクトを表示することができることと、その割には、MAYA、3DStudioMax、Blenderという3つのソフトに対応していることです。
ただし弱点もあって、細かい仕様になりますが、4つ以上のサーフェース(マテリアルをもつポリゴン)にボーンがまたがって影響を及ぼすようなモデルには対応していないということです。まぁゲームとかですと、1サーフェースでUV(テクスチャを貼付けるための値)を1つだけ持つのが多かったりするし、そこは我慢しましょう。
iPhoneの場合
準備
まず、SDKをダウンロードしてきましょう、日本のImaginationのサイトもあるのですが、SDKが本家のサイトからダウンロードしてきた方が最新なので、本家から持ってきましょう。
登録してSDKをダウンロードしてきます。OpenGLES2.0のSDKのライブラリはShaderを使ったものになっていて、使いにくいので、1.0のSDKを持ってきます。
1.0のImaginationのSDKは、iPhoneのSDKのバージョンが古いもので作られたものらしく、バージョンの変更や、一部ソースを書き換えなくてはサンプルなどがコンパイルできなくなっていますが、ライブラリは特に変更しなくとも利用することが可能です。
ライブラリを変更するまえにOpenGLを利用できるViewについて
OpenGLを利用するためにちょっとUIViewを継承したViewを作る必要があります。
XCodeには、標準でOpenGLのViewを作るためのウィザードがあるので、それで作ったEAGLViewというのを改造して今回のライブラリが利用できるように変更しましょう。
ViewにはContextというものがあるのですが、そのGL用のContextには今のところ2種類ありまして、gl~Matrix(glPushMatrixとか)が使えないマトリックスを全部シェーダーで書かなければいけないモードと、gl~Matrix系の関数はすべて使えてシェーダーを使わないで構築するモードとがあります。
今回のライブラリはマトリックスを使ってやるライブラリになっていますので、それを使います。
ウィザードで作られたOpenGL Viewは、両方に対応していて、kEAGLRenderingAPIOpenGLES2というモード(マトリックスが使えないモード)が使えない場合は、kEAGLRenderingAPIOpenGLES1というモードを使うようになっています。ただし、iphone3GS以降はkEAGLRenderingAPIOpenGLES2が使えますので、強制的にkEAGLRenderingAPIOpenGLES1のモードを選択するようにしてやる必要があります。どちらかを選択する部分があると思いますのでそこを改造します。
あとは、drawFrame、または、renderメソッドに描画処理をいれたりすれば描画ができます。
お手軽に利用するには
サンプルにSkinningというもろに使えるサンプルがあるので、そこからソースを利用させてもらいましょう。ただし一般的に利用するためにはちょっと変更する必要があります。
サンプルを変更し自分用のライブラリへ
TutorialにSkinningというのがあるので、それのサンプルのなかにOGLESSkinning.cppというソースがあり、そのソースのテクスチャロードの部分のメソッドを変更します。
元のソースでは、テクスチャファイル名がハードコードされているので、マテリアルからテクスチャ名をとってきてテクスチャをロードする部分を作成します。
サンプルの方では、ライブラリの固有のテクスチャ圧縮形式であるpvr形式というものが使われているようですが、マテリアルの中に書いてあるファイル名は普通の画像形式(3Dソフトのプラグインから出力された時のテクスチャ名)が入っているので、それをロードするコーディングをします。
LoadTexturesメソッドの中身を書き換えて、PVRTextureLoadFromPVRを使ってテクスチャ画像をファイル名から読み込んでいる部分を書き換えます。具体的な書き換え方は、CoreGraphics系のライブラリを使って、画像を読み込み、それをglBindTextureとかglTexImage2Dとかを使って設定します。
bool OGLESSkinning::LoadTextures(CPVRTString* const pErrorStr)
{
/*if(PVRTTextureLoadFromPVR(c_szBodyTexFile, &m_uiBodyTex) != PVR_SUCCESS)
{
*pErrorStr = "ERROR: Failed to load body texture.";
return false;
}
if(PVRTTextureLoadFromPVR(c_szLegTexFile, &m_uiLegTex) != PVR_SUCCESS)
{
*pErrorStr = "ERROR: Failed to load leg texture.";
return false;
}
if(PVRTTextureLoadFromPVR(c_szBeltTexFile, &m_uiBeltTex) != PVR_SUCCESS)
{
*pErrorStr = "ERROR: Failed to load belt texture.";
return false;
}*/
m_puiTextures = new GLuint[m_Scene.nNumMaterial];
if(!m_puiTextures)
{
//*pErrorStr = "ERROR: Insufficient memory.";
return false;
}
m_textureNum=0;
for(int i = 0; i < (int) m_Scene.nNumMaterial; ++i)
{
m_puiTextures[i] = 0;
SPODMaterial* pMaterial = &m_Scene.pMaterial[i];
if(pMaterial->nIdxTexDiffuse != -1)
{
/*
Using the tools function PVRTTextureLoadFromPVR load the textures required by the pod file.
Note: This function only loads .pvr files. You can set the textures in 3D Studio Max to .pvr
files using the PVRTexTool plug-in for max. Alternatively, the pod material properties can be
modified in PVRShaman.
*/
CPVRTString sTextureName = m_Scene.pTexture[pMaterial->nIdxTexDiffuse].pszName;
int len = strlen(sTextureName.c_str());
char * strc = (char *)malloc(sizeof(char)*(len+1));
strcpy(strc,sTextureName.c_str());
CPVRTString fullpath=CPVRTResourceFile::GetReadPath();
len =strlen(fullpath.c_str());
char * full = (char *)malloc(sizeof(char)*(len+1));
strcpy(full,fullpath.c_str());
NSString *patht= [[NSString alloc] initWithUTF8String:full];
NSString *str = [[NSString alloc] initWithUTF8String:strc];
NSString *pathname=[patht stringByAppendingString:str];
free(strc);
free(full);
CGImageRef image = [UIImage imageWithContentsOfFile:pathname].CGImage;
NSInteger width = CGImageGetWidth(image);
NSInteger height = CGImageGetHeight(image);
GLubyte* bits = (GLubyte*)malloc(width * height * 4);
CGImageAlphaInfo info;
BOOL hasAlpha;
size_t bitsPerComponent;
info=CGImageGetAlphaInfo(image);
//アルファ成分チェック
hasAlpha=((info==kCGImageAlphaPremultipliedLast) ||
(info==kCGImageAlphaPremultipliedFirst) ||
(info==kCGImageAlphaLast) ||
(info==kCGImageAlphaFirst)?YES:NO);
if (hasAlpha) {
bitsPerComponent=kCGImageAlphaPremultipliedLast;
} else {
bitsPerComponent=kCGImageAlphaNoneSkipLast;
}
CGColorSpaceRef colorSpace=CGColorSpaceCreateDeviceRGB();
CGContextRef textureContext =
CGBitmapContextCreate(bits, width, height, 8, width * 4,
colorSpace, bitsPerComponent);
CGContextScaleCTM(textureContext, 1, -1);
CGContextTranslateCTM(textureContext, 0, -(double)height);
CGColorSpaceRelease(colorSpace);
CGContextClearRect(textureContext,CGRectMake(0.0, 0.0, width, height));
CGContextDrawImage(textureContext, CGRectMake(0.0, 0.0, width, height), image);
CGContextRelease(textureContext);
glGenTextures(1, &m_puiTextures[i]);
glBindTexture(GL_TEXTURE_2D, m_puiTextures[i]);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, bits);
glBindTexture(GL_TEXTURE_2D, 0);
free(bits);
}
}
return true;
}
CPVRTModelPOD型の変数の簡単な説明
CPVRTModelPODは3Dのシーンを管理しているクラスで、3Dシーンファイル(Pod形式)の読み込みのメソッドや、アニメーション部分のマトリックス(クォータニオン)の計算部分を自動でやってくれるクラスになっています。
CPVRTModelPODクラスに大してSetFrameして、あとはOpenGLのExtensionにマトリックスを突っ込んでやればオブジェクトの変形をしてくれるようになっています。こんな感じです。
void OGLESSkinning::DrawModel()
{
//Set the frame number
m_Scene.SetFrame(m_fFrame);
// Enable lighting
if(lightsw)
glEnable(GL_LIGHTING);
else
glDisable(GL_LIGHTING);
// Enable States
glEnableClientState(GL_VERTEX_ARRAY);
glEnableClientState(GL_NORMAL_ARRAY);
m_MeshNum=0;
//Iterate through all the mesh nodes in the scene
for(int iNode = 0; iNode < (int)m_Scene.nNumMeshNode; ++iNode)
{
//Get the mesh node.
SPODNode* pNode = &m_Scene.pNode[iNode];
//Get the mesh that the mesh node uses.
SPODMesh* pMesh = &m_Scene.pMesh[pNode->nIdx];
// bind the VBO for the mesh
glBindBuffer(GL_ARRAY_BUFFER, m_puiVbo[pNode->nIdx]);
// bind the index buffer, won't hurt if the handle is 0
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_puiIndexVbo[pNode->nIdx]);
// Loads the correct texture using our texture lookup table
if(pNode->nIdxMaterial == -1 )
glBindTexture(GL_TEXTURE_2D, 0); // It has no pMaterial defined. Use blank texture (0)
else
glBindTexture(GL_TEXTURE_2D, m_puiTextures[pNode->nIdxMaterial]);
//If the mesh has bone weight data then we must be skinning.
bool bSkinning = pMesh->sBoneWeight.n != 0;
if(bSkinning)
{
//If we are skinning then enable the relevant states.
glEnableClientState(GL_MATRIX_INDEX_ARRAY_OES);
glEnableClientState(GL_WEIGHT_ARRAY_OES);
if(iNode==0)
{
if(m_fFrame==0)
{
m_max_vertex[0]=m_max_vertex[1]=m_max_vertex[2]=FLT_MIN;
m_min_vertex[0]=m_min_vertex[1]=m_min_vertex[2]=FLT_MAX;
}
m_max_vertex_anim[0]=m_max_vertex_anim[1]=m_max_vertex_anim[2]=FLT_MIN;
m_min_vertex_anim[0]=m_min_vertex_anim[1]=m_min_vertex_anim[2]=FLT_MAX; }
}
else
{
// If we're not using matrix palette then get the world matrix for the mesh
// and transform the model view matrix by it.
PVRTMat4 worldMatrix;
m_Scene.GetWorldMatrix(worldMatrix, *pNode);
//Push the modelview matrix
glPushMatrix();
glMultMatrixf(m_mTransform.f);
glMultMatrixf(worldMatrix.f);
}
// Set Data Pointers
// Used to display non interleaved geometry
glVertexPointer(pMesh->sVertex.n, GL_FLOAT, pMesh->sVertex.nStride, pMesh->sVertex.pData);
glNormalPointer(GL_FLOAT, pMesh->sNormals.nStride, pMesh->sNormals.pData);
if(pMesh->psUVW)
{
glEnableClientState(GL_TEXTURE_COORD_ARRAY);
glTexCoordPointer(pMesh->psUVW[0].n, GL_FLOAT, pMesh->psUVW[0].nStride, pMesh->psUVW[0].pData);
}
if(bSkinning)
{
//Set up the indexes into the matrix palette.
m_Extensions.glMatrixIndexPointerOES(pMesh->sBoneIdx.n, GL_UNSIGNED_BYTE, pMesh->sBoneIdx.nStride, pMesh->sBoneIdx.pData);
m_Extensions.glWeightPointerOES(pMesh->sBoneWeight.n, GL_FLOAT, pMesh->sBoneWeight.nStride, pMesh->sBoneWeight.pData);
}
// Draw
int i32Strip = 0;
int i32Offset = 0;
for(int i32Batch = 0; i32Batch <pMesh->sBoneBatches.nBatchCnt; ++i32Batch)
{
// If the mesh is used for skining then set up the matrix palettes.
if(bSkinning)
{
//Enable the matrix palette extension
glEnable(GL_MATRIX_PALETTE_OES);
/*
Enables the matrix palette stack extension, and apply subsequent
matrix operations to the matrix palette stack.
*/
glMatrixMode(GL_MATRIX_PALETTE_OES);
PVRTMat4 mBoneWorld;
int i32NodeID;
// Iterate through all the bones in the batch
for(int j = 0; j < pMesh->sBoneBatches.pnBatchBoneCnt[i32Batch]; ++j)
{
/*
Set the current matrix palette that we wish to change. An error
will be returned if the index (j) is not between 0 and
GL_MAX_PALETTE_MATRICES_OES. The value of GL_MAX_PALETTE_MATRICES_OES
can be retrieved using glGetIntegerv, the initial value is 9.
GL_MAX_PALETTE_MATRICES_OES does not mean you need to limit
your character to 9 bones as you can overcome this limitation
by using bone batching which splits the mesh up into sub-meshes
which use only a subset of the bones.
*/
m_Extensions.glCurrentPaletteMatrixOES(j);
// Generates the world matrix for the given bone in this batch.
i32NodeID = pMesh->sBoneBatches.pnBatches[i32Batch * pMesh->sBoneBatches.nBatchBoneMax + j];
m_Scene.GetBoneWorldMatrix(mBoneWorld, *pNode, m_Scene.pNode[i32NodeID]);
// Multiply the bone's world matrix by our transformation matrix and the view matrix
mBoneWorld = m_mView * m_mTransform * mBoneWorld;
// Load the bone matrix into the current palette matrix.
glLoadMatrixf(mBoneWorld.f);
}
}
else
{
//If we're not skinning then disable the matrix palette.
glDisable(GL_MATRIX_PALETTE_OES);
}
//Switch to the modelview matrix.
glMatrixMode(GL_MODELVIEW);
// Calculate the number of triangles in the current batch
int i32Tris;
if(i32Batch + 1 < pMesh->sBoneBatches.nBatchCnt)
i32Tris = pMesh->sBoneBatches.pnBatchOffset[i32Batch+1] - pMesh->sBoneBatches.pnBatchOffset[i32Batch];
else
i32Tris = pMesh->nNumFaces - pMesh->sBoneBatches.pnBatchOffset[i32Batch];
// Indexed Triangle list
if(pMesh->nNumStrips == 0)
{
glDrawElements(GL_TRIANGLES, i32Tris * 3, GL_UNSIGNED_SHORT, &((unsigned short*)0)[3 * pMesh->sBoneBatches.pnBatchOffset[i32Batch]]);
}
else // Indexed Triangle strips
{
int i32TrisDrawn = 0;
while(i32TrisDrawn < i32Tris)
{
glDrawElements(GL_TRIANGLE_STRIP, pMesh->pnStripLength[i32Strip]+2, GL_UNSIGNED_SHORT, &((GLshort*)0)[i32Offset]);
i32Offset += pMesh->pnStripLength[i32Strip]+2;
i32TrisDrawn += pMesh->pnStripLength[i32Strip];
++i32Strip;
}
}
}
if(!pMesh->sBoneBatches.nBatchCnt)
{
glDrawElements(GL_TRIANGLES, pMesh->nNumFaces*3, GL_UNSIGNED_SHORT, 0);
}
if(bSkinning)
{
glDisableClientState(GL_MATRIX_INDEX_ARRAY_OES);
glDisableClientState(GL_WEIGHT_ARRAY_OES);
// We are finished with the matrix pallete so disable it.
glDisable(GL_MATRIX_PALETTE_OES);
}
else
{
//Reset the modelview matrix back to what it was before we transformed by the mesh node.
glPopMatrix();
}
}
// Disable States
glDisableClientState(GL_VERTEX_ARRAY);
glDisableClientState(GL_NORMAL_ARRAY);
glDisableClientState(GL_TEXTURE_COORD_ARRAY);
// unbind the vertex buffers as we don't need them bound anymore
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
}
Androidの場合
準備
Androidの場合もSDKをダウンロードしてきましょう。
そのときにダウンロードしてくるのは、OpenGLES1.1のものをダウンロードしてきましょう。Androidの場合、OpenGLの部分はJavaではなく、C++で書かれているため、合わせてNDKを使ったアプリがコンパイルできる環境を準備しましょう。
お手軽に利用するには
Android版のほうはスキンアニメのサンプルがなぜか入ってないので、その部分を作らなければならないのですが、描画する部分はiPhone版のソースからそのまま持ってくれば使えるので大丈夫だと思います。
ただ、注意しなくてはならないのは、Android版は3Dのオブジェクトのデータをリソースなどに持っているのではなく、Cのソースにしてリンクしてそれを呼び出す感じになってしまっているので、それが嫌な場合は、Java側のソースでリソースのデータを/data/ディレクトリに書き出して、それをC側のソースから読み込んでやるようにしてやればよいと思います。
リソースをNDKで読み出す方法については、詳しくは検索してもらうとして、サンプルがないので0から作るのも大変なので、OGLESIntroducingPODを改造して作るとしましょう。
サンプルを変更し自分用のライブラリへ
まず、変更しなくてはいけないところはモデルを描画するところです。OGLESIntroducingPod.cppのOGLESIntroducingPOD::RenderSceneというメソッドの中身をちょっと変更します。そのメソッドの最後のほうにメッシュを描画している部分がありますが、その部分を全部コメントアウトして、独自のメソッドDrawModelを追加しましょう。
そのDrawModelメソッドは次の通りです(ほとんどiPhoneのソースから持ってきたものです)。
void OGLESIntroducingPOD::DrawModel()
{
//Set the frame number
m_Scene.SetFrame(m_fFrame);
// Enable lighting
glEnable(GL_LIGHTING);
// Enable States
glEnableClientState(GL_VERTEX_ARRAY);
glEnableClientState(GL_NORMAL_ARRAY);
//Iterate through all the mesh nodes in the scene
for(int iNode = 0; iNode < (int)m_Scene.nNumMeshNode; ++iNode)
{
//Get the mesh node.
SPODNode* pNode = &m_Scene.pNode[iNode];
//Get the mesh that the mesh node uses.
SPODMesh* pMesh = &m_Scene.pMesh[pNode->nIdx];
// bind the VBO for the mesh
glBindBuffer(GL_ARRAY_BUFFER, m_puiVbo[pNode->nIdx]);
// bind the index buffer, won't hurt if the handle is 0
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_puiIndexVbo[pNode->nIdx]);
// Loads the correct texture using our texture lookup table
if(pNode->nIdxMaterial == -1 || !texsw)
glBindTexture(GL_TEXTURE_2D, 0); // It has no pMaterial defined. Use blank texture (0)
else
glBindTexture(GL_TEXTURE_2D, m_puiTextures[pNode->nIdxMaterial]);
//If the mesh has bone weight data then we must be skinning.
bool bSkinning = pMesh->sBoneWeight.n != 0;
if(bSkinning)
{
//If we are skinning then enable the relevant states.
glEnableClientState(GL_MATRIX_INDEX_ARRAY_OES);
glEnableClientState(GL_WEIGHT_ARRAY_OES);
}
else
{
// If we're not using matrix palette then get the world matrix for the mesh
// and transform the model view matrix by it.
PVRTMat4 worldMatrix;
m_Scene.GetWorldMatrix(worldMatrix, *pNode);
//Push the modelview matrix
glPushMatrix();
glMultMatrixf(m_mTransform.f);
glMultMatrixf(worldMatrix.f);
}
// Set Data Pointers
// Used to display non interleaved geometry
glVertexPointer(pMesh->sVertex.n, GL_FLOAT, pMesh->sVertex.nStride, pMesh->sVertex.pData);
glNormalPointer(GL_FLOAT, pMesh->sNormals.nStride, pMesh->sNormals.pData);
if(pMesh->psUVW)
{
glEnableClientState(GL_TEXTURE_COORD_ARRAY);
glTexCoordPointer(pMesh->psUVW[0].n, GL_FLOAT, pMesh->psUVW[0].nStride, pMesh->psUVW[0].pData);
}
if(bSkinning)
{
//Set up the indexes into the matrix palette.
glMatrixIndexPointerOES(pMesh->sBoneIdx.n, GL_UNSIGNED_BYTE, pMesh->sBoneIdx.nStride, pMesh->sBoneIdx.pData);
glWeightPointerOES(pMesh->sBoneWeight.n, GL_FLOAT, pMesh->sBoneWeight.nStride, pMesh->sBoneWeight.pData);
}
// Draw
int i32Strip = 0;
int i32Offset = 0;
for(int i32Batch = 0; i32Batch <pMesh->sBoneBatches.nBatchCnt; ++i32Batch)
{
// If the mesh is used for skining then set up the matrix palettes.
if(bSkinning)
{
//Enable the matrix palette extension
glEnable(GL_MATRIX_PALETTE_OES);
glMatrixMode(GL_MATRIX_PALETTE_OES);
PVRTMat4 mBoneWorld;
int i32NodeID;
// Iterate through all the bones in the batch
for(int j = 0; j < pMesh->sBoneBatches.pnBatchBoneCnt[i32Batch]; ++j)
{
glCurrentPaletteMatrixOES(j);
// Generates the world matrix for the given bone in this batch.
i32NodeID = pMesh->sBoneBatches.pnBatches[i32Batch * pMesh->sBoneBatches.nBatchBoneMax + j];
m_Scene.GetBoneWorldMatrix(mBoneWorld, *pNode, m_Scene.pNode[i32NodeID]);
// Multiply the bone's world matrix by our transformation matrix and the view matrix
mBoneWorld = m_mView * mBoneWorld;
// Load the bone matrix into the current palette matrix.
glLoadMatrixf(mBoneWorld.f);
}
}
else
{
//If we're not skinning then disable the matrix palette.
glDisable(GL_MATRIX_PALETTE_OES);
}
//Switch to the modelview matrix.
glMatrixMode(GL_MODELVIEW);
// Calculate the number of triangles in the current batch
int i32Tris;
if(i32Batch + 1 < pMesh->sBoneBatches.nBatchCnt)
i32Tris = pMesh->sBoneBatches.pnBatchOffset[i32Batch+1] - pMesh->sBoneBatches.pnBatchOffset[i32Batch];
else
i32Tris = pMesh->nNumFaces - pMesh->sBoneBatches.pnBatchOffset[i32Batch];
// Indexed Triangle list
if(pMesh->nNumStrips == 0)
{
glDrawElements(GL_TRIANGLES, i32Tris * 3, GL_UNSIGNED_SHORT, &((unsigned short*)0)[3 * pMesh->sBoneBatches.pnBatchOffset[i32Batch]]);
}
else // Indexed Triangle strips
{
int i32TrisDrawn = 0;
while(i32TrisDrawn < i32Tris)
{
glDrawElements(GL_TRIANGLE_STRIP, pMesh->pnStripLength[i32Strip]+2, GL_UNSIGNED_SHORT, &((GLshort*)0)[i32Offset]);
i32Offset += pMesh->pnStripLength[i32Strip]+2;
i32TrisDrawn += pMesh->pnStripLength[i32Strip];
++i32Strip;
}
}
}
if(!pMesh->sBoneBatches.nBatchCnt)
{
glDrawElements(GL_TRIANGLES, pMesh->nNumFaces*3, GL_UNSIGNED_SHORT, 0);
}
if(bSkinning)
{
glDisableClientState(GL_MATRIX_INDEX_ARRAY_OES);
glDisableClientState(GL_WEIGHT_ARRAY_OES);
// We are finished with the matrix pallete so disable it.
glDisable(GL_MATRIX_PALETTE_OES);
}
else
{
//Reset the modelview matrix back to what it was before we transformed by the mesh node.
glPopMatrix();
}
}
// Disable States
glDisableClientState(GL_VERTEX_ARRAY);
glDisableClientState(GL_NORMAL_ARRAY);
glDisableClientState(GL_TEXTURE_COORD_ARRAY);
// unbind the vertex buffers as we don't need them bound anymore
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
}
注意しなければならないのは、Android版の場合m_Extensionというクラス(OpenGL Extensionのメソッドをロードするクラス)がエクテンションの関数をロードできない(NDKには、Extensionの関数をロードする関数が実装されてない)ので、エクステンションの関数はOpenGLのextensionの関数そのまま使うようにしなければならないことです。
ただしここでも罠があって、そのままの状態だとNDKのOpenGL ESの関数定義には、matrix_palleteのExtensionが使える関数定義がされないでincludeされるので、glext.hがインクルードされる前にdefineで、次の定義をしておきます。
それがGL_GLEXT_PROTOTYPESで、
#define GL_GLEXT_PROTOTYPES
と、おまじない程度に書いておきましょう。
ここでは、テクスチャの部分も変えないと「pvr」という特殊な画像形式を読むようになっているのですが、そこはlibpngなどを使って、読み込み部分を書いたりするのでしょうが、検索すれば、NDKを使った読み込みの仕方が書いてあるサイトもあるので、そこを参照してください。また、もう一つ頂点をロードしている部分も書き換えます。次のように書き換えます。ここもiPhoneと一緒のコードです。
bool OGLESIntroducingPOD::LoadVbos(CPVRTString* pErrorStr)
{
if(m_Scene.nNumMesh == 0) // If there are no VBO to create return
return true;
if(!m_puiVbo)
m_puiVbo = new GLuint[m_Scene.nNumMesh];
if(!m_puiIndexVbo)
m_puiIndexVbo = new GLuint[m_Scene.nNumMesh];
glGenBuffers(m_Scene.nNumMesh, m_puiVbo);
for(unsigned int i = 0; i < m_Scene.nNumMesh; ++i)
{
SPODMesh& Mesh = m_Scene.pMesh[i];
unsigned int uiSize = Mesh.nNumVertex * Mesh.sVertex.nStride;
glBindBuffer(GL_ARRAY_BUFFER, m_puiVbo[i]);
glBufferData(GL_ARRAY_BUFFER, uiSize, Mesh.pInterleaved, GL_STATIC_DRAW);
m_puiIndexVbo[i] = 0;
if(Mesh.sFaces.pData)
{
glGenBuffers(1, &m_puiIndexVbo[i]);
uiSize = PVRTModelPODCountIndices(Mesh) * sizeof(GLshort);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_puiIndexVbo[i]);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, uiSize, Mesh.sFaces.pData, GL_STATIC_DRAW);
}
}
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
return true;
}
ライブラリ使用のサンプル
実際にライブラリを使用してサンプルを作ってみました。データ的には、秒間約50万ポリゴンぐらいですが、秒間100万ポリゴンぐらいでも大丈夫なようです。この数値がどのくらいかというと、PSPぐらいの性能というとわかりやすいんでしょうか。
サンプル動画
サンプル画像
![]() |
![]() |
![]() | ![]() |
![]() |




























