iphone

スマートフォンでお手軽3D

カテゴリ
ブックマーク数
このエントリーを含むはてなブックマーク はてなブックマーク - スマートフォンでお手軽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オブジェクトとボーン(関節のような頂点を変形するもの)の関係は下の図のようになっています。

bone

この図をみると、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ぐらいの性能というとわかりやすいんでしょうか。


サンプル動画




サンプル画像


写真 3 写真 2
写真 1 写真
device


猫でもわかるiPhoneで画像にフィルターをかける方法

カテゴリ
ブックマーク数
このエントリーを含むはてなブックマーク はてなブックマーク - 猫でもわかるiPhoneで画像にフィルターをかける方法
このエントリーをはてなブックマークに追加
こんにちは!
ノンアルコールビールはビールと認めない派、gaoohです。

今回はiPhoneで簡単に画像にフィルターをかける方法を紹介します。 いわゆるカメラアプリによく搭載されている、撮った写真を白黒にしたりセピアにしたりする例のアレです。

基礎知識


まずは、そもそも画像を変化させるというのはどういうこと?を理解しなければなりません。
このあたりはデザイナーの人のほうが詳しかったりするので、もよりのデザイナーに聞くのが一番ですが、ざっくり説明すると色は 赤(A) 、緑(G)、青(B)、で表現することができます。
いわゆるRGB値というもので、HTMLやCSSなどでよくみかけるおなじみのものですね。
これに透過を表すアルファチャンネル(A)を加えてRGBAというものもあります。

さらに色相(hun)、彩度 (saturation)、明度(Value)、による色の表し方をHSVといいます。
さらにさらに、輝度(Y)、輝度と青色成分の差(U)、輝度と赤色成分の差(V)、による色を表す形式としてYUVというものがあります。 これらは表現方法の違いなので、RGBからHSVの値を求めたり、YUVからRGBの値を求めたりもできます。

もうやたら略語がでてきておなかいっぱいですが、ようは風景写真だろうが、子供の写真であろうが、結局のところ1枚の画像は1pxずつRGB値が存在しているわけで、それを一つずつ変化させてあげれば、白黒だったりセピアだったりに変化するわけです。

画像を白黒にしてみる


そもそも画像を白黒にするということは、理論上、RGBのそれぞれの値に差をなくして、明るさの差だけにしてしまえばいいということになります。
明るさである、輝度 y は以下の式で計算できます。

y = (77 * r + 28 * g + 151 * b) / 256;
このへんは公式なのでそういうものと理解してしまうのが楽です。 あとは計算結果の輝度yをRGBそれぞれにあてはめるだけです。具体的にコードになると以下のようになります。

