[PV3D]ジンバルロックを体験してみよう!

[PV3D]ジンバルロックを体験してみよう!3Dがある程度できるようになってくると、クオータニオンって言葉を見かけるようになってくる。名前がカッコいいしなんだかプロっぽい。ふむふむジンバルロックを回避できるすばらしいものだと。でも、、、ジンバルロックってなに?

http://ja.wikipedia.org/wiki/ジンバル
http://www.radiumsoftware.com/0501.html
http://marupeke296.com/DXG_No10_Quaternion.html

いろいろ読んでいるとなんとなくわかったような気がしてくるけど、実際に動かしてみないといまいちコレだ!って感じにはならない。そこで作ってみた。

Get Adobe Flash player


//Gimbal lock

★ジンバルロックを体験してみよう!

0.左上のrotationXYZの立方体に注目
1.初期状態(又はReset後)に「Y+=30」を三回クリックして、Y軸90度回転をする。
2.「Z+=30」をクリックしてみる。Z軸回転したことを確認。
3.「Z-=30」をクリックして戻す。
4.「X-=30」をクリックすると、、、アレレ!「Z+=30」と同じZ軸回転しちゃうぞ!
これがジンバルロックであります。
いろいろ回転させていろいろ試してくださいませ。

課題
ジャイロスコープを作ったらもうちょっとわかりやすくなるよね、、、。そのうち、、、。
→作ってみたhttp://www.mztm.jp/2009/07/24/pv3dジンバルロックを体験してみよう!2/
あと、FlashPlayer10のビルトイン関数のMatrix3Dではどうなるかのほうが
一般性があるよね。遠くないうちにやります。
→作ってみたhttp://www.mztm.jp/2009/07/31/ジンバルロックを体験してみよう!3(ビルトイン/
=============================

Papervision3Dにおいて、rotationX,localRotationX,Quaternionの使い分け。

左下のボタン、一回クリックごとにそれぞれの値を変化させる。

■rotationXYZ
簡単なものなら、rotationXで足りるものもあるんだろうけど、自在に回転させるとなると、どうしてもジンバルロックが問題になる。
値がわかりやすいってのが強み。

■localRotationXYZ
localRotationXってのは、なんだろうと思ってたけど、比較すると良くわかった。
ジンバルロック回避だけなら、これでなんとかなっちゃうこともあるかも。

■Quaternion
本命はやっぱりQuaternion。
ジンバルロックは無いし、画面上と同じX,Y軸回転をするからUIとの統合もしやすい。
内部的にはちょっとめんどくさくなるのは嫌だけど。

▼Wonderfl
http://wonderfl.net/code/2d5664a811359bb65aeebb97422f4668250a74c9

▼ActionScript AS3(FP9)
[sourcecode language=”as3″]
/*
* ★ジンバルロックを体験してみよう!
*
* 0.左上のrotationXYZの立方体に注目
* 1.初期状態(又はReset後)に「Y+=30」を三回クリックして、Y軸90度回転をする。
* 2.「Z+=30」をクリックしてみる。Z軸回転したことを確認。
* 3.「Z-=30」をクリックして戻す。
* 4.「X-=30」をクリックすると、、、アレレ!「Z+=30」と同じZ軸回転しちゃうぞ!
* これがジンバルロックであります。
* いろいろ回転させていろいろ試してくださいませ。
*
* 課題
* ジャイロスコープを作ったらもうちょっとわかりやすくなるよね、、、。そのうち、、、。
* あと、FlashPlayer10のビルトイン関数のMatrix3Dではどうなるかのほうが
* 一般性があるよね。遠くないうちにやります。
* =============================
*
* Papervision3Dにおいて、rotationX,localRotationX,Quaternion
* の使い分け。
*
* 左下のボタン、一回クリックごとにそれぞれの値を変化させる。
*
* ■rotationXYZ
* 簡単なものなら、rotationXで足りるものもあるんだろうけど、
* 自在に回転させるとなると、どうしてもジンバルロックが問題になる。
* 値がわかりやすいってのが強み。
*
* ■localRotationXYZ
* localRotationXってのは、なんだろうと思ってたけど、比較すると良くわかった。
* ジンバルロック回避だけなら、これでなんとかなっちゃうこともあるかも。
*
* ■Quaternion
* 本命はやっぱりQuaternion。
* ジンバルロックは無いし、画面上と同じX,Y軸回転をするからUIとの
* 統合もしやすい。
* 内部的にはちょっとめんどくさくなるのは嫌だけど。
*
*
* */

