BlenderのモデルをAway3Dで表示してスクショを撮るところまでのメモ

=前置きの前置き=

 7月の頭から3週間ほど休暇をとっていて、そこで土台部分のあれこれをやっていた。で、休暇もそろそろ終わりだけど色々と目処は立ったので、来週からは土日を使って徐々にミニゲーム作成を進めていく予定。今の調子だと3ヶ月に1つくらいのペースでできると良いなーと思っている。

 で、土台部分の一環として「Blenderで作った3Dモデルを2D画像にして使う」というのがあり、この部分で色々と手こずったのでそのメモ。

=前置き=

 Blenderで作ったモデルをobjファイルとしてExport(以下ではpot.obj&pot.mtl)し、それをAS3で表示してキャプチャして画像を保存する処理のメモ。

 自分の実際の用途では画像そのままではなく法線画像化+αして保存しているが、あとから参照してわかりやすいように普通の画像としての保存のコードも載せておく。

=通常のスクショ保存コード=

Away3Dを使ってOBJファイルを表示し、そのスクショを保存するコード。

以下、要点の列挙。

  • Blenderの1マスの大きさのものを想定
  • BlenderでobjとしてExportするとmtlも保存されるのでそれも必要
    • daeファイルをロードする場合、loadDataあたりでDAEParserを渡す必要があるはず(未検証)
  • 平行投影にするためlensを置き換えているが、透視投影で良ければ置き換えは不要
  • スクリーンショットはqueueSnapshotで撮る
    • この関数を呼んでから次のDraw時に実際に保存されるので、タイミングに注意
    • backgroundAlphaを0にすることで背景を透明にしている
  • あとはBitmapDataをPNGEncoderでpng化して保存している
package  
{
	//面倒なので一通りimportしてる
	import away3d.Away3D;
	import away3d.containers.*;
	import away3d.cameras.*;
	import away3d.cameras.lenses.*;
	import away3d.lights.*;
	import away3d.containers.*;
	import away3d.core.math.*;
	import away3d.materials.*;
	import away3d.materials.lightpickers.*;
	import away3d.materials.methods.*;
	import away3d.materials.utils.*;
	import away3d.loaders.*;
	import away3d.loaders.parsers.*;
	import away3d.events.*;
	import away3d.loaders.misc.*;
	import away3d.library.*;
	import away3d.library.assets.*;
	import away3d.library.utils.*;
	import away3d.events.*;
	import away3d.animators.*;
	import away3d.animators.data.*;
	import away3d.animators.nodes.*;
	import away3d.entities.*;

	import flash.display.*;
	import flash.errors.*;
	import flash.events.*;
	import flash.net.*;
	import flash.geom.*;

	import mx.graphics.codec.*;
   

	[SWF(width="512", height="512", frameRate="60", backgroundColor="0xFFFFFF")]
	public class Test extends Sprite
	{
		//==Embed==

		//モデル
		[Embed(source="pot.obj",mimeType="application/octet-stream")]
		private var Model:Class;
		[Embed(source="pot.mtl",mimeType="application/octet-stream")]
		private var Mtl:Class;


		//==Const==

		//保存する画像のサイズ
		static public const BMD_W:int = 512;

		//モデル用スケーリング
		//- 真横から用
//		static public const MODEL_SCALE:int = BMD_W * 0.9;
		//- 斜め上から用
		static public const MODEL_SCALE:int = BMD_W * 0.75;


		//==Var==

		//Away3D
		//- View
		private var m_View:View3D;

		//Model
		private var m_Loader_Model:Loader3D;

		//スクリーンショット用BitmapData
		public var m_BitmapData_SS:BitmapData;

		///コンストラクタ
		public function Test()
		{
			super();
		   
			addEventListener(Event.ADDED_TO_STAGE, OnAddedToStage);
		}

		///Init
		private function OnAddedToStage(event:Event):void
		{
			//Away3D
			//- View
			{
				m_View = new View3D();
				m_View.antiAlias = 4;
				addChild(m_View);
			}

			//レンズ:平行投影化
			{
				var lens:OrthographicLens = new OrthographicLens(BMD_W);
				m_View.camera.lens = lens;
			}

			//カメラMatrix
			{
				//- 真横から
				//m_View.camera.lookAt(new Vector3D(m_View.camera.x, m_View.camera.y, m_View.camera.z + 100));
				//m_View.camera.y = BMD_W * 0.45;

				//- 斜め上から
				m_View.camera.lookAt(new Vector3D(m_View.camera.x, m_View.camera.y - 100, m_View.camera.z + 100));
				m_View.camera.y = BMD_W * 2.4;
			}

			//モデルロード
			{
				var assetLoaderContext:AssetLoaderContext = new AssetLoaderContext();
				assetLoaderContext.mapUrlToData("pot.mtl", new Mtl());

				Parsers.enableAllBundled();
				m_Loader_Model = new Loader3D(false);
				m_Loader_Model.addEventListener(LoaderEvent.RESOURCE_COMPLETE, OnResourceComplete);
				m_Loader_Model.addEventListener(LoaderEvent.LOAD_ERROR, OnLoadError);
				m_Loader_Model.loadData(new Model(), assetLoaderContext);
			}

			//スクショ
			{
				//スクリーンショット用BitmapDataの用意
				m_BitmapData_SS = new BitmapData(stage.stageWidth, stage.stageHeight, true, 0x00000000);

				//クリックでスクショの保存
				stage.addEventListener(MouseEvent.MOUSE_DOWN, OnMouseDown);
			}
		}

		///ロード完了時コールバック
		private function OnResourceComplete(event:LoaderEvent):void
		{
			//コールバック解除
			m_Loader_Model.removeEventListener(LoaderEvent.RESOURCE_COMPLETE, OnResourceComplete);
			m_Loader_Model.removeEventListener(LoaderEvent.LOAD_ERROR, OnLoadError);

			//モデルの大きさを調整
			m_Loader_Model.scaleX *= MODEL_SCALE;
			m_Loader_Model.scaleY *= MODEL_SCALE;
			m_Loader_Model.scaleZ *= MODEL_SCALE;

			//モデルを表示登録
			m_View.scene.addChild(m_Loader_Model);

			//Updateの開始
			addEventListener(Event.ENTER_FRAME, Update);

			//SSをこの段階で取得(厳密には取得要求)
			m_View.backgroundAlpha = 0;//背景の色は不要なので透明にしておく
			m_BitmapData_SS.fillRect(m_BitmapData_SS.rect, 0x00000000);
			m_View.renderer.queueSnapshot(m_BitmapData_SS);
		}

		///ロード失敗時コールバック
		private function OnLoadError(event:LoaderEvent):void
		{
			throw new IOError("Load Err : " + event.url);
		}

		///クリックでSSの保存開始
		private function OnMouseDown(e:Event):void{
			(new FileReference).save((new PNGEncoder()).encode(m_BitmapData_SS), "test.png");
		}

		///毎フレーム実行
		private function Update(event:Event):void
		{
			m_View.render();
		}
	}
}

