FFEXはカスタマイズ好きのためのゲームだった

=前置き=

 いつもの「帰郷時に遊んできたゲームのログ」なんだけど、この冬はほぼFFEXだけ遊んでたし、自分のTwitter上ではLight版でいまいち楽しみを見いだせてない人が多かった&自分も最初は懐疑的だったので、今回はFFEXについて解説しつつオススメしておこうかと思う。

FFEX:概要=

 ファイナルファンタジー エクスプローラーズ(以下、FFEX)は大雑把に言うと「FFCCの見た目で」「ネットRPGっぽく操作し」「モンハンのようなクエストをこなしつつ」「武器・防具・アビリティのカスタマイズを楽しむ」ゲームである。

 FFEXは見た目的にどうしてもFFCCを思い出してしまい、あれと比べるとアクション性が薄いし地形ギミック的なものもなくて単調に感じてしまう。Light版を軽く触ってもその印象は大して変わらないんじゃないかと思う。ただ、FFEXの真価は「カスタマイズ」にある。そのため、楽しさのベクトルとしてはFFCCやモンハンよりもむしろパルテナ風来のシレン(のカスタマイズ部分)に近いと言える。

 カスタマイズ系の遊びの宿命として「素材や情報が揃わないと楽しみづらい」というのがあり、FFEXも序盤の情報だけでは楽しむのがわりと難しい。というわけでここでは具体的にどういうカスタマイズができるかを挙げて楽しさを説明していくことにする。

=カスタマイズ例:連携コンボ火力=

 FFEXのアビリティ(ダッシュで攻撃する技やらファイアのような魔法などの総称)は「連携」という能力を付与することでつなぐことができる。いわゆる格ゲーのキャンセルと同じで技のスキを潰すことができ、さらに次に出すアビリティの動作がやや早くなった上にダメージも少し上がる効果がある。そしてアビリティに付与できる別の能力として「コンボ補正」という「コンボ数が上がるほどダメージが上がる」ものまであるため、「ダッシュ攻撃で接近→多段ヒット攻撃でコンボ数稼ぎ→強力な一撃にコンボ補正をかけて大ダメージ」ということなどができる。

 実際には何に連携できるかは制限があったり連携先を閃くのはランダムなので狙ったものが出なかったりもするが、意外なところにつながったりもするし制限があったうえで目的を達成できる連携を考えるのも楽しいので悪くないんじゃないかと思う。

=カスタマイズ例:範囲即死=

 FFEXは意外なアビリティに意外な能力がつく。

 例えば棍棒技のアビリティに「レデュース」というものがあり、これは「自分を中心に円形のエリアを作り出し、この中に居る間は自分達が相手に与える状態異常の発生率が上がる&与えた状態異常の効果時間が長くなる」という(メテオのような派手な火力などと比べると)やや地味な能力である。

 しかしこの「レデュース」というアビリティには「即死」能力が付与できる。するとどうなるかというと「範囲内の敵が即死する」という能力に早変わりする。実際には「即死」は確率発動なので確実に全滅させられるわけではないし、ボスにはもちろん効かなかったりするのだが、その代わり「即死以外にも凍結や石化が付けられるので、即死を免れても極めて高確率で敵の足を止められる」「時魔道士であれば効果範囲がさらに広がり、銃などの遠距離系の攻撃よりもさらに広い距離かつ敵全員に状態異常をかけられる」という状態なので、ザコが大量に出てくる中ボス系のクエストであればとても有用な技になる。

 こういう感じで意外なアビリティにつく意外な能力がとても有用だったりするため、そういうものを探したりあるいは偶然見つけたりするのが楽しい。