package
{
import flash.display.Sprite;
import flash.events.Event;
import flash.events.MouseEvent;
import flash.text.TextField;
import org.papervision3d.lights.PointLight3D;
import org.papervision3d.materials.shadematerials.FlatShadeMaterial;
import org.papervision3d.objects.primitives.Cube;
import org.papervision3d.view.BasicView;
import org.papervision3d.materials.utils.MaterialsList;
import org.papervision3d.objects.DisplayObject3D;

import flash.display.SimpleButton;
[SWF(width = "465", height = "465", frameRate = "30", backgroundColor = "#000000")]
public class Test3 extends Sprite{
private var dodai:DisplayObject3D = new DisplayObject3D();
public function Test3():void {
//立方体を生成
var c0:CubeView = new CubeView();
c0.x = -110;
c0.y = -110;
addChild(c0);
var c1:CubeView = new CubeView();
c1.x = 110;
c1.y = -110;
addChild(c1);
var c2:CubeView = new CubeView();
c2.x = 0;
c2.y = 65;
addChild(c2);

//Button
var btnX1:SimpleButton = Create.newSimpleButton([5,396,60,20,"X+=30"]);
var btnX0:SimpleButton = Create.newSimpleButton([70, 396, 60, 20, "X-=30"]);
var btnY1:SimpleButton = Create.newSimpleButton([5,418,60,20,"Y+=30"]);
var btnY0:SimpleButton = Create.newSimpleButton([70, 418, 60, 20, "Y-=30"]);
var btnZ1:SimpleButton = Create.newSimpleButton([5,440,60,20,"Z+=30"]);
var btnZ0:SimpleButton = Create.newSimpleButton([70, 440, 60, 20, "Z-=30"]);
var reset:SimpleButton = Create.newSimpleButton([400, 440, 60, 20, "Reset"]);
btnX1.name = "X1";
btnX0.name = "X0";
btnY1.name = "Y1";
btnY0.name = "Y0";
btnZ1.name = "Z1";
btnZ0.name = "Z0";
btnX1.addEventListener(MouseEvent.CLICK, CLICK);
btnX0.addEventListener(MouseEvent.CLICK, CLICK);
btnY1.addEventListener(MouseEvent.CLICK, CLICK);
btnY0.addEventListener(MouseEvent.CLICK, CLICK);
btnZ1.addEventListener(MouseEvent.CLICK, CLICK);
btnZ0.addEventListener(MouseEvent.CLICK, CLICK);
reset.addEventListener(MouseEvent.CLICK, resetCLICK);
addChild(btnX1);
addChild(btnX0);
addChild(btnY1);
addChild(btnY0);
addChild(btnZ1);
addChild(btnZ0);
addChild(reset);

//TextField
var tfc0:TextField = Create.newTextField([90, 110, 100, 20,"rotationXYZ",0xFFFFFF],[["selectable",false]]);
var tfc1:TextField = Create.newTextField([295, 110, 100, 20,"localRotationXYZ",0xFFFFFF],[["selectable",false]]);
var tfc2:TextField = Create.newTextField([200, 290, 100, 20,"Quaternion",0xFFFFFF],[["selectable",false]]);
addChild(tfc0);
addChild(tfc1);
addChild(tfc2);

//rotationXYZ
var tf0x:TextField = Create.newTextField([5, 5, 120, 20, "rotationX:0", 0xFFFFFF],[["selectable",false]]);
var tf0y:TextField = Create.newTextField([5, 25, 120, 20, "rotationY:0", 0xFFFFFF],[["selectable",false]]);
var tf0z:TextField = Create.newTextField([5, 45, 120, 20, "rotationZ:0", 0xFFFFFF],[["selectable",false]]);
addChild(tf0x)
addChild(tf0y)
addChild(tf0z)
//localRotationXYZ
var tf1x:TextField = Create.newTextField([340, 5, 120, 20, "rotationX:0", 0xFFFFFF],[["selectable",false]]);
var tf1y:TextField = Create.newTextField([340, 25, 120, 20, "rotationY:0", 0xFFFFFF],[["selectable",false]]);
var tf1z:TextField = Create.newTextField([340, 45, 120, 20, "rotationZ:0", 0xFFFFFF],[["selectable",false]]);
addChild(tf1x)
addChild(tf1y)
addChild(tf1z)
//Quaternion
var tf2x:TextField = Create.newTextField([300, 315, 120, 20, "rotationX:0", 0xFFFFFF],[["selectable",false]]);
var tf2y:TextField = Create.newTextField([300, 335, 120, 20, "rotationY:0", 0xFFFFFF],[["selectable",false]]);
var tf2z:TextField = Create.newTextField([300, 355, 120, 20, "rotationZ:0", 0xFFFFFF],[["selectable",false]]);
addChild(tf2x)
addChild(tf2y)
addChild(tf2z)

function CLICK(e:MouseEvent = null):void {
if (count > 0) { return };
var axis:String = e.currentTarget.name.substr(0,1);
var d:Number = Number(e.currentTarget.name.substr(1, 1))*60-30;
if ("X" == axis) {
toRotationXYZ[0] = d;
}else if ("Y" == axis) {
toRotationXYZ[1] = d;
}else if ("Z" == axis) {
toRotationXYZ[2] = d;
}
count = 10;
}
function resetCLICK(e:MouseEvent):void {
c0.reset();
c1.reset();
c2.reset();
count = 1;
ENTER_FRAME(e);
tf0x.text = tf1x.text = tf2x.text = "rotationX:0";
tf0y.text = tf1y.text = tf2y.text = "rotationY:0";
tf0z.text = tf1z.text = tf2z.text = "rotationZ:0";
}

var toRotationXYZ:Array = [0,0,0];
var count:int = 0;

addEventListener(Event.ENTER_FRAME, ENTER_FRAME);
function ENTER_FRAME(e:Event):void {
if (count <= 0) { return };
count –;
var _x:Number = toRotationXYZ[0] * 0.1;
var _y:Number = toRotationXYZ[1] * 0.1;
var _z:Number = toRotationXYZ[2] * 0.1;
if (count == 0) {
toRotationXYZ[0] = toRotationXYZ[1] = toRotationXYZ[2] = 0;
}
//左上の立方体に対してはrotationX,Y,Zの値を変更
c0.rotationXYZ(_x, _y, _z);
//右上の立方体に対してはlocalRotationX,Y,Zの値を変更
c1.localRotationXYZ(_x, _y, _z);
//中央下の立方体に対してはQuaternionでX,Y,Zの値を変更
c2.quaternionXYZ(_x, _y, _z);

//回転数表示を更新
tf0x.text = "rX:" + near(c0._cube.rotationX);
tf0y.text = "rY:" + near(c0._cube.rotationY);
tf0z.text = "rZ:" + near(c0._cube.rotationZ);
tf1x.text = "rX:" + near(c1._cube.rotationX);
tf1y.text = "rY:" + near(c1._cube.rotationY);
tf1z.text = "rZ:" + near(c1._cube.rotationZ);
tf2x.text = "rX:" + near(c2._cube.rotationX);
tf2y.text = "rY:" + near(c2._cube.rotationY);
tf2z.text = "rZ:" + near(c2._cube.rotationZ);
}
}
private function near(num:Number):Number {
return Math.round(num*1000000)/ 1000000;
}
}
}