=法線のスクショ保存コード=

上のコードに「OnAssetCompleteまわり」と「Method_Diffuse_Custom」を追加したもの。これにより、モデル表示のピクセルシェーダをこちらが指定したものに置き換えている。

具体的には、モデル等が生成された時にOnAssetCompleteが呼ばれ、マテリアルが生成された時にこの関数でピクセルシェーダの管理をしているmethod系を置き換えている。とりあえずDiffuseMethodを置き換えるだけで目標は達成できそうだったので、以下ではそのようになっている。(念のためAmbientの方もコメントアウトして残してあるが)

Blenderではテクスチャを設定していないモデルをExportしたためマテリアルはColorMaterialになっているが、テクスチャを設定したものだとTextureMaterialになっているかもしれない。(未検証)

法線の値は(vo.needsNormals = trueにしておけば)_normalFragmentRegに入ってくるが、値の範囲が-1〜+1になっており、表示できるのは0〜1なので0.5倍して0.5を足すことで表示用に調整している。

また、後の検証用に「t.b = material_index」というコメント部分を設定しているが、これは法線の値を一部破壊している(Zの値を置き換えている)ので注意。一応、法線の値はXとYさえあればZ=Sqrt(1 - x*x - y*y)で求まるという想定。

package  
{
	//面倒なので一通りimportしてる
	import away3d.Away3D;
	import away3d.containers.*;
	import away3d.cameras.*;
	import away3d.cameras.lenses.*;
	import away3d.lights.*;
	import away3d.containers.*;
	import away3d.core.math.*;
	import away3d.materials.*;
	import away3d.materials.lightpickers.*;
	import away3d.materials.methods.*;
	import away3d.materials.utils.*;
	import away3d.loaders.*;
	import away3d.loaders.parsers.*;
	import away3d.events.*;
	import away3d.loaders.misc.*;
	import away3d.library.*;
	import away3d.library.assets.*;
	import away3d.library.utils.*;
	import away3d.events.*;
	import away3d.animators.*;
	import away3d.animators.data.*;
	import away3d.animators.nodes.*;
	import away3d.entities.*;

	import flash.display.*;
	import flash.errors.*;
	import flash.events.*;
	import flash.net.*;
	import flash.geom.*;

	import mx.graphics.codec.*;
   

	[SWF(width="512", height="512", frameRate="60", backgroundColor="0xFFFFFF")]
	public class Test extends Sprite
	{
		//==Embed==

		//モデル
		[Embed(source="pot.obj",mimeType="application/octet-stream")]
		private var Model:Class;
		[Embed(source="pot.mtl",mimeType="application/octet-stream")]
		private var Mtl:Class;


		//==Const==

		//保存する画像のサイズ
		static public const BMD_W:int = 512;

		//モデル用スケーリング
		//- 真横から用
//		static public const MODEL_SCALE:int = BMD_W * 0.9;
		//- 斜め上から用
		static public const MODEL_SCALE:int = BMD_W * 0.75;


		//==Var==

		//Away3D
		//- View
		private var m_View:View3D;

		//Model
		private var m_Loader_Model:Loader3D;

		//スクリーンショット用BitmapData
		public var m_BitmapData_SS:BitmapData;

		///コンストラクタ
		public function Test()
		{
			super();
		   
			addEventListener(Event.ADDED_TO_STAGE, OnAddedToStage);
		}

		///Init
		private function OnAddedToStage(event:Event):void
		{
			//Away3D
			//- View
			{
				m_View = new View3D();
				m_View.antiAlias = 4;
				addChild(m_View);
			}

			//レンズ:平行投影化
			{
				var lens:OrthographicLens = new OrthographicLens(BMD_W);
				m_View.camera.lens = lens;
			}

			//カメラMatrix
			{
				//- 真横から
				//m_View.camera.lookAt(new Vector3D(m_View.camera.x, m_View.camera.y, m_View.camera.z + 100));
				//m_View.camera.y = BMD_W * 0.45;

				//- 斜め上から
				m_View.camera.lookAt(new Vector3D(m_View.camera.x, m_View.camera.y - 100, m_View.camera.z + 100));
				m_View.camera.y = BMD_W * 2.4;
			}

			//モデルロード
			{
				var assetLoaderContext:AssetLoaderContext = new AssetLoaderContext();
				assetLoaderContext.mapUrlToData("pot.mtl", new Mtl());

				Parsers.enableAllBundled();
				m_Loader_Model = new Loader3D(false);
				m_Loader_Model.addEventListener(AssetEvent.ASSET_COMPLETE, OnAssetComplete);
				m_Loader_Model.addEventListener(LoaderEvent.RESOURCE_COMPLETE, OnResourceComplete);
				m_Loader_Model.addEventListener(LoaderEvent.LOAD_ERROR, OnLoadError);
				m_Loader_Model.loadData(new Model(), assetLoaderContext);
			}

			//スクショ
			{
				//スクリーンショット用BitmapDataの用意
				m_BitmapData_SS = new BitmapData(stage.stageWidth, stage.stageHeight, true, 0x00000000);

				//クリックでスクショの保存
				stage.addEventListener(MouseEvent.MOUSE_DOWN, OnMouseDown);
			}
		}

		///ロード完了時コールバック
		private function OnAssetComplete(ev : AssetEvent) : void
		{
			switch (ev.asset.assetType) {
			case AssetType.MATERIAL:
				//マテリアルのピクセルシェーダを置き換えて、法線表示のものにする
//				var material:TextureMaterial = ev.asset as TextureMaterial;
				var material:ColorMaterial = ev.asset as ColorMaterial;
				if(material != null){
//					material.ambientMethod = new Method_Ambient_Custom();
					material.diffuseMethod = new Method_Diffuse_Custom();
//					material.specularMethod = null;
				}
				break;
			}
		}

		///ロード完了時コールバック
		private function OnResourceComplete(event:LoaderEvent):void
		{
			//コールバック解除
			m_Loader_Model.removeEventListener(LoaderEvent.RESOURCE_COMPLETE, OnResourceComplete);
			m_Loader_Model.removeEventListener(LoaderEvent.LOAD_ERROR, OnLoadError);

			//モデルの大きさを調整
			m_Loader_Model.scaleX *= MODEL_SCALE;
			m_Loader_Model.scaleY *= MODEL_SCALE;
			m_Loader_Model.scaleZ *= MODEL_SCALE;

			//モデルを表示登録
			m_View.scene.addChild(m_Loader_Model);

			//Updateの開始
			addEventListener(Event.ENTER_FRAME, Update);

			//SSをこの段階で取得(厳密には取得要求)
			m_View.backgroundAlpha = 0;//背景の色は不要なので透明にしておく
			m_BitmapData_SS.fillRect(m_BitmapData_SS.rect, 0x00000000);
			m_View.renderer.queueSnapshot(m_BitmapData_SS);
		}

		///ロード失敗時コールバック
		private function OnLoadError(event:LoaderEvent):void
		{
			throw new IOError("Load Err : " + event.url);
		}

		///クリックでSSの保存開始
		private function OnMouseDown(e:Event):void{
			(new FileReference).save((new PNGEncoder()).encode(m_BitmapData_SS), "test.png");
		}

		///毎フレーム実行
		private function Update(event:Event):void
		{
			m_View.render();
		}
	}
}


