2014夏の帰郷ログ
=前置き=
いつもの「帰郷時に何を遊んだか」のログ。今回は台風の影響で帰郷できるかギリギリだったものの、有給をとって1日早く帰ったおかげでなんとか難は免れたので、いつものように遊んでこれた。
=パルテナ=
集まりやすいメンツは全員パルテナを持っていて、ちょうど6人だったので今回はパルテナをやることが多かった。
以前は初めての対人戦だったので慣れで精一杯だったが、今回はようやく相手の動きを見る余裕が出てきたので立ち回りを徐々に理解してきたところ。自分に合っている戦術も段々とわかってきたので神器の方向も固まってきたが、ハート稼ぎがだいぶ必要なので実際の作成は次の冬あたりになりそう。ハート稼ぎ用のダッシュアッパーはできて1分に1万前後は稼げるので、ゆっくりやってても冬までには十分稼げると思う。ただ、来月からのゲームはスマブラに移行すると思うし、冬もパルテナではなくてスマブラとかをやるかもしれないので、稼ぐ意味があるかやや怪しいところだが。
=マリオチェイス(ニンテンドーランド)=
集まる人数が5人の時は主にマリオチェイスをやっていた。WiiUの特徴を活かした「1人がパッドで全体を把握しつつ逃げて、4人がそれぞれの視界を活かして情報共有しつつ追いかける」という鬼ごっこで、エアライドでもドライブで鬼ごっこして楽しんでいた身としてはとても楽しめた。
唯一の難点は必要な人数で、どうも4人以下だと楽しくないらしく、「5人限定時のゲーム」という感じになっていた。しかし見てるだけでもわりと楽しめるので、6人以上の時もそれなりに楽しかったかもしれない。
=キャンディーまつり(ニンテンドーランド)=
同じくニンテンドーランドのキャンディーまつり(どうぶつの森)も主に人数が5人の時に遊んでいた。こちらは逆に「1人が2キャラを操作して追いかけて、4キャラが逃げつつキャンディーを規定数回収する」というゲームで、こちらも楽しかった。
鬼は2キャラを動かすので、これを利用して鬼を2人で別々に操作して6人プレイを試したりしたものの、挟み撃ちや陽動などの作戦も立てづらかったらしく、6人でのプレイはイマイチだった様子。また、戦術が固まってくると鬼が不利になってきたようで、勝率は逃げる側の方が高くなっていた。これはむしろMaxの5人ではなく、4人くらいでやるのがちょうど良いっぽかった。
=斬=
唯一やったボドゲで、詳細は帰郷時のログ - Master of Noneを参照。
7人でもできるボドゲということでやってみて、ゲーム内容的には十分に楽しめたが、7人で輪をつくるように集まるのはスペース的にややキツかった。ここらへんは各自が好きなスペースに居られるバンブラPの方が良かったように思う。
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択問題
=前置き=
ガールズモードのアップデート無料配信&アップデート済み版の販売が開始されたので、以前に「ゲーマーとしてのガールズモードの楽しみ方」をすでに書いたものの、「コーデそのものを男でも楽しめるのか?」という肝心の部分を書いてなかったので、改めてオススメ記事というか体験版的なものを書いておこうと思う。
トマトの期限が切れそうな人のためのバンブラP曲紹介
=前置き=
バンブラPを初日に買ってトマトもその月にゲットした人は今月(3月)でトマトの期限が切れるはずなので、自分がダウンロードして楽しかった曲などを紹介しておきたい。
バンブラDXからの移植曲以外は公式サイトで試聴ができるので、そちらは試聴リンク付き。また、投稿者によって曲構成や難易度なども変わるので、実際にダウンロードする際は投稿者情報にも注意。
=弾いてて楽しい曲=
まずは順当に「弾いてて楽しい曲」をリストアップ。どのパートもそれなりに楽しめるものを優先してみた。
曲名 | アーティスト | 投稿者 | 試聴 |
---|---|---|---|
ダイナマイト | →Pia-no-jaC← | ダイズがヤバイ | リンク |
とにかく曲がかっこよく、弾いてて楽しい。テンポ変化があり、中盤にタイミング的に難しいところがあるものの、トータルで見れば十分に楽しめると思う。
「ピアノ1」がやや難しく、「ピアノ4」がややヒマ&単調以外なところ以外は基本的にどのパートもオススメ。特に(前述以外の)ピアノ系は一通りオススメで、さらに「ピアノ5」が特にオススメ。
曲名 | アーティスト | 投稿者 | 試聴 |
---|---|---|---|
眠り姫 | SEKAI NO OWARI | あがるさがる | リンク |
テンポ変化が強いものの、影響を大きく受けるのは「ロックドラム」くらいで他の部分はそこまで影響を受けず楽しめる。逆に言えば、「ロックドラム」の演奏を選んでしまうとテンポ変化の影響を大きく受けて、その余波が他の楽器にまで波及しやすい。
ということでオススメは「ロックドラム」以外。やや単調だったり序盤がヒマなものもあるが、どのパートもそれなりに楽しめると思う。
曲名 | アーティスト | 投稿者 | 試聴 |
---|---|---|---|
DIRTY | ナイトメア | にゃーち | リンク |
サビの出入りでテンポ変化があるが、慣れれば十分に対応できるレベル。全体的に難易度がやや高めだが、どれも弾けると楽しいタイプ。
「バイオリン2」はわりとヒマなハモりなのでそれ以外がオススメ。
曲名 | アーティスト | 投稿者 | 試聴 |
---|---|---|---|
そしてパレードは続く | Plastic Tree | てるみ | なし |
曲の速度がわりとスロウで曲自体も長く、演奏が単調なパートも多いが曲が好きであれば十分に楽しめると思う。また、難易度がかなり低いので操作はプロを推奨。
「ビブラフォン」がサビでヒマする以外はどのパートもそれなりにオススメ。
曲名 | アーティスト | 投稿者 | 試聴 |
---|---|---|---|
スーパータップ | (リズム天国) | よっしゅ | なし |
リズム天国プレイヤーにはお馴染みの曲。テンポ変化があるものの途中から微妙に高速化するだけなのでほぼ問題ないレベル。
「ロックドラム」がやや難しいが、基本的にはどのパートでも楽しめるはず。
=個人的オススメ曲=
パートによってはヒマだったり難易度が高かったりして微妙に難があるものの、個人的にオススメしたい曲のリストアップ。
曲名 | アーティスト | 投稿者 | 試聴 |
---|---|---|---|
バイキンはかせ | (リズム天国) | ほーたん | なし |
リズム天国プレイヤーにはお馴染みの曲その2。曲自体は楽しいのだが、それなりにヒマになるパートが多いので楽器選択に注意が必要。
「ロックオルガン」「スラップベース」「チェンバロ」は普通に楽しめる。
「シンセドラム」はやや難しいところがあるが弾けると楽しい。
「トランペット」「ブラス1」「ブラス2」がややヒマ。
「ピアノ」はヒマかつ高難度なので非推奨。
曲名 | アーティスト | 投稿者 | 試聴 |
---|---|---|---|
ピンポン | (リズム天国) | はにょ | リンク |
リズム天国プレイヤーにはお馴染みの曲その3。こちらもヒマになるパートが多いので楽器選択に注意が必要。
「ストリングス1」「ストリングス2」「シンセベース」「ロックオルガン」「ピックベース」は普通に楽しめる。
「ドラム」は単調であるがやや難しい。
「ブラス1」「ブラス2」「シンセパッド」「ティンパニー」はヒマ。
曲名 | アーティスト | 投稿者 | 試聴 |
---|---|---|---|
女々しくて | ゴールデンボンバー | ターミヤだよね | リンク |
曲自体がだいぶお馴染みだしダウンロード数も多いのでいまさら感はあるが、「女々しくて」の部分を皆で連打するのはやはり楽しいと思う。
「クラリネット」「ピアノ」「D・ギター」はヒマなのでそれ以外がオススメ。難易度がそれなりに高いものがあるので難易度表記にも注意。
曲名 | アーティスト | 投稿者 | 試聴 |
---|---|---|---|
ブラックアウト | 東京事変 | へいおまち | リンク |
リズム変化は終盤に部分的に少しあるだけで全体のプレイには特に支障がないレベル。
全体的に難易度は高めで、特に「ODギター1」は表記に反して難しく、「ポップドラム」は表記通りに難しい。「ボンゴセット」はややヒマなのでそれ以外がオススメ。
曲名 | アーティスト | 投稿者 | 試聴 |
---|---|---|---|
賽 | Kagrra, | ゆげぼくじょう | リンク |
弾いてて楽しい曲ではあるが、ヒマなパートも多いので楽器選択に注意。
「シャクハチ1」「ストリングス」「スラップベース」「ロックドラム」がオススメ。
曲名 | アーティスト | 投稿者 | 試聴 |
---|---|---|---|
もしかしてだけど | どぶろっく | トイドラ | リンク |
ネタ曲ではあるが、難易度がそれなりに高めで弾いてて楽しいパートが多い。ヒマなパートも多いのでやはり楽器選択に注意。
「テナーソックス」と「ロックオルガン」がヒマなのでそれ以外がオススメ。メロディ以外のパートは難易度がやや高め。
曲名 | アーティスト | 投稿者 | 試聴 |
---|---|---|---|
サムライハート(Some Like It Hot!!) | SPYAIR | かつひで | リンク |
ヒマなパートと難易度が高いパートが混在しているので曲選択に特に注意。
「ソプラノサックス1」「ピックベース」「Dギター3」はやや難易度が高いがオススメ。
「Dギター1」「シンセリード1」「ロックドラム」は難易度がかなり高め。
「ボンゴセット」はクラップを存分に堪能できるものの、サビでヒマ。ミス扱いにはならないのでサビで適当にクラップを鳴らして楽しめるパートとも言える。
「ソプラノサックス2」「Dギター2」「シンセリード2」はヒマ。
また、サムライハートに関しては投稿者によってテンポ変化があって弾きづらいものがあるので投稿者に特に注意。
曲名 | アーティスト | 投稿者 | 試聴 |
---|---|---|---|
千川通りは夕風だった | LUNKHEAD | kira | なし |
全体的に難易度が高めだが楽しめるレベルだと思う。パート数が7つしかないのでその点は注意。
「ODギター3」がヒマなのでそれ以外がオススメ。
曲名 | アーティスト | 投稿者 | 試聴 |
---|---|---|---|
コンプリケイション | ROOKiEZ is PUNK'D | がし | リンク |
難易度が普通めのパートが多い。
「ソプラノサックス」「ODギター2」「Dギター」「ストリングス1」「ストリングス2」「ピックベース」は普通に楽しめる。
「ODギター1」「ロックドラム」はやや難しい。
「トランペット」と「ストリングス3」はややヒマなので非推奨。
=その他=
ある程度曲をダウンロードして好きな曲が出てきたら、その投稿者で検索して曲を探すのがオススメ。「同じアーティストの他の曲」であったり「アニメのOP・ED曲メイン」であったり、ある種の傾向が決まっていることが多く、その部分が自分の好みと一致する可能性が高い。
SiONでハナウタ機能(音高検出)を作る
=前置き=
バンブラで耳コピや作曲をやっていると「原曲を聴くorメロディを思いつく→ハナウタでその音を出す→ハナウタ機能で実際の音(音高)を見つける」という流れで作るのがラクなんだけど、バンブラPではハナウタ機能がなくなってしまったのでできなくなった。Androidで音高を検出するだけのアプリもあるものの、自分の作曲ツールにもこの機能が欲しかったのでこれを作って組み込んでみた。というわけでそこらへんのメモ。
ちなみに作曲ツール自体はそれなりに形になってきたらなんらかの形で公開する予定。
=ライブラリ=
作曲ツールはAdobe Airで作っており、音を出すのにSiONを使わせてもらっている。で、直接音高をとる機能はないものの、音高をとるのに必要なフーリエ変換をサポートしている(org.si.utils.FFT)ので、ここではこれを使う。
=アルゴリズム=
ハナウタから音高をとるには、音声データをもとに「一番大きい音を出している周波数」を求めれば良い。音声データそのものは周波数が合成されたものなのでそのままでは「一番大きい音を出している周波数」はとれないけれど、音声データをフーリエ変換すると周波数ごとに分離できるため「一番大きい音を出している周波数」を求めることができる。
ということで、ハナウタから音高をとる手順は以下のようになる。
- マイクから音声データを取得する
- 音声データをフーリエ変換する
- 変換後の周波数データから一番大きいものを選ぶ
- 一番大きい部分をHzとして計算する
- 求めたHzに対応する音高(音名)を求める
特にフーリエ変換自体については説明しないけど、基礎的な理解としてはフーリエ変換の本質が役に立つかと思う。
Hzから音高(音名)を計算する部分は以下のような感じ。
- ラ=440 Hzとする
- 1オクターブ上がると2倍の880 Hzとなる
- 1オクターブ下がると半分の220 Hzとなる
- ラから次のラまでは12段階(12半音)あるので、1段階につき「2の12乗根」倍になっている
- 実際の音(Hz)がラ(440 Hz)の何倍になっているかを求め、それが「2の12乗根」の何回分になっているか求める
実際には今回の用途では音名さえわかれば十分なので、220 Hz〜440 Hzに収まるように事前に計算してから「220 Hzの何倍になっているか」→「対数をとって何回分か」を調べて求める。
=コード=
実際のコードはだいたい以下のような感じ。
ただ、論理的には正しくない場所が2箇所あるので注意。
1つ目は音の大きさを「getIntensity」で取得している部分。Intensityは「音の大きさ(実部)と位相のズレの大きさ(虚部)」の両方を合成したものであり、位相のズレの方は本来は不要。ただ、あれこれ試したところ、Intensityの方がキレイに目的の音高を取得しやすかったのでこの方式を採用している。
2つ目はHzを求めている部分。mic.rateの単位はkHzなので実際には1000倍すべきだったりあれこれするが、わりと複雑なのでとりあえず合致するようにやや強引に計算している。ここはマイクから与えられるデータが増減したりSOURCE_LENを増減させたりすると正しくなくなるかもしれない。が、手元での検証が難しいので正確なところは保留にしている。(ハナウタ機能は自分の端末で正常に動けばひとまず十分なので、ひとまずここまでで置いておいて他の機能の作成に入りたかった)
import flash.events.*; import flash.media.*; import flash.utils.*; import org.si.utils.FFT; class Hanauta { //==Const== //Param //- データの大きさ //- マイクのサンプリングデータの大きさ(float:4byte×SOURCE_LENがByteArray.lengthと一致するようにしておく) static public const SOURCE_LEN:int = 1024; //==Var== //FFT //- フーリエ変換を行う本体 public var fft:FFT; //- 音声データ public var src:Vector.<Number>; //- 変換後のデータの実部(音量に相当) public var dst:Vector.<Number>; //Mic //- マイク public var mic:Microphone; //==Function== //Constructor public function Hanauta(){ //FFT Init_FFT(); //Mic Init_Mic(); } //Init : FFT public function Init_FFT():void{ fft = new FFT(SOURCE_LEN); src = new Vector.<Number>(SOURCE_LEN); dst = new Vector.<Number>(SOURCE_LEN/2); } //Init : Mic public function Init_Mic():void{ mic = Microphone.getMicrophone(); if(mic != null){ //サンプリングレート //- 低いほうが分解能は高くなるらしい(Hzの計算時に影響) //mic.rate = 44;//44,100 Hz //mic.rate = 8;//8,000 Hz mic.rate = 5;//5,512Hz //mic.rate = 1;//test:ムリっぽい //どれくらいの音量から取得するか //- 低いと環境音にも反応してしまうので用途に応じて要調整 mic.setSilenceLevel(4); //マイクのデータを受け取るコールバックの設定 mic.addEventListener("sampleData", OnSampleData); } } //Callback : Mic public function OnSampleData(e:SampleDataEvent):void { var buf_num:int = e.data.bytesAvailable / 4;//float:4byteの格納数 //マイクのデータをフーリエ変換に回す for(e.data.position = 0; 0 < e.data.bytesAvailable; ) { Exec_FFT(e.data); break;//1回だけで終わらせてみる(音高とるだけならそれで十分なはず) } //音程を求める Calc_Note(buf_num); } //Exec : FFT public function Exec_FFT(data:ByteArray) : void { for(var i:int = 0; i < SOURCE_LEN; ++i){ src[i] = data.readFloat(); } fft.setData(src).calcFFT();//高速フーリエ変換 } //Calc : Note public function Calc_Note(buf_num:int):void{ //dstに変換後のデータを入れる //- 実部ではなくIntensityを使ってるのは、こちらの方がキレイに音高を取れることが多かったため fft.getIntensity(dst); //一番大きい部分を取得 var max:Number = 10;//ノイズな反応を除去するため、ここでも閾値を設定してみる var index:int = -1; for(var i:int = 0; i < dst.length; ++i){ if(max < dst[i]){ max = dst[i]; index = i; } } //閾値を超える部分がなければムシ if(index < 0){ return; } //Hzを求める //- サンプリング時の周波数 //- * フーリエ変換での周波数 //- * 実際のサンプリングデータ数/フーリエ変換に渡したデータ数 //- = 本来の周波数、のはず(ここはあまり自信ない) var mic_rate:Number = mic.rate; //- rate=5の時は厳密には5.512でだいぶ違うので補正(他はそこまで大きくは違わない) //-- 参考:http://help.adobe.com/ja_JP/FlashPlatform/reference/actionscript/3/flash/media/Microphone.html#rate if(mic_rate == 5){mic_rate = 5.512;} var hz:Number = mic_rate * index * (buf_num/SOURCE_LEN); //ラ=440 Hz として音名計算 //- 880 Hz, 220 Hzがそれぞれ1オクターブ上下となる var key_name:String = ""; { const KEY_NAME_ARR:Vector.<String> = new <String>[ "A", "B♭", "B", "C", "D♭", "D", "E♭", "E", "F", "G♭", "G", "A♭", ]; //Hzを操作して音名を検出 var base_hz:Number = hz; if(base_hz <= 10){base_hz = 220;}//低い値の精度はどうせ高くないので、適当に値を設定してしまう(エラー対策) //まずは220Hz〜440Hzに収めてみる while(base_hz < 220){base_hz *= 2;} while(440 <= base_hz){base_hz /= 2;} //これでbase_hz/220は1〜2の係数となり、Log2をとることで平均律としてのratioが求まる //Log2(base_hz / 220) = Log(base_hz / 220) / Log(2) var ratio:Number = Math.log(base_hz / 220) / Math.log(2); //1/12ごとに半音上がるものとして捉えることができるので、12段階にすることで最終的な音名を求める var key_index:int = int(ratio*12 + 0.5) % 12; key_name = KEY_NAME_ARR[key_index]; } //あとはkey_nameを表示すればOK //- Hzも併記するとわかりやすい //- さらにdstをグラフ化して表示すると波形データとしてもわかりやすい } }
=まとめ=
というわけでフーリエ変換でハナウタから音高を取得する機能を作った。コードのところで書いたように他の端末で動くかは怪しい部分があるので、ツールを公開する時はもうちょっとここら辺の検証を済ませてからにしたい。