=カスタマイズ例:消費AP減少+AP吸収=

 FFEXのアビリティはAPというMP的なリソースを消費して発動する。アビリティを使わない(&走らない)状態であればAPは自然回復するし、通常攻撃を当てることでさらに回復することもできるが、なんにせよAPを回復している間の攻撃はとても地味なものになる。

 しかしもちろんFFEXではこういう状態にならずにアビリティをばんばん使っていける状態が作れる。武器には「消費AP減少」がつけられるし、防具に「AP吸収」を付けられる。武器は最終的には大抵のジョブで二刀流できるようになるし、防具は頭・体・足の3箇所がある。なのでそれぞれに前述の能力を加えることで、大抵のアビリティがAP消費を気にせずに連発できるようになる。

 これらの能力以外にも武器と防具には色々と付与できるし、武器と防具で合計5種類+(実質的にリセットマラソン必須なのでアレではあるが)アクセサリーによってさらに1〜4種類くらいの能力を付与することができ、アビリティだけでなく装備の部分でもあれこれカスタマイズを考えることができて楽しい。

=カスタマイズ例:見た目=

 あと個人的に重要なのは「見た目のカスタマイズ」だろうか。モンハンだと男の見た目がいまいち食指が動かない感じだったが、FFEXではFFCCっぽくてわりと好みだし、何より「頭の防具を非表示」というオプションがあるのでゴツい頭装備でせっかくカスタマイズした顔や髪型を隠すこともなくて済む。

 また、防具に関しては強化さえすれば最終的なパラメータは大差ないため、わりと見た目重視で選んでしまっても問題ないのも嬉しい。(さすがに特定のモンスターでは耐性などを気にしないとキツかったりするが)

=まとめ=

 というわけで、FFEXは「アビリティ」「装備」「見た目」のカスタマイズが楽しめてとても良かった。

 実際のゲーム内容は「ライトなモンハン」という感じで、「モンハンは楽しいけど疲れる」という人にはちょうど良い感じではないかと思う。ボスの攻略は「パターンを覚えて回避しつつ攻撃」という感じなのでモンハンと同じ感覚で進められると思う。あまり負荷を感じずに遊べるので、「連続6体討伐」とかモンハン的には正気の沙汰とは思えないものも実際にやってみると意外とラクだった。(さすがにヘイストとテレポストーンは必須レベルだったが)

 何かのゲームの続編というわけではないのでさすがに細かい不備・不満はあるものの、一通りのクエストをクリアしたあとの感想が「もっと遊びたかった(堪能したかった)なぁ」というものだったので、続編が出たりしたらたぶんまた買って遊ぶと思う。

=今年の予定=

 FFEXのオススメは以上。

 で、今年の予定だけど、去年の教訓が「予定を立てても体調の都合で上手くはいかないし、むしろ予定を遂行しようとするせいで体調が悪化している面もあるので、そもそも自分は予定とか立てない方が良い」というものだったので、「今年から予定はなし」という結論になった。

 なので、今年からは「作りたいものを作りたい時に作る」という感じになると思う。たぶんいくつかのゲームを断続的に作っていくことになると思うので、もうしばらく(今年の前半くらい)は商品レベルのゲームは完成しないと思う。wonderflに上げていたようなプロトタイプは今年の前半でいくつか作るかもしれないけど。

 細かい状況はTwitterで報告しつつ、プロトタイプ以上の具体的な成果物はブログに上げていこうと思うので、気長にお待ちください。

近況報告

 風邪をひいたり体調が悪化したりしたため、今年中の新しいゲームのUPは断念。今日も体調が微妙なので、本格的な活動再開は来年からになりそう。

 さらに、「予定を立てて作業する」というのは趣味側でやるとかなり体調に負荷がかかるのがわかったので、これからは「モチベが向いているものを作業する」という方針で行く予定。あれこれ半端に積み上げていくことになるので、またしばらく(半年くらい?)は新しいゲームのUPがないかもしれない。ただ、モチベの向き次第では「1日程度で作れるもの」を作ったりするかもしれないので、唐突に何か出てきたりはするかも。

2014夏の帰郷ログ

=前置き=

 いつもの「帰郷時に何を遊んだか」のログ。今回は台風の影響で帰郷できるかギリギリだったものの、有給をとって1日早く帰ったおかげでなんとか難は免れたので、いつものように遊んでこれた。

 今回はボドゲはほとんどやらず、3DSWiiUメインになった。