import away3d.arcane;
import away3d.core.managers.*;
import away3d.materials.utils.*;
import away3d.textures.Texture2DBase;
import away3d.containers.*;
import away3d.entities.*;
import away3d.events.*;
import away3d.loaders.*;
import away3d.loaders.parsers.*;
import away3d.loaders.misc.*;
import away3d.library.assets.*;
import away3d.materials.*;
import away3d.materials.methods.*;
import flash.display3D.*;
import flash.display.*;
import flash.errors.*;
import flash.events.*;
import flash.net.*;

use namespace arcane;

/*
//特に意味なさそう
class Method_Ambient_Custom extends BasicAmbientMethod
{
	override arcane function initVO(vo : MethodVO) : void
	{
		super.initVO(vo);

		vo.needsNormals = true;
	}

	override arcane function getFragmentCode(vo : MethodVO, regCache : ShaderRegisterCache, targetReg : ShaderRegisterElement) : String
	{
		var code : String = "";

		var t : ShaderRegisterElement;
		t = regCache.getFreeFragmentVectorTemp();
		regCache.addFragmentTempUsages(t, 1);

		_ambientInputRegister = regCache.getFreeFragmentConstant();
		vo.fragmentConstantsIndex = _ambientInputRegister.index*4;

//		throw new Error("AAA : " + t);//=> ft1
//		throw new Error("BBB : " + _normalFragmentReg);//=> 
		code += "mov " + t + ", " + _normalFragmentReg + "\n";
//		code += "mov " + t + ".w, " + _ambientInputRegister + ".w\n";
		code += "mov " + targetReg + ", " + t + "\n";

		regCache.removeFragmentTempUsage(t);

		return code;
	}
}
//*/