import flash.events.Event;
import org.papervision3d.lights.PointLight3D;
import org.papervision3d.materials.shadematerials.FlatShadeMaterial;
import org.papervision3d.objects.primitives.Cube;
import org.papervision3d.view.BasicView;
import org.papervision3d.materials.utils.MaterialsList;
import org.papervision3d.objects.DisplayObject3D;
import org.papervision3d.core.math.Quaternion;
import org.papervision3d.core.math.Matrix3D;

import flash.display.SimpleButton;

class CubeView extends BasicView{
public var _cube:DisplayObject3D = GetCubeClass.getCube(200);
public function CubeView():void{
scene.addChild(_cube);
startRendering();
}
public function rotationXYZ(degreeX:Number = 0, degreeY:Number = 0, degreeZ:Number = 0):void {
_cube.rotationX += degreeX;
_cube.rotationY += degreeY;
_cube.rotationZ += degreeZ;
}
public function localRotationXYZ(degreeX:Number = 0, degreeY:Number = 0, degreeZ:Number = 0):void {
_cube.localRotationX -= degreeX;
_cube.localRotationY -= degreeY;
_cube.localRotationZ -= degreeZ;
}
public function quaternionXYZ(degreeX:Number = 0,degreeY:Number = 0,degreeZ:Number = 0):void {
var pos1:Quaternion = Quaternion.createFromMatrix(_cube.transform);
var pos2:Quaternion = Quaternion.createFromEuler(degreeY, degreeZ, degreeX, true);
pos2.normalize();
pos2.mult(pos1);
pos2.normalize();
_cube.copyTransform(Matrix3D.quaternion2matrix(pos2.x, pos2.y, pos2.z, pos2.w));
}
public function reset():void {
_cube.copyTransform(new Matrix3D());
_cube.rotationX = _cube.rotationY = _cube.rotationZ = 0;
}
}