パルテナ

 集まりやすいメンツは全員パルテナを持っていて、ちょうど6人だったので今回はパルテナをやることが多かった。

 以前は初めての対人戦だったので慣れで精一杯だったが、今回はようやく相手の動きを見る余裕が出てきたので立ち回りを徐々に理解してきたところ。自分に合っている戦術も段々とわかってきたので神器の方向も固まってきたが、ハート稼ぎがだいぶ必要なので実際の作成は次の冬あたりになりそう。ハート稼ぎ用のダッシュアッパーはできて1分に1万前後は稼げるので、ゆっくりやってても冬までには十分稼げると思う。ただ、来月からのゲームはスマブラに移行すると思うし、冬もパルテナではなくてスマブラとかをやるかもしれないので、稼ぐ意味があるかやや怪しいところだが。

バンブラP

 集まる人数が最高の時は7〜8人くらいになって、その時はかなりやれる遊びが限られていたものの、バンブラP3DSさえあれば参加できるのでちょうど良かった。

=マリオチェイスニンテンドーランド)=

 集まる人数が5人の時は主にマリオチェイスをやっていた。WiiUの特徴を活かした「1人がパッドで全体を把握しつつ逃げて、4人がそれぞれの視界を活かして情報共有しつつ追いかける」という鬼ごっこで、エアライドでもドライブで鬼ごっこして楽しんでいた身としてはとても楽しめた。

 唯一の難点は必要な人数で、どうも4人以下だと楽しくないらしく、「5人限定時のゲーム」という感じになっていた。しかし見てるだけでもわりと楽しめるので、6人以上の時もそれなりに楽しかったかもしれない。

=キャンディーまつり(ニンテンドーランド)=

 同じくニンテンドーランドのキャンディーまつり(どうぶつの森)も主に人数が5人の時に遊んでいた。こちらは逆に「1人が2キャラを操作して追いかけて、4キャラが逃げつつキャンディーを規定数回収する」というゲームで、こちらも楽しかった。

 鬼は2キャラを動かすので、これを利用して鬼を2人で別々に操作して6人プレイを試したりしたものの、挟み撃ちや陽動などの作戦も立てづらかったらしく、6人でのプレイはイマイチだった様子。また、戦術が固まってくると鬼が不利になってきたようで、勝率は逃げる側の方が高くなっていた。これはむしろMaxの5人ではなく、4人くらいでやるのがちょうど良いっぽかった。

=斬=

 唯一やったボドゲで、詳細は帰郷時のログ - Master of Noneを参照。

 7人でもできるボドゲということでやってみて、ゲーム内容的には十分に楽しめたが、7人で輪をつくるように集まるのはスペース的にややキツかった。ここらへんは各自が好きなスペースに居られるバンブラPの方が良かったように思う。


=まとめ=

 というわけで今回は主にデジタルゲームマルチプレイを楽しんできた。

 来月はスマブラが出るし、再来月はMH4Gが出るので、休日のゲームはそこらへんになると思うし、次の長期休暇ではそこらへんのローカル対戦をやることになるかもしれない。

 もちろん休日はゲームとは別に個人のゲーム作成を進める予定。今のところ概ね想定通りに進んでるので、体調をまた崩したりしない限りは再来月くらいには何か出来上がるんじゃないかと思う。

Away3Dにてテクスチャに別テクスチャの画像などを書き込んで表示するまで

=前置き=

 ミニゲーム作成のための描画まわりを作成していて、「BitmapDataやFilterではなくシェーダで書いた方がラクな画像加工」というのがあったので、そこら辺のメモ。
 今回の用途では端末のサイズに合わせてゲーム内で行う必要があったのでAway3Dを使っているものの、単なるコンバート目的であれば生のStage3Dだけでも普通にできると思う。

 ちなみにこんな感じでしばらくは検証とかが入るので、ミニゲームの作成は3ヶ月に1個というよりは6ヶ月に2個という感じのペースになりそう。(序盤は検証で時間を食いそう)