class Method_Diffuse_Custom extends BasicDiffuseMethod
{
	///必要な情報の要求など
	//- 法線の情報が必要なのでその部分だけ設定
	override arcane function initVO(vo : MethodVO) : void
	{
		super.initVO(vo);

		//法線の情報を使うのでTrueにしておく
		vo.needsNormals = true;
	}

	//使わないので空にする
	override arcane function getFragmentPreLightingCode(vo : MethodVO, regCache : ShaderRegisterCache) : String{
		return "";
	}
	override arcane function getFragmentCodePerLight(vo : MethodVO, lightIndex : int, lightDirReg : ShaderRegisterElement, lightColReg : ShaderRegisterElement, regCache : ShaderRegisterCache) : String{
		return "";
	}
	arcane override function getFragmentCodePerProbe(vo : MethodVO, lightIndex : int, cubeMapReg : ShaderRegisterElement, weightRegister : String, regCache : ShaderRegisterCache) : String{
		return "";
	}

	//定数設定
	override arcane function activate(vo : MethodVO, stage3DProxy : Stage3DProxy) : void
	{
		var context : Context3D = stage3DProxy._context3D;

		var index : int = vo.fragmentConstantsIndex;
		var data : Vector.<Number> = vo.fragmentData;
		data[index] = 0.5;//0.5
		data[index+1] = 0x01/0xFF;//material index
		data[index+2] = 0;
		data[index+3] = 0;
	}

	///法線の可視化
	override arcane function getFragmentPostLightingCode(vo : MethodVO, regCache : ShaderRegisterCache, targetReg : ShaderRegisterElement) : String
	{
		var code : String = "";

		//一時変数の確保
		var t : ShaderRegisterElement;
		t = regCache.getFreeFragmentVectorTemp();
		regCache.addFragmentTempUsages(t, 1);

		//本来はDiffuseColorの設定だが、定数設定を乗っ取って別の値にしている
		_diffuseInputRegister = regCache.getFreeFragmentConstant();
		vo.fragmentConstantsIndex = _diffuseInputRegister.index*4;

		//test
		//throw new Error("BBB : " + _normalFragmentReg);//=> ft0

		//- 右:赤、上:緑
		//-- 中央:黒、右上:黄
		//→ちゃんとマイナスの値が来てるようで、中央より少しでも左や下になれば黒になる
		// →0.5 + 0.5 * Valにしてやる必要がある
		//code += "mov " + targetReg + ", " + _normalFragmentReg + "\n";

		//
		//- res = 0.5 + 0.5 * nrm
		//-- t = 0.5 * nrm
		code += "mul " + t + ", " + _diffuseInputRegister + ".x, " + _normalFragmentReg + "\n";
		//-- t = 0.5 + t
		code += "add " + t + ", " + _diffuseInputRegister + ".x, " + t + "\n";
		//-- t.b = material_index
		code += "mov " + t + ".z, " + _diffuseInputRegister + ".y\n";
		//-- res = t
		code += "mov " + targetReg + ", " + t + "\n";

		//一時変数の解放
		regCache.removeFragmentTempUsage(t);

		return code;
	}
}