+ (UIImage*) grayscale:(UIImage*) anImage {
    CGImageRef  imageRef;
    cgImage = anImage.CGImage;
    
    size_t width  = CGImageGetWidth(imageRef);
    size_t height = CGImageGetHeight(imageRef);

    // ピクセルを構成するRGB各要素が何ビットで構成されている
    size_t                  bitsPerComponent;
    bitsPerComponent = CGImageGetBitsPerComponent(imageRef);
    
    // ピクセル全体は何ビットで構成されているか
    size_t                  bitsPerPixel;
    bitsPerPixel = CGImageGetBitsPerPixel(imageRef);
    
    // 画像の横1ライン分のデータが、何バイトで構成されているか
    size_t                  bytesPerRow;
    bytesPerRow = CGImageGetBytesPerRow(imageRef);
    
    // 画像の色空間
    CGColorSpaceRef         colorSpace;
    colorSpace = CGImageGetColorSpace(imageRef);
    
    // 画像のBitmap情報
    CGBitmapInfo            bitmapInfo;
    bitmapInfo = CGImageGetBitmapInfo(imageRef);
    
    // 画像がピクセル間の補完をしているか
    bool                    shouldInterpolate;
    shouldInterpolate = CGImageGetShouldInterpolate(imageRef);
    
  // 表示装置によって補正をしているか
    CGColorRenderingIntent  intent;
    intent = CGImageGetRenderingIntent(imageRef);
    
    // 画像のデータプロバイダを取得する
    CGDataProviderRef   dataProvider;
    dataProvider = CGImageGetDataProvider(imageRef);
    
    // データプロバイダから画像のbitmap生データ取得
    CFDataRef   data;
    UInt8*      buffer;
    data = CGDataProviderCopyData(dataProvider);
    buffer = (UInt8*)CFDataGetBytePtr(data);
    
    // 1ピクセルずつ画像を処理
    NSUInteger  x, y;
    for (y = 0; y < height; y++) {
        for (x = 0; x < width; x++) {
            UInt8*  tmp;
            tmp = buffer + y * bytesPerRow + x * 4; // RGBAの4つ値をもっているので、1ピクセルごとに*4してずらす
            
            // RGB値を取得
            UInt8 red,green,blue;
            red = *(tmp + 0);
            green = *(tmp + 1);
            blue = *(tmp + 2);
            
      // 輝度計算
            UInt8 brightness;
            brightness = (77 * red + 28 * green + 151 * blue) / 256;
            
            *(tmp + 0) = brightness;
            *(tmp + 1) = brightness;
            *(tmp + 2) = brightness;
        }
    }
    
    // 効果を与えたデータ生成
    CFDataRef   effectedData;
    effectedData = CFDataCreate(NULL, buffer, CFDataGetLength(data));
    
    // 効果を与えたデータプロバイダを生成
    CGDataProviderRef   effectedDataProvider;
    effectedDataProvider = CGDataProviderCreateWithCFData(effectedData);
    
    // 画像を生成
    CGImageRef  effectedCgImage;
    UIImage*    effectedImage;
    effectedCgImage = CGImageCreate(
									width, height, 
									bitsPerComponent, bitsPerPixel, bytesPerRow, 
									colorSpace, bitmapInfo, effectedDataProvider, 
									NULL, shouldInterpolate, intent);
    effectedImage = [[UIImage alloc] initWithCGImage:effectedCgImage];
    
    // データの解放
    CGImageRelease(effectedCgImage);
    CFRelease(effectedDataProvider);
    CFRelease(effectedData);
    CFRelease(data);
	
    return effectedImage;

画像をセピアにしてみる


次はセピアです。セピアって元々イカ墨を原料にした顔料の名前らしいですね。個人的にはイカスミスパゲッティなんかよりイカ腸焼きが好きです。

セピアもモノクロと実はあまり変わっていなくて、セピアという色と、あとは色の明るさに変換してあげればいいだけです。
セピアの色というのはきちんと定義されていてRGBでいうと R = 107, G = 74 B = 43 という値です。
Rを100%とするとGは約70%、Bは約40%なのでその割合を保持していればセピア調になっちゃうわけですね。

コードはほぼ白黒と同じで画像処理の部分だけ以下のようにしてあげればOKです。

    NSUInteger  x, y;
    for (y = 0; y < height; y++) {
        for (x = 0; x < width; x++) {
            UInt8*  tmp;
            tmp = buffer + y * bytesPerRow + x * 4; // RGBAの4つ値をもっているので、1ピクセルごとに*4してずらす
            
            // RGB値を取得
            UInt8 red,green,blue;
            red = *(tmp + 0);
            green = *(tmp + 1);
            blue = *(tmp + 2);

            // 輝度計算
            UInt8 brightness;
            brightness = (77 * red + 28 * green + 151 * blue) / 256;
            
            *(tmp + 0) = brightness;
            *(tmp + 1) = brightness * 0.7;
            *(tmp + 2) = brightness * 0.4;
        }
    }

もっと高度な画像変換


はい、ここまでできると「画像をトイカメラ風にしたい!」とか「モザイクかけたい!」とかいろいろと欲がでてくるわけです。

基本的に考え方はモノクロやセピアと同じように1pxずつそれにあった計算式があるので、変換していけばいいのですが……高度な処理になればなるほど当然計算はややこしくなります。それはそれで必要なのですが、場合によってはもうちょっと楽をしたい、そんなときにはImageMagickを使うという手があります。

Webサービスなどでは画像のリサイズなんかでけっこう当たり前につかっているImageMagick。かくいう私もiPhoneアプリを作り始める前は頼りっきりで、中で何をしているかなんてまったく気にせず使ってました。これをiPhone上でも使える用にコンパイルしたものがあり、それを使うとお手軽に変換できます。

http://www.cloudgoessocial.net/2010/02/10/imagemagick-for-iphone-via-snowleopard/

導入方法


自分でコンパイルすることも可能ですが、配布元でサンプルプロジェクトが用意されており、そこからまるっとコピーしてとりあえず使ってみることもできます。

まず、ライブラリであるIMディレクトリをコピーし、Frameworks以下に既存ファイルの追加でプロジェクトに組み込みます。

2011-02-22_1148

次にコンパイル時のリンカフラグに以下のものを追加します。

-lMagickCore -lMagickWand -lbz2 -lz -ljpeg -lpng

2011-02-22_1140

最後に「ヘッダ検索パス」と「ライブラリ検索パス」に "$(SRCROOT)/" を追加します。「再帰的」にチェックも入れてください。

2011-02-22_1153

使い方はサンプルプロジェクトを参考にするのが一番はやいです。 サンプルプロジェクトには一部の使い方しか載っていませんが、当然いろいろできます。

月夜風


 // factor 偏移係数。1.5が標準。
 MagickBooleanType MagickBlueShiftImage(
     MagickWand* wand,
     const double factor
 )

Before:
Photo 2月 22, 12 21 38
After:
Photo 2月 22, 12 17 13

油絵風


 // radius 半径(単位:ピクセル数)。0を指定すると適切な半径を選択します。 
MagickBooleanType MagickOilPaintImage(
    MagickWand* wand,
    const double radius
)


Before:
Photo 2月 22, 12 21 38
After:
Photo 2月 22, 12 16 28

スケッチ風


// radius ガウス演算用の中心を含まない半径(単位:ピクセル数)。0を指定すると適切な半径を選択します。
// sigma ガウス演算用の標準偏差(単位:ピクセル数)。
// angle モーションブラーの角度。
MagickBooleanType MagickSketchImage(
    MagickWand* wand,
    const double radius,
    const double sigma,
    const double angle
)

Before:
Photo 2月 22, 12 21 38
After:
Photo 2月 22, 12 14 54

細かい調整値はほぼデフォルトを使っているので、その調整次第でいろいろ変化しますので試してみてください。

トイカメラ風


ImageMagick応用編です。さすがにトイカメラ風だと直で変換できるような関数はないので、既存のものを、いろいろと組み合わせます。

トイカメラ風の画像にするにはいろいろ手法はありますが、とりあえずコントラストをあげて、彩度をあげます。

コード的には以下のようになります。
※エラー処理や細かいことなどは考慮していないので、そのへんは適時がんばって下さい。

CGImageRef createStandardImage(CGImageRef image) {
	const size_t width = CGImageGetWidth(image);
	const size_t height = CGImageGetHeight(image);
	CGColorSpaceRef space = CGColorSpaceCreateDeviceRGB();
	CGContextRef ctx = CGBitmapContextCreate(NULL, width, height, 8, 4*width, space,
											 kCGBitmapByteOrder32Big | kCGImageAlphaPremultipliedFirst);
	CGColorSpaceRelease(space);
	CGContextDrawImage(ctx, CGRectMake(0, 0, width, height), image);
	CGImageRef dstImage = CGBitmapContextCreateImage(ctx);
	CGContextRelease(ctx);
	return dstImage;
}

+ (UIImage*) imagemagick:(UIImage*) anImage {
    CGImageRef  imageRef;
    imageRef = anImage.CGImage;
	
    size_t width = CGImageGetWidth(imageRef);
    size_t height = CGImageGetHeight(imageRef);
 
    // 画像データをNSDataに変換
    const char *map = "ARGB"; // カラーモード
    const StorageType inputStorage = CharPixel;
    CGImageRef standardized = createStandardImage(imageRef);
   
    NSData *srcData = (NSData *) CGDataProviderCopyData(CGImageGetDataProvider(standardized));
    CGImageRelease(standardized);
    const void *bytes = [srcData bytes];

    MagickWandGenesis();
    MagickWand *magick_wand_local= NewMagickWand();

    // 画像NSDataの情報をmagick_wand_localに挿入
    MagickConstituteImage(magick_wand_local, width, height, map, inputStorage, bytes);

  // コントラストを上げる
    MagickContrastImage(magick_wand_local, true);
    MagickContrastImage(magick_wand_local, true);
    MagickContrastImage(magick_wand_local, true);
    MagickContrastImage(magick_wand_local, true);
    // 明度、彩度、色相をそれぞれ現在を100とした場合として調整
    MagickModulateImage(magick_wand_local, 100, 250, 100);

    // magick_wand_localを元に画像を生成
    const int bitmapBytesPerRow = (width * strlen(map));
    const int bitmapByteCount = (bitmapBytesPerRow * height);
    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
    char *trgt_image = malloc(bitmapByteCount);
    MagickExportImagePixels(magick_wand_local, 0, 0, width, height, map, CharPixel, trgt_image);
    magick_wand_local = DestroyMagickWand(magick_wand_local);
    MagickWandTerminus();
    
    CGContextRef context = CGBitmapContextCreate (trgt_image,
												  width,
												  height,
												  8, // bits per component
												  bitmapBytesPerRow,
												  colorSpace,
												  kCGImageAlphaPremultipliedFirst);
    CGColorSpaceRelease(colorSpace);
    CGImageRef cgimage = CGBitmapContextCreateImage(context);
    UIImage *image = [[UIImage alloc] initWithCGImage:cgimage];
    CGImageRelease(cgimage);
    CGContextRelease(context);
    [srcData release];
    free(trgt_image);
    return image;
} 

Before:
Photo 2月 22, 12 21 38
After1:
Photo 2月 22, 12 45 37

ちょっとそれっぽくなりましたね。なんか三毛部分が強化された気がしますが。

よりらしくするために画像の四隅をちょっと暗くしてみます。
コントラストと再度の調整の後に以下のように付け加えます。

 // コントラストを上げる
    MagickContrastImage(magick_wand_local, true);
    MagickContrastImage(magick_wand_local, true);
    MagickContrastImage(magick_wand_local, true);
    MagickContrastImage(magick_wand_local, true);
    // 明度、再度、色相をそれぞれ現在を100とした場合として調整
    MagickModulateImage(magick_wand_local, 100, 250, 100);

    PixelWand *color = NewPixelWand();
    PixelSetColor(color, "transparent");

    // 画像と同じ大きさで黒い楕円形を描く
    MagickWand * black_frame_wand_local= NewMagickWand();
    MagickNewImage(black_frame_wand_local, width, height, color);
    DrawingWand *draw_wand = NewDrawingWand();
    DrawEllipse(draw_wand, width/2, height/2, width/2-10, height/2-10, 0, 360);
    MagickDrawImage(black_frame_wand_local, draw_wand);
    
    // 黒い楕円形画像にぼかしを入れる
    MagickBlurImageChannel(black_frame_wand_local, AllChannels, 0, 30);

    // 元画像と合成して、黒い楕円形画像をmaskとした画像を生成
  MagickCompositeImage(black_frame_wand_local, magick_wand_local, InCompositeOp, 0, 0);

    PixelWand *pw1 = NewPixelWand();
    PixelSetColor(pw1, "#00ff00");
    PixelWand *pw2 = NewPixelWand();
    PixelSetColor(pw2, "#ff0000");
    MagickTintImage(magick_wand_local, pw1, pw2);
	
    MagickCompositeImage(magick_wand_local, black_frame_wand_local, OverCompositeOp, 0, 0);

After2:
Photo 2月 22, 14 17 43
こんな感じです。とりあえずやってみた感じなので、パラメータの調整などで、よりそれっぽくなると思います。

パフォーマンス


とてもお手軽ですが、iPhone上で使うとなるとやはりパフォーマンスが気になります。以下が一番シンプルに同じ画像をセピア化した場合の処理時間です。

※テストに使った画像は600x800のものです。

シュミレーターでの値

imagemagickのMagickSepiaToneImageを利用
0.293929 sec
imagemagickを利用せず単純計算
0.003878 sec

iPhone3Gでの値

imagemagickのMagickSepiaToneImageを利用
1.975130 sec
imagemagickを利用せず単純計算
0.191766 sec

iPhone4での値

imagemagickのMagickSepiaToneImageを利用
0.412554 sec
imagemagickを利用せず単純計算
0.008401 sec

端末の状態や画像によっても値は上下するので参考程度ですが、単純な変換だとやはり生の計算にはかなわない部分はあります。
ただユーザに時間がかかっていると感じさせないUIでカバーするとか、方法はいろいろあるかなと思います。

なお、ライブラリをごそっと追加すると最終的なバイナリサイズで 5、6 M が増えることになります。iPhoneアプリは 20Mを超えると3G回線でのダウンロードはできなくなり、wifiやPC経由でのインストールしかできなくなるのでその点も気をつけたいところです。

ではでは駆け足で画像変換について説明しましたが、カメラアプリはiPhoneアプリの中でも大激戦地区。だからこそみなさんいろいろ工夫している分野なので、だいぶ奥が深い!だからこそいろいろおもしろいので、ちょっと週末あそんでみると楽しいですニャ!

社内のスマフォ開発用ライブラリの管理方法を公開しちゃいます!

カテゴリ
ブックマーク数
このエントリーを含むはてなブックマーク はてなブックマーク - 社内のスマフォ開発用ライブラリの管理方法を公開しちゃいます!
このエントリーをはてなブックマークに追加
こんにちは!こんにちは!
最近JavaやObjective-Cで開発をしていて、やっぱりPerlって使いやすい言語なんだなぁと改めて感じている栗原です。
#と言いつつもJavaもObjective-Cも好きだったりします。

今回は「スマフォのライブラリなどの管理をどげんかせんといかん」とCTOに言われたために考えた、弊社のスマフォ開発チームが行なっているAndroid、iPhoneそれぞれの社内用ライブラリやスニペットの管理方法についてご紹介したいと思います。

ソースの管理方法

まずソースの管理についてですが、現在弊社のスマートフォン系のアプリはGitを使って管理しています。
通常のアプリも含めて「android」と「iphone」というプロジェクトを作成し、その中でそれぞれのプラットフォーム毎にリポジトリを作成しています。
以下のようなイメージですね。

git://example.com/iphone/app01.git
git://example.com/iphone/app02.git
git://example.com/android/app01.git
git://example.com/android/app02.git
共通ライブラリに関しても、アプリと同レベルの階層にAndroidであれば「common」、iPhoneであれば「ldtouch」という名前でリポジトリを作成し、その中にライブラリやスニペットをつっこんでいます。(なぜ名前が違うかは気にしないでくださいw)

git://example.com/iphone/ldtouch.git
git://example.com/android/common.git

ガイドライン

リポジトリを作ったら、その中になんでもつっこめばいいってもんじゃないですよね。
のちのち「ほげほげするライブラリがあった気がするけどどこだっけな…」とか「このライブラリちゃんと動くんかな」とか「このライブラリどう使うかわかんねーYO、まぁソース読みゃーいいけどさ」とかなるのは目に見えています。

そこで現在スマフォ開発チームでは、共通ライブラリやスニペットを管理する上で以下のようなガイドラインを設けています。

  • なんでもできる1つの大きな便利ライブラリは作らない
  • ライブラリを作成するときはユニットテスト必須(作れないものナシでもOK)
  • ライブラリにもならないようなものは簡単にコピペして使えるようにスニペットとして管理する
  • ドキュメントを書く(JavaDoc形式)
  • 可能な限り(常識の範囲内で)下位互換性を保つ(@Deprecatedを付けるとか)

プラットフォーム別の管理方法


Androidのライブラリ管理


まずAndroidについてですが、common/以下のディレクトリ構成は下記のようになっています。

common/
+ android-common-lib01/
    + AndroidManifest.xml
    + res/
    + src/
    + pom.xml
+ android-common-test-lib01/
    + AndroidManifest.xml
    + res/
    + src/
    + pom.xml
+ android-common-lib02/
    + AndroidManifest.xml
    + res/
    + src/
    + pom.xml
+ android-common-test-lib02/
    + AndroidManifest.xml
    + res/
    + src/
    + pom.xml
+ android-common-snippet01/
    + AndroidManifest.xml
    + res/
    + src/
    + pom.xml
+ pom.xml
ライブラリは「android-common-ライブラリ名」というフォーマットでディレクトリを掘っていくと同時に、そのライブラリのテストも「android-common-test-ライブラリ名」というフォーマットでディレクトリを掘っています。

各ディレクトリの中には、そのライブラリとそのライブラリを使ったサンプルプログラムが含まれており、どのように使うのかがすぐにわかるようになっています。

上図をよく見るとそれぞれのディレクトリにpom.xmlというファイルがあります。これはビルド管理ソフトウェアであるMavenの設定ファイルになっています。各ライブラリのビルド管理はこのMavenを使用して行なっており、ビルドフェーズの中でユニットテストもできるような形になっています。なお、トップ階層にもpom.xmlがありますが、これはライブラリ全体でも個別でもビルド管理をできるようにしているからです。

また、スニペットに関してもライブラリと同じ位置付けで「android-common-スニペット名」みたいな形で個別のスニペットとサンプルプログラムを同梱させていたりします。

iPhoneのライブラリ管理


iPhoneのldtouch/以下の構成は下記のようになっています。

ldtouch/
+ Classes/
   + LDFooView/
      + LDFooView.h
      + LDFooView.m
   + NSArray+Bar/
   + NSData+Baz/
   + QuxAuth/
+ Externals/
+ GHUnitOSTestMain.m
+ LDTouch-Info.plist
+ LDTouch-.xcodeproj/
+ LDTouch_Prefix.pch
+ Scripts/
   + RunTest.sh
+ Tests/
   + LDFooView/
      + LDFooViewTest.m
   + NSArray+Bar/
   + NSData+Baz/
   + QuxAuth/
+ UISample/
   + Classes/
iPhoneのライブラリですが、現状フレームワーク(foo.framework)のような形は取っていません。基本的にはldtouch/Classes/にあるライブラリのファイル(foo.mとfoo.h)を自分のプロジェクトにコピーして使うようになっています。

この方法はのちのちライブラリがバージョンアップした際に追従できないなど、いろいろと問題もありますが、現状ライブラリとしてまとめられるほどの数もないため、一旦はこの形式を取っています。この管理方法については今後変わっていく可能性があると考えています。

テストに関しては、ライブラリと同じ名前でldtouch/Tests/の中に入れています。テストフレームワークはGHUnitというものを使っています。GHUnitを使うと以下のようなGUIを使ったテストができます。なんかカッコイイ!

ldtouch_ghunit


また、UISampleというアプリも同梱されています。このアプリはView系のライブラリのサンプルアプリになっていて、そのライブラリを使うとどういう動きが作れるのかを見ることができるサンプルアプリになっています。起動すると以下のような感じになっています。
これを見ることによってぐっとそのライブラリの動作がイメージしやすくなりますね!

uisample

ということで、現在スマフォ開発チームで進めている社内用ライブラリをどのように管理しているかのご紹介しましたがいかがだったでしょうか。
今回ご紹介した管理方法については、このようにライブラリをまとめること自体始めたばかりですし完璧なものではないと考えていて、今後もブラッシュアップしていこうと考えていたり、有用なライブラリができたら公開等も考えていますので、ぜひ楽しみにしていてください。



あ、あと余談ですが、最近弊社スマフォ開発グループではFacebookのグループで日報を書くのが流行っています(他のグループでもやっているらしい)。

「普段あまり話さないあの人はこんなこと考えてたのか」とか「この人が紹介しているこのライブラリ、あまり興味なかったけど確かに面白いな」とか、いつも自分の頭の中だけで考えていることだったり、やっているプロジェクトには直接関連しないから話さないけど仕事自体には関連する事柄や意見などを共有しようとしています。

普段面と向かって話す時間はあまりないけど、こんなこと思ってるんだ考えてるんだというのを共有できたりするとコミュニケーションの幅が広がりそうですね!興味のある方はやってみては如何でしょうか。

facebook_group


ということで、今回はこの辺で。
ではでは。

夏休み自由研究 - iPhoneでいきもの図鑑を作る

カテゴリ
ブックマーク数
このエントリーを含むはてなブックマーク はてなブックマーク - 夏休み自由研究 - iPhoneでいきもの図鑑を作る
このエントリーをはてなブックマークに追加
おつかれサマーです。開発部モバイルGの井上です。ケータイライブドアの各サービス、iPhoneアプリ開発等を担当しています。

さて、夏休みもあと数日になりました。全国の小学生の皆さんは、夏の宿題に追われている頃ではないでしょうか。 え、まだ手をつけてない? 大丈夫、たいていそんなものです。これからが勝負ですね。

たとえば昆虫採集なんか、比較的お手軽でよいんじゃないでしょうか。近くの公園で虫を捕まえて、お中元の箱かなんかに並べて学校に持っていけばオッケー。 ただ、研究テーマとしては古典的すぎて今さらという感じですね。虫をピンで刺すのもかわいそうだし。

そこで今回は、iPhoneアプリでいきもの図鑑を作ってみます。エコかつIT、スマートですね。 先生もびっくり、クラスのあの子にも超アピールです。

iPhoneアプリは、作り込みをはじめると時間がかかりますが、簡単なアプリなら工作感覚で手軽に組むことも可能です。 カメラやGPSも、最小限のコードで利用できるAPIが用意されています。 カメラでいきものの写真を撮ると、位置情報つきで一覧できて、地図にプロットできる、というライブな図鑑アプリを作ってみます。

プログラムなんかできない? 問題ナッシングです。英語がちょっと読めればプログラムもだいたい読めます。

それでは始めてみましょう。


■必要なもの


○開発機
Mac(9.4万円〜)
ノートでもデスクトップでも可。

○開発ソフト
XCode(無料)
アップルのサイトからDLできます。UIデザインをするInterfaceBuilderと、動作確認用のiPhoneシミュレータも同梱されています。

○できれば用意したいもの
iPhone か iPod Touch(2.5万円〜)
iPhone Developer Program($99)
シミュレータである程度代用できます。本気で開発するときにはお父さんに貸してもらいましょう。

MacでXCode、InterfaceBuilder、iPhoneシミュレータを立ち上げ、実機(ある場合)を接続したら、準備は完了です。


■画面図を描く


まずアプリ画面の全体図をかんたんに描いてみます。チラシの裏でオッケーです。 ボタンやバーの配置、ナビゲーションなどを、途中で迷わないように決めておきます。

02_01

今回は、いきものの写真を撮り、マップに表示させるということで、大きく画面は2つです。 下にタブバーを置いて、カメラとマップをそれぞれ左右のタブにあてました。 写真を撮るときには、カメラビューの右上のボタンを押すと、カメラユニットが立ち上がります。

全体の画面が決まったら、Macを立ち上げて製作に入ります。


■ビューをデザインする


Xcodeで新規プロジェクトを作ってアプリを作っていきます。 メニューから、ファイル > 新規プロジェクト > iPhone OS > Tab Bar Application を選択すると、タブバーを使ったアプリケーションのスケルトンが作られます。

03_01

項目が山ほどあってイヤンな感じですが、とりあえず左カラムのResourcesとClassesのフォルダに注目です。

Resourcesの中に、FirestView.xib・SecondView.xibというファイルがあります。それぞれ、タブで切り替わる2つの画面デザインを定義するファイルです。InterfaceBuilderで編集します。 Classesの中にあるのが、動作のロジックを記述していくプログラムファイルです。

プログラムの処理は画面単位でまとめます。FirstViewでの処理は、FirstViewController.mに書きます。同名の.hというファイルがありますが、これはヘッダファイルといって、変数やメソッドの定義を書くところです。

このスケルトンはもうアプリとして動きます。「ビルドと実行」という緑のボタンを押してみてください。 シミュレータが立ち上がり、アプリが起動します。タブバーで2つの画面を切り替えられます。

03_02

このひな形をもとに、ビューのデザインやプログラムコードを追加して、アプリを作っていきます。

まず、FirstViewとSecondViewにカメラのボタンと地図を配置してみましょう。

FirstView.xibをダブルクリックすると、InterfaceBuilderで画面のプレビューがでてきます。 このプレビューにバーやボタン、画像や地図などを配置することで、画面をデザインすることができます。

さまざまなパーツが並んだ LibraryペインからUIをドラッグ&ドロップします。レゴの組み立ての感覚ですね。 パーツの色・サイズ・アイコンの種類などは、View Attributesペインで設定できます。

04_01
04_02

続いてSecondViewにはバーとマップを配置します。

04_03

マップと位置情報を使うためには、追加の設定が必要です。 Xcode左カラムのFrameworkフォルダをCtrl+クリックして、「追加 > 既存のフレームワーク…」から MapKit.frameworkとCoreLocation.frameworkを選んでください。

1回保存して、緑のボタンでビルドしてみましょう。

04_04 04_05

ボタンやマップが出てきます。 この状態でもう、マップをスクロールしたり拡大することもできます。

1行もコードを書いてませんが、それらしくなってきましたね。


ロジックを書く


いよいよプログラミングです。 何をしたらどうなるかというロジックの部分を、XCodeの中に書いていきます。

今回必要なプログラムの処理は4つです。意外と少ないですね。

ファーストビューの処理
・カメラボタンを押したとき → カメラが立ち上がって写真が撮れる
・写真が撮れたとき → 写真の画像データ、および位置情報・タイトル・日付のデータを保存する
・データがあるとき → テーブルに画像とタイトルの一覧を表示する

セカンドビューの処理
・データがあるとき → マップに画像と位置を示すピンを表示する

それぞれ数十行程度の簡単なプログラムです。 順番に見ていきます。

まずカメラボタンを押したときのコードです。分からなくてもいいので、ざっと見てみてください。

- (IBAction)pushedCameraButton {
	
	self.imagePickerController = [[[UIImagePickerController alloc] init] autorelease];
	self.imagePickerController.delegate = self;
    self.imagePickerController.sourceType = UIImagePickerControllerSourceTypeCamera;
	self.imagePickerController.showsCameraControls = YES;
	[self presentModalViewController:self.imagePickerController animated:YES];
}

- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info {
	
    UIImage *takenPhoto = [info valueForKey:UIImagePickerControllerOriginalImage];
	[self dismissModalViewControllerAnimated:YES];
	[self performSelector:@selector(showCameraInputView:) withObject:takenPhoto afterDelay:1.0];
}

プッシュカメラボタンとあって、そのあとカッコでくくられてますね。 ボタンを押したら、カッコの中の処理が走るということです。 イメージピッカーというのがiPhoneでのカメラ機能を受け持つAPIです。 いくつか設定をして、最後の presentModalViewController で画面に表示させています。カメラの画面がにゅっと下から出てくることになります。

ふたつめのカッコで、シャッターが押されたときの画像が返ってきます。takenPhoto というのが画像データを表します。

ここでは、タイトルを入力するフォームに takenPhoto を渡しています。フォーム上でタイトルなどを入れて、保存ボタンを押すと、データが一式保存されます。

次に保存するところのコードです。

タイムスタンプ + 乱数のidを決めて、その名前で写真のpngファイルを保存、さらにid・日時・位置情報・タイトル・ノートをディクショナリーと呼ばれるデータホルダーの中に突っ込んで、それを保存しています。ちょっと長いですが、だいたいの流れが分かればオッケーです。

- (IBAction)save {
	
	NSMutableDictionary *dictionary = [NSMutableDictionary dictionary];
	NSDate *date = [NSDate date];

	// ID
	srand(time(nil));
	int num = rand() % 100;
	int sec = (double)[date timeIntervalSince1970];
	NSString *anId = [NSString stringWithFormat:@"%d%d", sec, num];
	[dictionary setObject:anId forKey:@"id"] ;
	
	// 位置情報
	CLLocation *location = [locationManager location];
	if (!location) return;
	CLLocationCoordinate2D coordinate = [location coordinate];
	[dictionary setObject:[NSNumber numberWithDouble:coordinate.latitude] forKey:@"latitude"];
	[dictionary setObject:[NSNumber numberWithDouble:coordinate.longitude] forKey:@"longitude"];
	
	// 日付
	[dictionary setObject:date forKey:@"date"];
	
	// タイトルとノート
	[dictionary setObject:titleTextField.text forKey:@"title"];
	[dictionary setObject:noteTextView.text forKey:@"note"];
	
	IkimonoAppDelegate *appDelegate = (IkimonoAppDelegate *)[[UIApplication sharedApplication] delegate];
	
	// 画像の保存
	NSData *pngPhoto = UIImagePNGRepresentation(photo);
	[pngPhoto writeToFile:[NSString stringWithFormat:@"%@/photo/%@@2x.png", [appDelegate documentPath], anId] atomically:NO];
	
	NSData *pngTumbnail = UIImagePNGRepresentation(thumbnail);
	[pngTumbnail writeToFile:[NSString stringWithFormat:@"%@/thumbnail/%@@2x.png", [appDelegate documentPath], anId] atomically:NO];
	
	// Dictionaryの追加
	[appDelegate.ikimonoArray addObject:dictionary];
	[appDelegate saveArray];
	
	[self dismissModalViewControllerAnimated:YES];
}

データが保存できればあとは表示するだけですね。マップへのピンの表示は、アノテーションというのを作って、そこに先ほどのディクショナリーからタイトルや位置情報をセットしてやって、マップに渡してやります。

- (void)addAnnotation {
	NSDictionary *dictionary = [appDelegate.ikimonoArray objectAtIndex:i];
	Annotation *annotation = [[Annotation alloc] init];
	annotation.latitude = [dictionary objectForKey:@"latitude"];
	annotation.longitude = [dictionary objectForKey:@"longitude"];
	annotation.title = [dictionary objectForKey:@"title"];

	NSDateFormatter *outputFormatter = [[NSDateFormatter alloc] init];
	[outputFormatter setDateFormat:@"MM/dd HH:mm"];
	annotation.subtitle = [outputFormatter stringFromDate:[dictionary objectForKey:@"date"]];
	[self.mapAnnotations addObject:annotation];
	[self.mapView addAnnotation:annotation];	
}

という感じで、必要なプログラムを追加していきます。

05_01

処理が書けたら、InterfaceBuilderで配置したパーツと、プログラムを結びつけます。 InterfaceBuilderのFile's OwnerをCtrl+クリックすると、先ほどのpushedCameraButtonがでてくるので、それをカメラのボタンにドラッグしてやります。

これでデザインとロジックが結合したことになります。 こんな感じでInterfaceBuilderとXCodeを行ったりきたりしていると、アプリがだんだん出来あがってきます。

さて、最後に仕上げです。 アプリ名やアイコン画像を設定します。アイコンはきれいなのを作りたいものです。 絵のうまい友だちに描いてもらうといいですね。

自分はあまりうまくないです。

05_02

完成!!!


いきものを撮影


では、近所の小金井公園にいきものを探しに行ってみます。 小金井公園は79万平米、東京都で2番目に広い公園です。園内には広い芝生や雑木林、池などもあります。

草むらの陰にセミの抜け殻を発見!

06_01

民家の塀のアサガオ。

06_02

池にはカメ。

06_03

オケラを見つけてしまいました。こいつはレアです。

06_04

図鑑らしくなりました。 マップのタブを確認すると、見つけた場所にピンが落ちてます。

06_06 06_05

ばっちりですね!

写真を撮りまくって、自分だけのいきもの図鑑を作ってみてください。 ネットワーク機能を追加して、友だちと共有しても楽しそうです。

といういわけで、夏の自由研究で作ってみたいちょっとしたiPhoneアプリのススメでした。

iPhoneプログラミングはMac1台から始められますし、GPS・カメラ・加速度センサーといった最新のモバイル機能を手軽に利用できます。 工作スピリッツ燃える小中高生の皆さんは是非チャレンジしてみるといいと思います。もちろん、大人の自由研究というのもアリですね。

今回のサンプルコードはこちらからダウンロードできます。

iPhoneアプリを多言語対応にする

カテゴリ
ブックマーク数
このエントリーを含むはてなブックマーク はてなブックマーク - iPhoneアプリを多言語対応にする
このエントリーをはてなブックマークに追加

こんにちは、デスクの上にiPhoneを意味もなく3台並べる男、faultierです。

僕の普段の仕事はlivedoor blogやlivedoor ニュースのモバイル版のシステム開発ですが、ときにはlivedoor クリップアプリのようなiPhoneアプリの開発もやっています。ということで、今日はPerlの話ではなく、iPhoneアプリとObjective-Cの話をしようと思います。

iphone_jaiphone_ja_icon

上の画像はlivedoor クリップアプリですが、実は言語設定が「日本語」以外に設定されていると日本語部分が英語に変わります。

ピクチャ 3iphone_en_icon

MacOSXのアプリケーションを作成したことがある方はご存知かと思いますが、Cocoaアプリケーションは簡単に多言語化する仕組みが備わっています(できることは単順ですが)。

ローカライズされたリソースを用意する

まず、ローカライズされたリソースの置き場を用意します。英語と日本語に対応する場合は、en.lprojとja.lprojというディレクトリを作って、Xcode上でプロジェクトに取り込みます。他の言語にも対応する場合は、(ISO 639で定義されている2文字もしくは3文字の言語コード).lprojというディレクトリを追加していくだけです。

また、もっと細かく分類したいのであれば、en_USやen_GBなどのように、ISO 3166で定義されているコードを使用して国や地域に絞り込むことができます。

さらに、English、Japanese、French、Germanなど言語名での指定もサポートしていますが、こちらはレガシーなやり方のようなので、言語コードを使う方をお薦めします(後述するXcode上からローカライズドなリソースを作る方法では、何故かEnglishやJapaneseのようなディレクトリになりますが…)。

en.lprojとja.lprojをプロジェクトに取り込んだら、それぞれの下にInfoPlist.stringsとLocalizable.stringsというファイルを作ります。InfoPlist.stringsは、Info.plistで設定される内容(例えばアプリの表示名など)をローカライズするために使います。Localizable.stringsの方は好きに使えるローカライズドされた文字列を入れて置きます。

中身は以下のように、"key" = "localized value"の形式で記述します。あとは、プログラム側からこのキーを元に文字列を取得すれば、そのときのiPhoneの設定に応じてそれぞれの言語の文字列を使うことができます。

/*
 ja.lproj/InfoPlist.strings
 */
CFBundleDisplayName = "日本の心";
/*
 ja.lproj/Localizable.strings
 ここはコメント
 エンコーディングはUTF-16でなければならないことに注意
 */
"Hoge" = "ほげ";
/*
 en.lproj/Localizable.strings
 */
"Hoge" = "foo";

また、先にlprojディレクトリを作るのではなく、先にInfoPlist.stringsやLocalizable.stringsをプロジェクトに追加しておき、「情報を見る」から「ローカライズ可能にする」こともできます。この場合、Xcodeのプロジェクト上にはlprojグループは表示されず、stringsファイルそのものが二つに分裂したように表示されますが、内部では(言語名).lprojが作られています。

プログラム側をLocalizedStringに対応させる

こうして用意したローカライズドされた文字列をプログラム側から使うコードは、次のようになります。

NSString *localizedString = [[NSBundle mainBundle] localizedStringForKey:@"Hoge" value:@"Hoge" table:nil];

これで、「Localizable.stringsからキーがHogeのローカライズドされた文字列を取得する。現在の設定の言語専用のリソースが無いか、あってもこのキーが定義されていなければ、"Hoge"という文字列を返す」という処理です。

ただ、毎回これを書くのは「文字列を取得する」という目的のためだけにはちょっと煩雑なので、次のようなマクロが用意されています。

NSString *localizedString = NSLocalizedString(@"Hoge", @"");

2個目の引数はただのコメントなので、特に何かに使われるわけではありません。これで最初のコードと同じ文字列が得られます。元のメソッドでは、どこのバンドルから取ってくるか、どのテーブル(ファイル)から文字列を探すか、指定されたキーが見つからなかった場合に返すデフォルトの値は何か、まで指定できますが、「バンドルがmainBundle、テーブルはLocalizable.strings、指定したキーが見つからない場合はキーと同じ文字列」で良いのであれば、このマクロの方が楽ですね。

ちなみに、テーブル名を指定した場合はLocalizable.strings以外のファイルから文字列を取得することもできます。例えば、Error.stringsというファイルを作り、そこにエラーメッセージをまとめておくと、プログラムからは次のように呼び出せます。

NSString *lcalizedErrorMessage = NSLocalizedStringFromTable(@"ConnectionError", @"Error", @"");
// 次のコードでも同じ
//NSString *lcalizedErrorMessage = [[NSBundle mainBundle] localizedStringForKey:@"ConnectionError" value:@"ConnectionError"];

このようにして、アプリ中で使われる文字列を(言語名).lproj以下のstringsファイルに抜き出しておけば、あとはそのファイルを言語毎に作ってやるだけで、表示する文言やメニューのラベルなどを変える程度ではありますが、簡単に多言語対応できるようになります。

xibファイルのローカライズ

stringsファイルでローカライズする他に、xibファイルも同じ方法で各言語ごとのローカライズ版を作ることができます。プログラム側を大幅に変更せずとも、言語に合わせてUIを変えることができるようになります。

ただ、機能やデザインに関わるxibファイルを各言語分用意して管理するのは、文字列しか含まれていないstringsファイルの管理よりは手間もかかり気を使う必要もありますし、サイズも大きくなります。これを使う場合はその必要があるかどうか、慎重に検討した方が良いでしょう。

ちなみに、livedoor クリップアプリのUIは大部分をxibファイルを使わずに実装しているため、特にUI自体のローカライズはしていません。

多言語対応のメリット

iPhoneアプリは、AppStoreのおかげで思った以上に海外のiPhoneユーザにも見てもらえます。

livedoor クリップの場合、元のサービスがそもそも日本語を理解できる人でなければ使えないサービスなので、日本語のみ対応にしてもよかったのですが、リリース後に国別のダウンロード数を見たところ予想以上に海外からのダウンロードがありました(もちろん、海外在住の日本人だったり、無料のアプリなので気軽にダウンロードしてしまっただけの可能性もありますが)。

よく分からないアプリケーションでも、まったく読めない言語で書いてあるより、英語化されているだけでも少し安心してもらえるようです。

みなさんも、是非、世界中で使ってもらえるiPhoneアプリを作ってみて下さい!