=解説=

 今回は「テクスチャに描いてあるものを別のテクスチャに普通に描画する」というところまで。実際の用途ではこの描画の際のシェーダをいじったり、複数のテクスチャを書き込んだりする。

 で、要点は以下のような感じ

  • 描画そのものはわりと普通の描画と同じ
  • setRenderToTextureで描画先をテクスチャに変更しておく
    • その際のテクスチャは普通に作成しておき、getTextureForStage3Dで得たものを使う
  • 描画元テクスチャを画面いっぱいに描く感じに設定しておくことで描画先テクスチャ全体に描く
    • ここらへんはDATA_VERTEXあたりの設定で行っている

=コード=

/*
一旦テクスチャに描いてから表示するところまで
・シェーダは普通なので現段階では通常描画とほぼ変わらない(linearとかsmoothとかの影響はあるが)
*/



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.core.managers.*;
	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 away3d.textures.*;
	import away3d.primitives.*;
	import com.adobe.utils.AGALMiniAssembler;

	import flash.display.*;
	import flash.display3D.*;
	import flash.display3D.textures.*;
	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 TexToTexTest extends Sprite
	{
		//==Embed==

		[Embed(source='test_grad_circle32.png')]
		 private static var Bitmap_Test: Class;


		//==Const==

		//実際の画面サイズ
		public function GetStageW():int{
			return stage.stageWidth;
//			return Capabilities.screenResolutionX;
		}
		public function GetStageH():int{
			return stage.stageHeight;
//			return Capabilities.screenResolutionY;
		}

		//テクスチャサイズ
		//- 実際の画面サイズよりも大きい2^nサイズ
		//- 起動直後に計算して設定
		static public var TEX_W:int;

		//ポリゴンサイズ
		//- 平行投影で画面の横サイズに合わせるので値の大きさにあまり意味はない(共通化することに意味がある)
		static public const GEOM_W:Number = 1;

		//元画像サイズ
		//- 本体から取るようにした方が良さそうだが今回はこれで
		static public const BMD_W:int = 32;


		//似非スーパーサンプリング用
		//- 頂点シェーダ
		private const SHADER_VERTEX:String = 
			"m44 op, va0, vc0	\n" +	//頂点座標 = 頂点座標×Matrix3D
			"mov v0, va1";              //V0 = UV
		//- ピクセルシェーダ
		private const SHADER_FRAGMENT:String =
			"tex ft0, v0, fs0 <2d,linear,clamp>	\n" +//ft0 = テクスチャ0[V0.x, V0.y]
			"mov oc, ft0";	//色 = ft0


		//==Var==

		//Away3D
		//- View
		private var m_View:View3D;
		//- View
		private var m_Scene:Scene3D;
		//- Camera
		private var m_Camera:Camera3D;

		//テクスチャ描画用
		private var m_TexToTex_Vertices:VertexBuffer3D;
		private var m_TexToTex_Indices:IndexBuffer3D;
		private var m_TexToTex_Program:Program3D;
		private var m_TexToTex_Matrix3D:Matrix3D;


		//==Function==

		///コンストラクタ
		public function TexToTexTest()
		{
			super();

			addEventListener(Event.ADDED_TO_STAGE, OnAddedToStage);
		}

		///Init
		private function OnAddedToStage(event:Event):void
		{
			//Calc Const Param
			{
				//TEX_W
				{
					for(TEX_W = BMD_W; TEX_W < GetStageW(); TEX_W *= 2){}
				}
			}

			//Away3D
			{
				//- View
				m_View = new View3D();
				{
					m_View.antiAlias = 4;
					addChild(m_View);
				}

				//- Scene
				m_Scene = m_View.scene;

				//- Camera
				m_Camera = m_View.camera;
				//- 最終表示の確認のため、カメラ位置の調整
				{
					m_Camera.x = 0;
					m_Camera.y = 100;
					m_Camera.z = 0;

					var cam_trg_pos:Vector3D = new Vector3D();
					m_Camera.lookAt(cam_trg_pos);
				}

				//- レンズ
				//-- 平行投影化
				{
					var lens:OrthographicLens = new OrthographicLens(GEOM_W);
					m_Camera.lens = lens;
				}
			}

			//Context3Dを使いたいので、残りはそれが生成されたあとに行う
			{
				m_View.stage3DProxy.addEventListener(Stage3DEvent.CONTEXT3D_CREATED, onContext3dCreated);
			}
		}

		private function onContext3dCreated(e:Event):void{
			var stage3DProxy:Stage3DProxy = m_View.stage3DProxy;
			var context : Context3D = stage3DProxy.context3D;

			//元画像のテクスチャ
			var tex_ori:BitmapTexture;
			{
				//Texture
				{
					var bmd_ori:BitmapData = new BitmapData(BMD_W, BMD_W, true, 0x00000000);

					//適当な画像を描画しておく
					bmd_ori.draw(new Bitmap_Test());

					tex_ori = new BitmapTexture(bmd_ori);
				}

				//こちらは最終的な描画には使わないのでGeometryなどの作成は行わずテクスチャの生成のみ
			}

			//描画確認用
			var tex:BitmapTexture;
			{
				//Geometry
				var geom:PlaneGeometry;
				{
					var segment_w:int = 1;
					var segment_h:int = 1;
					geom = new PlaneGeometry(GEOM_W, GEOM_W, segment_w, segment_h);
				}

				//Material
				var material:TextureMaterial;
				{
					//Texture
					{
						var bmd:BitmapData = new BitmapData(TEX_W, TEX_W, true, 0x00000000);

						tex = new BitmapTexture(bmd);
					}

					//smoothをオンにしてスーパーサンプリングっぽくする
					var smooth:Boolean = true;
					var repeat:Boolean = false;
					material = new TextureMaterial(tex, smooth, repeat);
				}

				//Mesh
				{
					var mesh:Mesh = new Mesh(geom, material);
					m_Scene.addChild(mesh);
				}
			}

			//テクスチャ描画用初期化
			{
				//頂点シェーダの作成
				var assembler_vertex:AGALMiniAssembler = new AGALMiniAssembler();
				assembler_vertex.assemble(Context3DProgramType.VERTEX, SHADER_VERTEX);
				//ピクセルシェーダの作成
				var assembler_fragment:AGALMiniAssembler = new AGALMiniAssembler();
				assembler_fragment.assemble(Context3DProgramType.FRAGMENT, SHADER_FRAGMENT);

				//シェーダをまとめるやつの作成
				m_TexToTex_Program = context.createProgram();
				m_TexToTex_Program.upload(assembler_vertex.agalcode, assembler_fragment.agalcode);

				//頂点情報
				var DATA_VERTEX:Vector.<Number> = Vector.<Number>([
					//X, Y, Z	U, V
					-1, -1, 0,	0, 1,
					-1,  1, 0,	0, 0,
					 1,  1, 0,	1, 0,
					 1, -1, 0,	1, 1,
				]);
				//頂点データ化
				//- 4行、5要素
				m_TexToTex_Vertices = context.createVertexBuffer(4, DATA_VERTEX.length/4);
				//- 4行のデータとして送る
				m_TexToTex_Vertices.uploadFromVector(DATA_VERTEX, 0, 4);

				//頂点Index情報
				var DATA_INDEX:Vector.<uint> = Vector.<uint>([
					0, 1, 2,
					2, 3, 0
				]);
				//頂点Indexデータ化
				m_TexToTex_Indices = context.createIndexBuffer(DATA_INDEX.length);
				m_TexToTex_Indices.uploadFromVector(DATA_INDEX, 0, DATA_INDEX.length);

				//Matrix3Dの生成
				m_TexToTex_Matrix3D = new Matrix3D();
			}

			//テクスチャからテクスチャへの描画
			{
				DrawTexToTex(
					tex_ori.getTextureForStage3D(stage3DProxy),//Src
					tex.getTextureForStage3D(stage3DProxy)//Dst
				);
			}

			//Update
			{
				addEventListener(Event.ENTER_FRAME, Update);
			}
		}

		///テクスチャからテクスチャへの描画
		public function DrawTexToTex(tex_src:TextureBase, tex_dst:TextureBase):void{
			var stage3DProxy:Stage3DProxy = m_View.stage3DProxy;
			var context : Context3D = stage3DProxy.context3D;

			//基本設定
			{
				//描画先を表示用テクスチャに切替
				context.setRenderToTexture(tex_dst, false, 0, 0);

				//透明でクリア
				context.clear(0, 0, 0, 0);

				//シェーダの設定
				context.setProgram(m_TexToTex_Program);

				//頂点データの設定
				//- Coord x 3
				context.setVertexBufferAt(0, m_TexToTex_Vertices, 0, Context3DVertexBufferFormat.FLOAT_3);
				//- UV x 2
				context.setVertexBufferAt(1, m_TexToTex_Vertices, 3, Context3DVertexBufferFormat.FLOAT_2);

				//テクスチャの設定
				stage3DProxy.setTextureAt(0, tex_src);

				//Matrix3D
				context.setProgramConstantsFromMatrix(Context3DProgramType.VERTEX, 0, m_TexToTex_Matrix3D);
			}

			//描画
			{
				//描画
				context.drawTriangles(m_TexToTex_Indices, 0, -1);
				//反映
				context.present();
			}

			//設定を戻す
			{
				stage3DProxy.setTextureAt(0, null);
				context.setVertexBufferAt(0, null);
				context.setVertexBufferAt(1, null);

				//presentで自動で戻るはずだが一応描画先を戻す
				context.setRenderToBackBuffer();
			}
		}

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

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;
	}
}

