新しいPlaneを使って画像のトランジション効果のようなものを作ってみた。
参考はClockMaker.jp "Papervision3D演出サンプルNo.04:細切れフォトグラフ"
package { import alternativa.engine3d.core.Object3D; import alternativa.engine3d.materials.TextureMaterial; import alternativa.engine3d.resources.BitmapTextureResource; import flash.display.BitmapData; import flash.events.Event; import flash.geom.Point; import org.libspark.betweenas3.BetweenAS3; import org.libspark.betweenas3.easing.Expo; import org.libspark.betweenas3.tweens.ITween; /** * ... * @author */ [SWF(width="550",height="400")] public class Main extends AlBasicView { [Embed(source="tree.jpg")] static private const EmbedTexture1:Class; [Embed(source = "sky.jpg")] static private const EmbedTexture2:Class; // private const RAD:Number = Math.PI / 180; private var division:uint = 20; private var firstTween:ITween; private var roundTween1:ITween; private var roundTween2:ITween; // public function Main():void { stage.frameRate = 60; camera.view.backgroundColor = 0x333333; var controller:RoundCameraController = new RoundCameraController(camera, stage); controller.setCameraAt(600, -70, 90); // var sourceBd:BitmapData = new EmbedTexture1().bitmapData; var resizedBd:BitmapData = resizeBitmapData(sourceBd); var perW:Number = sourceBd.width / resizedBd.width; //max u coordinate of texture var perH:Number = sourceBd.height / resizedBd.height; var material:TextureMaterial = new TextureMaterial(new BitmapTextureResource(resizedBd)); var resizedBd2:BitmapData = resizeBitmapData(new EmbedTexture2().bitmapData); var material2:TextureMaterial = new TextureMaterial(new BitmapTextureResource(resizedBd2)); // var container:Object3D = new Object3D(); container.rotationX = RAD * 90; scene.addChild(container); // var parallel:Array = new Array(division * division * 2); //for parallel tween var parallelR1:Array = new Array(division * division * 2); var parallelR2:Array = new Array(division * division * 2); // var tipW:uint = sourceBd.width / division; //plane width var tipH:uint = sourceBd.height / division; var dU:Number = perW / division; //plane texture width var dV:Number = perH / division; var baseX:Number = (-sourceBd.width + tipW) / 2; //min x coordinate of plane var baseY:Number = (sourceBd.height - tipH) / 2; var uStart:Number; var uEnd:Number; var vStart:Number; var vEnd:Number; for (var i:int = 0; i < division; i++){ uStart = i * dU; uEnd = (i + 1) * dU; for (var j:int = 0; j < division; j++){ var tip:Plane = new Plane(tipW, tipH, 1, 1, true, material); tip.getSurface(1).material = material2; vStart = j * dV; vEnd = (j + 1) * dV; tip.setUV(uStart, uEnd, vStart, vEnd); tip.x = baseX + i * tipW; tip.y = baseY - j * tipH; container.addChild(tip); // var ii:int = (division >> 1) - i; var jj:int = (division >> 1) - j; var delay:Number = Math.sqrt(ii * ii + jj * jj) * 0.1; //determin delay from distance var num:uint = (i * division + j) << 1; parallel[num] = BetweenAS3.delay(BetweenAS3.from(tip, {z: 3000}, 0.2, Expo.easeOut), delay * 2); parallel[uint(num + 1)] = BetweenAS3.delay(BetweenAS3.from(tip, {rotationY: 540 * RAD}, 1), delay * 2 + 1); parallelR1[num] = BetweenAS3.delay(BetweenAS3.tween(tip, {rotationY: 540 * RAD}, {rotationY: 0 * RAD}, 0.6), delay); parallelR1[uint(num + 1)] = BetweenAS3.delay(BetweenAS3.func(setUV, [tip, perW - uEnd, perW - uStart, vStart, vEnd]), delay + 0.3); parallelR2[num] = BetweenAS3.delay(BetweenAS3.tween(tip, {rotationY: 0 * RAD}, {rotationY: 540 * RAD}, 0.6), delay); parallelR2[uint(num + 1)] = BetweenAS3.delay(BetweenAS3.func(setUV, [tip, uStart, uEnd, vStart, vEnd]), delay + 0.3); } } firstTween = BetweenAS3.parallelTweens(parallel); firstTween.onComplete = onComplete; roundTween1 = BetweenAS3.delay(BetweenAS3.parallelTweens(parallelR1), 3); roundTween1.onComplete = play2; roundTween2 = BetweenAS3.delay(BetweenAS3.parallelTweens(parallelR2), 3); roundTween2.onComplete = onComplete; // request(); } override public function onContextCreate(e:Event):void { super.onContextCreate(e); firstTween.play(); } private function onComplete():void { roundTween1.play(); } private function play2():void { roundTween2.play(); } private function setUV(tip:Plane, uStart:Number, uEnd:Number, vStart:Number, vEnd:Number):void { tip.setUV(uStart, uEnd, vStart, vEnd); tip.geometry.upload(context3D); } //utility private function resizeBitmapData(data:BitmapData):BitmapData {//resize BitmapData to var length:uint = toPowerOfTwo(data.width, data.height); var bd:BitmapData = new BitmapData(length, length, false, 0x0); bd.copyPixels(data, data.rect, new Point()); return bd; } private function toPowerOfTwo(x:uint, y:uint):uint { if (y > x){ x = y; } if ((x & (x - 1))){ var i:uint = 1; while (i < x){ i <<= 1; } x = i; } return x; } } }
ちょっと長い。
BetweenAS3の詳しい使い方は省略。
インスタンス変数はラジアン変換単位の定数RAD、写真の一方向分割数division、トゥイーン用の変数(最初の登場、表→裏、裏→表)が3つ。
それから埋め込みの画像が二枚。
最初のブロックはいつもどおりなので省略。setCameraAt()で適当な位置にカメラを持ってきてる。
var sourceBd:BitmapData = new EmbedTexture1().bitmapData;
var resizedBd:BitmapData = resizeBitmapData(sourceBd);
次のブロックではまずsourceBdとして画像1のBitmapDataをもってくる。
下のほうに作ってあるresizeBitmapData()メソッドでそれをリサイズ。
private function resizeBitmapData(data:BitmapData):BitmapData {//resize BitmapData to
var length:uint = toPowerOfTwo(data.width, data.height);
var bd:BitmapData = new BitmapData(length, length, true, 0x0);
bd.copyPixels(data, data.rect, new Point());
return bd;
}
Molehillではアップロードするテクスチャは両辺とも2の乗数のピクセルでなくてはならない。
あと軽く調べたところ3Dでは正方形にするのが一般的らしいのでついでにそうする。
toPowerOfTwo()は引数2つのuintのうち、大きいほうを基準としてその数以上の最小の2の乗数を返すメソッド。内容は省略。
ここに画像のwidthとheightを渡してテクスチャにするのに最適なlengthを得る。
このlength*lengthのBitmapDataを新たに作成し、元画像をcopyPixelsしたものを返す。
整形っていってもscaleとかがかわるわけじゃなく、2の乗数の正方形の額縁にいれるだけ。
var perW:Number = sourceBd.width / resizedBd.width; //max u coordinate of texture
var perH:Number = sourceBd.height / resizedBd.height;
var material:TextureMaterial = new TextureMaterial(new BitmapTextureResource(resizedBd));
var resizedBd2:BitmapData = resizeBitmapData(new EmbedTexture2().bitmapData);
var material2:TextureMaterial = new TextureMaterial(new BitmapTextureResource(resizedBd2));
リサイズされたresizedBdに対して元画像の幅と高さがそれぞれどのくらいの割合なのかをperWとperHにいれておく。
たとえば元画像が800*600だったらperWは800/1024、perHは600/1024だ。
つまり最高値は1。uv座標で使うっぽいよね。
リサイズした画像からBitmapTextureResource→TextureMaterialを作成。
さらに画像2もリサイズしてマテリアルを作っとく。
ちなみに今回の場合は画像1と2のアスペクト比が異なるとよろしくないのでそろえよう。
var container:Object3D = new Object3D();
container.rotationX = RAD * 90;
scene.addChild(container);
分割したPlaneが入るコンテナを作っておく。
Planeは最初上向いてるからこっち向けとく。
var parallel:Array = new Array(division * division * 2); //for parallel tween
var parallelR1:Array = new Array(division * division * 2);
var parallelR2:Array = new Array(division * division * 2);
パラレルトゥイーン用の配列を作っとく。Vectorじゃないんだね...
サイズはPlaneの数なのでdivision*division。
だけど諸事情により1つのPlaneのトゥイーンにつき2ついるので2倍しとく。
var tipW:uint = sourceBd.width / division; //plane width
var tipH:uint = sourceBd.height / division;
var dU:Number = perW / division; //plane texture width
var dV:Number = perH / division;
var baseX:Number = (-sourceBd.width + tipW) / 2; //min x coordinate of plane
var baseY:Number = (sourceBd.height - tipH) / 2;
var uStart:Number;
var uEnd:Number;
var vStart:Number;
var vEnd:Number;
いよいよPlaneを作る準備。
デカPlaneの大きさは元画像に合わせるので、1枚のPlaneの幅tipW、高さtipHは元画像のそれを分割数で割ったもの。
uvの観点から見ると、デカPlaneのuの最小値は0で最大値はさっき求めたperW。
だから1枚のPlaneのu幅はperWを分割数で割ったもの。
ミニPlane(以下チップ)をコンテナ内に配置するわけだけど、一番左のチップのx座標を求める。
planeの中心(基準点)は真ん中。
だから一番左の頂点のx座標は-sourceBd.width/2なので左端のチップのx座標はそれにチップの幅を2で割ったものを足す。
一番の上のチップのy座標は符号が反転するけど似たような求め方。
uStartとかは後で使うけど変数宣言だけしとく。つかいみちは新PlaneのメソッドsetUV()。
for (var i:int = 0; i < division; i++){
uStart = i * dU;
uEnd = (i + 1) * dU;
for (var j:int = 0; j < division; j++){
var tip:Plane = new Plane(tipW, tipH, 1, 1, true, material);
tip.getSurface(1).material = material2;
vStart = j * dV;
vEnd = (j + 1) * dV;
tip.setUV(uStart, uEnd, vStart, vEnd);
tip.x = baseX + i * tipW;
tip.y = baseY - j * tipH;
container.addChild(tip);
いよいよめんどくさい部分。
まず幅でループ。この時点でチップのu座標は決まる。
左端の列のチップのu座標は左端が0、右端がさっき求めたチップのu幅dU。
次の列は左端がdU、右端がdU+dU。
次の列は左端が2dU、右端が2dU+dU。
つまりuStartはi*dU、uEndは(i+1)*dUとなる。
次に高さでループ。tipW*tipHのPlaneを作ります。両面で。マテリアルはとりあえず1枚目のに設定しとく。
裏面のSurafaceを取得して2枚目のマテリアルを設定。
u座標と同じようにvStartとvEndも求める。
ここからこないだつけたしたメソッドを使って各チップのuv座標を設定する。
x座標は左端のbaseXを基準としてi*tipWを足したもの。yも符号は反転するが同じ。
でコンテナに入れる。
var ii:int = (division >> 1) - i;
var jj:int = (division >> 1) - j;
var delay:Number = Math.sqrt(ii * ii + jj * jj) * 0.1; //determin delay from distance
var num:uint = (i * division + j) << 1;
parallel[num] = BetweenAS3.delay(BetweenAS3.from(tip, {z: 3000}, 0.2, Expo.easeOut), delay * 2);
parallel[uint(num + 1)] = BetweenAS3.delay(BetweenAS3.from(tip, {rotationY: 540 * RAD}, 1), delay * 2 + 1);
parallelR1[num] = BetweenAS3.delay(BetweenAS3.tween(tip, {rotationY: 540 * RAD}, {rotationY: 0 * RAD}, 0.6), delay);
parallelR1[uint(num + 1)] = BetweenAS3.delay(BetweenAS3.func(setUV, [tip, perW - uEnd, perW - uStart, vStart, vEnd]), delay + 0.3);
parallelR2[num] = BetweenAS3.delay(BetweenAS3.tween(tip, {rotationY: 0 * RAD}, {rotationY: 540 * RAD}, 0.6), delay);
parallelR2[uint(num + 1)] = BetweenAS3.delay(BetweenAS3.func(setUV, [tip, uStart, uEnd, vStart, vEnd]), delay + 0.3)
}
};
つぎはトウィーンの設定。
トウィーンするときのdelay(実行の遅延)をi,jを使った中心からの距離で求める。
中心点は(division/2 ,division/2)なのでそこからの距離を求め、適当な数0.1かけたものを基準delayとする。
ijの二次元を一次元に落とし込むためnumを求める。
まず登場時のトウィーンをparralelにいれてく。
1枚のチップがz=3000からz=0まで0.2秒かけてトウィーン。
と同時に1秒後にrotationY=540からrotationY=0まで1秒かけてトウィーン。
次に回転するトウィーンをparallelR1に入れる。
1枚のチップがrotationY=0からrotationY=540まで0.6秒かけてトウィーン。1.5回転分。
ここがミソなんだけど、このまま回転して反対側を見せると、モザイク調のおかしな画像になってしまう。
最初のトゥイーンのときにちらっとモザイクっぽく見えると思う。あれになっちゃう。
なぜかっていうと例えば一番左上のチップは、裏面の画像は本来右上にあるべきものだからだ。
そのまま裏から見る分にはきちんと表示されるが、 分割して1枚1枚反転するとuが固定されているせいでダメになる。
そこで回転中にuv座標を入れ替えてしまおう。回転が始まって0.3秒後、実は各チップの画像は変わっている。
たぶん気づく人はいない。
最初から裏面は裏替えった時にちゃんと見えるようにすればいいじゃんと思うかもしれないが、そうすると裏から見るとモザイクになってしまう。裏から見てもきれいに見せたいの。実際マウスで裏に回ってもちゃんと表示されてるはず。
v座標はそのままでいいので、uだけ変えよう。
左端のチップ(i=0)は右端になってほしいので、uEndはperW、uStartはperW-dUだ。
左から2番目のチップ(i=1)は右から2番目になるのでuEndはperW-dU、uStartはperW-dU*2だ。
このことからuStart=perW-(i+1)*dU、uEnd=perW-i*dUとなる。これらはさっきのuStartとuEndで表現でkきる。
uStart=perW-uEnd、uEnd=perW-uStart。無駄な計算は減らすのが常。
BetweenAs3.func()は実行する関数と引数を設定できる。これにdelayを設定しつつ
Planeとuvパラメータを受け取ってそのPlaneにsetUV()し、即時アップロード(これも重要。Geometry変えたらupload()だよ)するメソッドsetUVを作ってそこに渡す。
逆回りのトウィーンも同じように設定。
firstTween = BetweenAS3.parallelTweens(parallel);
firstTween.onComplete = onComplete;
roundTween1 = BetweenAS3.delay(BetweenAS3.parallelTweens(parallelR1), 3);
roundTween1.onComplete = play2;
roundTween2 = BetweenAS3.delay(BetweenAS3.parallelTweens(parallelR2), 3);
roundTween2.onComplete = onComplete;
最後に配列に入れた同時実行トウィーンをBetweenAS3.parallelTweens()で作る。
onCompleteを設定してfirstTween→roundTween1→roundTween2→roundTween1→以下ループ
になるようにすれば完成。
もう1つ大事な最初のトウィーンの実行タイミング。
Context3Dが作られて、表示するResourceがupload()された後だよ!
今回作ったトランジションの特徴は、画像を分割しないこと。
1枚(表裏2枚だけど)の画像リソースからPlaneのuv座標を設定することで分割してるように見える。
最初に分割しちゃってdivision*division枚upload()→回転時に動的にmaterial差し替え(ノーラグ)でもいいんだけど、まあuvのお勉強。
分割してもしなくても1枚の画像リソースだからGPUの資源は同じだと思う。
そしてuの入れ替えで画像が変わっても回転中で気づかないこと。
裏から見てもちゃんと見えること。
フレームレートはあんましでないなあ。。
Author:9ballsyn
ActionScriptについて
最近はMolehill