class GetCubeClass{
public static function getCube(size:Number = 100):DisplayObject3D {
var light:PointLight3D = new PointLight3D();
var colors_array:Array = [ 0x00FF00, 0xFF0000, 0x00FFFF, 0xFF00FF,0x0000FF, 0xFFFF00 ];
var base:DisplayObject3D = new DisplayObject3D();
var ml:MaterialsList = new MaterialsList( {
front:new FlatShadeMaterial(light, colors_array[0], 0x333333),
back:new FlatShadeMaterial(light, colors_array[1], 0x333333),
right:new FlatShadeMaterial(light, colors_array[2], 0x333333),
left:new FlatShadeMaterial(light, colors_array[3], 0x333333),
top:new FlatShadeMaterial(light, colors_array[4], 0x333333),
bottom:new FlatShadeMaterial(light, colors_array[5], 0x333333)
})
var _cube:Cube = new Cube(ml, size, size, size);
base.addChild(_cube);
ml = new MaterialsList( { all:new FlatShadeMaterial(light, colors_array[2], 0x333333)})
_cube = new Cube(ml, size, 10, 10);
_cube.x = size;
base.addChild(_cube);
ml = new MaterialsList( { all:new FlatShadeMaterial(light, colors_array[3], 0x333333)})
_cube = new Cube(ml, size, 10, 10);
_cube.x = -size;
base.addChild(_cube);
ml = new MaterialsList( { all:new FlatShadeMaterial(light, colors_array[0], 0x333333)})
_cube = new Cube(ml, 10, size, 10);
_cube.z = size;
base.addChild(_cube);
ml = new MaterialsList( { all:new FlatShadeMaterial(light, colors_array[1], 0x333333)})
_cube = new Cube(ml, 10, size, 10);
_cube.z = -size;
base.addChild(_cube);
ml = new MaterialsList( { all:new FlatShadeMaterial(light, colors_array[4], 0x333333)})
_cube = new Cube(ml, 10, 10, size);
_cube.y = size;
base.addChild(_cube);
ml = new MaterialsList( { all:new FlatShadeMaterial(light, colors_array[5], 0x333333)})
_cube = new Cube(ml, 10, 10, size);
_cube.y = -size;
base.addChild(_cube);
return base;
}
}