説明しよう!マテリアルとはッ!

=前置き=

 以前に書いた説明しよう!シェーダーとはッ! - Master of Noneの続きのようなもの。シェーダまわりの学習を始めた頃は「マテリアルとテクスチャとシェーダの関係」や「ポリゴンとモデルの違い」もよくわからなかったのを思い出したので、そこらへんの説明を昔の自分がわかるように書いてみようと思う。

 前回と同様に粒度の粗い説明なので、これで大まかな概念を理解したらちゃんとした説明を読むと良いと思う。

=テクスチャ=

 テクスチャとは「画像」である。一般的にはtgaというファイル形式が多く使われるかと思うが、pngなど他の形式でもサポートされていれば使うことが可能である。とにもかくにも基本的には「テクスチャ=ただの画像」である。

 逆に言えばテクスチャは「画像以外のデータ」は持たない。アンビエントとかいうパラメータを持ったりしない、あくまで単なる画像である。

=ポリゴン=

 ポリゴンとは「頂点データのかたまり」である。これは以前の説明しよう!シェーダーとはッ! - Master of Noneで書いたように、頂点(=位置情報やテクスチャ座標などのデータ)によって主に三角形の頂点として並べる形で表現されたものになる。

 ポリゴンとテクスチャは別物である。これらが違うのはわかりやすいかと思うが、最終的な表示ではこのポリゴンにテクスチャを貼って表示しないといけない。これらの対応付けを行うのが次の「マテリアル」と「モデル」になる。

=マテリアル=

 マテリアルは「ポリゴンの表示方法を決めるアレコレをまとめたもの」である。ただ、ベースにあるのは「頂点シェーダ」と「ピクセルシェーダ」なので、基本的には「頂点シェーダとピクセルシェーダをまとめたもの」と考えると良い。

 マテリアルには「テクスチャ」だの「アンビエントカラー」だのを設定する場合があるが、これらはあくまで頂点シェーダやピクセルシェーダに渡して「このテクスチャで表示する」とか「アンビエントカラーはこれを使う」などの処理を行うためのものであり、「ピクセルシェーダで表示する色は全て黒」という風にシェーダ内で計算が完結していれば「テクスチャ」などの設定は不要である。

 逆に言えばマテリアルというのはシェーダなどによって設定するパラメータが決まるとも言える。テクスチャを2枚重ねて表示するシェーダを使うのであればマテリアルでテクスチャを2枚設定できる必要があるし、2つの色のグラデーションを使うのであれば2つの色を設定できる必要がある。(ちなみに色のグラデーションはポリゴンの頂点カラーの方で実現する方法もあるが、状況に応じて色を変化させる必要がある場合はマテリアル経由で変化させたりする)

 そういうわけで、マテリアルとは「ポリゴンの描画方法を(頂点シェーダとピクセルシェーダとそれらに必要なデータを保持することで)決めるもの」である。