/////////////

import flash.display.DisplayObject;
import flash.display.Graphics;
import flash.text.TextField;
import flash.text.TextFieldType;
import flash.text.TextFormat;
import flash.display.Sprite;
import flash.display.Shape;
import flash.display.SimpleButton;
//import flash.events.MouseEvent;
//import flash.events.Event;
//import flash.events.KeyboardEvent;

class Create{
public static var defaultTextFormat:TextFormat = new TextFormat();

public static function newSimpleButton(x_y_w_h_txt:Array = null,property:Array=null,graphics:Array=null):SimpleButton{
var upState:Sprite = newSprite([x_y_w_h_txt[0],x_y_w_h_txt[1]],null,[["beginFill",[0xCCCCCC,1]],["drawRoundRect",[0,0,x_y_w_h_txt[2],x_y_w_h_txt[3],8]]]);
upState.addChild(newShape([2,2],null,[["beginFill",[0xE5E5E5,1]],["drawRoundRect",[0,0,x_y_w_h_txt[2]-4,x_y_w_h_txt[3]-4,6]]]))
var overState:Sprite = newSprite([x_y_w_h_txt[0],x_y_w_h_txt[1]],null,[["beginFill",[0xBBBBBB,1]],["drawRoundRect",[0,0,x_y_w_h_txt[2],x_y_w_h_txt[3],8]]]);
overState.addChild(newShape([2,2],null,[["beginFill",[0xEEEEEE,1]],["drawRoundRect",[0,0,x_y_w_h_txt[2]-4,x_y_w_h_txt[3]-4,6]]]))
var downState:Sprite = newSprite([x_y_w_h_txt[0],x_y_w_h_txt[1]],null,[["beginFill",[0xAAAAAA,1]],["drawRoundRect",[0,0,x_y_w_h_txt[2],x_y_w_h_txt[3],8]]]);
downState.addChild(newShape([2,2],null,[["beginFill",[0xDDDDDD,1]],["drawRoundRect",[0,0,x_y_w_h_txt[2]-4,x_y_w_h_txt[3]-4,6]]]))
var hitTestState:Shape = newShape([x_y_w_h_txt[0],x_y_w_h_txt[1]],null,[["beginFill",[0,1]],["drawRoundRect",[0,0,x_y_w_h_txt[2],x_y_w_h_txt[3],8]]]);
if(x_y_w_h_txt[4]){
upState.addChild(newTextField([0,2,x_y_w_h_txt[2],x_y_w_h_txt[3]-2],[["defaultTextFormat",new TextFormat("_sans", null, null, null, null, null, null, null, "center")],["text",x_y_w_h_txt[4]]]));
overState.addChild(newTextField([0,2,x_y_w_h_txt[2],x_y_w_h_txt[3]-2],[["defaultTextFormat",new TextFormat("_sans", null, null, null, null, null, null, null, "center")],["text",x_y_w_h_txt[4]]]));
downState.addChild(newTextField([0,3,x_y_w_h_txt[2],x_y_w_h_txt[3]-3],[["defaultTextFormat",new TextFormat("_sans", null, null, null, null, null, null, null, "center")],["text",x_y_w_h_txt[4]]]));
}
var sb:SimpleButton = new SimpleButton(upState,overState,downState,hitTestState);

return sb;
}

public static function newShape(x_y_w_h_sh:Array = null,property:Array=null,graphics:Array=null):Shape{
var i:int;
var sh:Shape;
if(x_y_w_h_sh && x_y_w_h_sh[4]){
sh = x_y_w_h_sh[4];
}else{
sh = new Shape();
}
if(x_y_w_h_sh){
if (x_y_w_h_sh[0]) { sh.x = x_y_w_h_sh[0] };
if (x_y_w_h_sh[1]) { sh.y = x_y_w_h_sh[1] };
}
if(property){
for (i = 0; i < property.length; i++) {
if(property[i] && property[i].length > 1){
sh[property[i][0]] = property[i][1];
}
}
}
if(graphics){
for (i = 0; i < graphics.length; i++) {
if(graphics[i] && graphics[i].length > 1){
sh.graphics[graphics[i][0]].apply(null, graphics[i][1]);
}
}

}
if(x_y_w_h_sh){
if (x_y_w_h_sh[2]) { sh.width = x_y_w_h_sh[2] };
if (x_y_w_h_sh[3]) { sh.height = x_y_w_h_sh[3] };
}
return sh;
}
public static function newSprite(x_y_w_h_sp:Array = null,property:Array=null,graphics:Array=null,addChild:DisplayObject = null):Sprite{
var i:int;
var sp:Sprite;
if(x_y_w_h_sp && x_y_w_h_sp[4]){
sp = x_y_w_h_sp[4];
}else{
sp = new Sprite();
}
if(x_y_w_h_sp){
if (x_y_w_h_sp[0]) { sp.x = x_y_w_h_sp[0] };
if (x_y_w_h_sp[1]) { sp.y = x_y_w_h_sp[1] };
}
if(property){
for (i = 0; i < property.length; i++) {
if(property[i] && property[i].length > 1){
sp[property[i][0]] = property[i][1];
}
}
}
if(graphics){
for (i = 0; i < graphics.length; i++) {
if(graphics[i] && graphics[i].length > 1){
sp.graphics[graphics[i][0]].apply(null, graphics[i][1]);
}
}

}
if(addChild){
sp.addChild(addChild);
}
if(x_y_w_h_sp){
if (x_y_w_h_sp[2]) { sp.width = x_y_w_h_sp[2] };
if (x_y_w_h_sp[3]) { sp.height = x_y_w_h_sp[3] };
}
return sp;
}

public static function newTextField(x_y_w_h_txt_color_alpha:Array = null,property:Array=null,method:Array=null):TextField{
var i:int;
var ta:TextField = new TextField();
ta.defaultTextFormat = defaultTextFormat;
if(x_y_w_h_txt_color_alpha){
if (x_y_w_h_txt_color_alpha[0]) { ta.x = x_y_w_h_txt_color_alpha[0] };
if (x_y_w_h_txt_color_alpha[1]) { ta.y = x_y_w_h_txt_color_alpha[1] };
if (x_y_w_h_txt_color_alpha[2]) { ta.width = x_y_w_h_txt_color_alpha[2] };
if (x_y_w_h_txt_color_alpha[3]) { ta.height = x_y_w_h_txt_color_alpha[3] };
if (x_y_w_h_txt_color_alpha[4]) { ta.text = x_y_w_h_txt_color_alpha[4] };
if (x_y_w_h_txt_color_alpha[5]) { ta.textColor = x_y_w_h_txt_color_alpha[5] };
if (x_y_w_h_txt_color_alpha[6]) { ta.alpha = x_y_w_h_txt_color_alpha[6] };
}
if(property){
for (i = 0; i < property.length; i++) {
if(property[i] && property[i].length > 1){
ta[property[i][0]] = property[i][1];
}
}
}
if(method){
for (i = 0; i < method.length; i++) {
if(method[i] && method[i].length > 1){
ta[method[i][0]].apply(null, method[i][1]);
}
}
}
return ta;
}
}
[/sourcecode]