=モデル=

 モデルは「ポリゴンとマテリアルをまとめたもの」である。

 マテリアルの中には「シェーダで必要なもの」として「テクスチャ」や「アンビエントカラー」などを保持しているが、「ポリゴン」は保持しない。なぜなら同じマテリアルで色んな形状のポリゴン(例えば色んな形の岩とか)を表示することがあり、そういう時は同じマテリアルを使いまわしつつポリゴンだけ別のものにできた方が都合が良いからだ。というわけで、マテリアルとポリゴンは分けられるのだが、表示の際にはくっつけておいた方が都合が良いので「モデル」という形でまとめる。これが一般的に表示するためのデータ一式となる。

=まとめ=

 まとめると、
 「テクスチャは画像である」
 「ポリゴンは頂点データをまとめたものである」
 「マテリアルはシェーダ(頂点シェーダ&ピクセルシェーダ)とそれらに必要なデータ(テクスチャなど)を保持するものである」
 「モデルはポリゴンとマテリアルを保持するものである」
 という感じ。

 特にマテリアルの理解が難しいかと思うが、シェーダ2つを起点にすればわかりやすいかと思う。

 ポリゴンとモデルの違いは上記の通りで「モデルはポリゴンとマテリアルを内包する」という感じなので、モデルはポリゴンよりも上のレイヤーの概念となる。

 「アンビエントカラー」とかはマテリアルと一緒に目にすることが多いのでマテリアル特有のものに見えるかもしれないが、これはあくまで「マテリアルで設定できるようにピクセルシェーダの外に出したパラメータ」であり、基本的にはシェーダ内で完結するものだ。

 あとは「テクスチャ=画像」と書いたものの、それは必ずしも「表示される画像」というわけではなくて「法線マップ」などの「特殊な画像」だったりもする。ここらへんについてはここでは書かないので、3D描画まわりの別サイトなり書籍なりを参考にしてもらえばと思う。そしてその場合もRGBの扱いが違うだけでデータ形式としては画像であるのは間違いない。

「ガールズモードは男でも楽しめるのか?」という人のための3択問題

=前置き=

 ガールズモードのアップデート無料配信&アップデート済み版の販売が開始されたので、以前に「ゲーマーとしてのガールズモードの楽しみ方」をすでに書いたものの、「コーデそのものを男でも楽しめるのか?」という肝心の部分を書いてなかったので、改めてオススメ記事というか体験版的なものを書いておこうと思う。

=3択問題=

 ということで問題。以下の女性に帽子を合わせるとしたら



 次の3つのどれが良いでしょう?



 このゲームの進行はだいたい上のように「何を合わせるか」を考えることになるので、上の問題で「これが良い!」と思えるのがあったら男でも楽しめるし、「この3つじゃなくてもっとこんな感じのはないの?」と思えたらかなりハマって楽しめると思う。

 実際にはこれらを在庫としてちゃんと揃えておかないといけないので、「まんべんなく汎用性の高いものを揃える」のか「特定のジャンルに特化してあれこれ揃える」のかでここにゲーム的な要素が入ってくる感じ。

 というわけで、上の問題であれこれ思えた人にはオススメ。