!Perfume×Canvasを支える技術
まずこれを書くためにkarukiをいじっていたら書くのが遅くなりましたw
間違いとか わけわからん箇所とかアレば どんどん指摘ください!
!必要な知識
-行列の概念
-HTML5 Canvasの概念
!参考
!!コンピュータグラフィックス
{{amz 4903474003 http://ecx.images-amazon.com/images/I/215BARKN1XL._SL160_.jpg http://www.amazon.co.jp/%E3%82%B3%E3%83%B3%E3%83%94%E3%83%A5%E3%83%BC%E3%82%BF%E3%82%B0%E3%83%A9%E3%83%95%E3%82%A3%E3%83%83%E3%82%AF%E3%82%B9-%E7%AC%AC2%E7%89%88-%E3%82%B3%E3%83%B3%E3%83%94%E3%83%A5%EF%BC%8D%E3%82%BF%E3%82%B0%E3%83%A9%E3%83%95%E3%82%A3%E3%83%83%E3%82%AF%E3%82%B9%E7%B7%A8%E9%9B%86%E5%A7%94%E5%93%A1%E4%BC%9A/dp/4903474003%3FSubscriptionId%3D0XWJJHYBK2E3MDJAQ9G2%26tag%3Dinajob-22%26linkCode%3Dxm2%26camp%3D2025%26creative%3D165953%26creativeASIN%3D4903474003 コンピュータグラフィックス 第2版}}
大学の授業の教科書だったけど、CGの技術がざっくりと網羅してあっていい本。
アフィン変換とかなんだっけー と参考にしながらコードを書きました。
ソースコードとかは一切乗ってないです、概念と数式です。
!!HTML5.jp
http://www.html5.jp/canvas/
Canvasのことならここ見てれば他を見る必要はない というくらい充実しています。
!!Perfume global site project
- http://perfume-dev.github.com/
データの配布元 感謝!
githubには他の言語での実装があったので 特にprocessingのやつが読みやすかったので、行き詰まった時に答えを見る感覚でチラチラ覗いてました。(行列掛ける順番の確認とか)
!ソースコード
>> code javascript
/*
Perfume script
created by @ina_ani
MIT Licence.
PLEASE FORK THIS!
*/
<<
とりあえずライセンスは書かなきゃね
>> code javascript
var global = {};
<<
デバッグ用 気にしないで…
>> code javascript
// 3x3行列 便利クラス
function Affine33(){
this.data=[[1,0,0],[0,1,0],[0,0,1]];
if(arguments.length == 9){
this.data = [
[arguments[0],arguments[1],arguments[2]],
[arguments[3],arguments[4],arguments[5]],
[arguments[6],arguments[7],arguments[8]]
];
}else if(arguments.length == 0){概念
}else{
throw "arguments size error";
}
}概念
<<
後に使う3x3行列のクラスのコンストラクタ。
一応引数で初期値を取れるようにしたけどあまりつかってないかなー。
何も入れないと単位行列
>> math
\begin{pmatrix}
1 & 0 & 0\\
0 & 1 & 0\\
0 & 0 & 1
\end{pmatrix}
<<
が初期値になります。
4x4と3x3を一緒に扱いたかったけど、回転とか結局それぞれ個別に書いたほうがわかりやすい気がしたので分けた。
>> code javascript
Affine33.prototype = {
mul:function(a){
var tmp = 0;
var next = [];
for(var i = 0; i < 3; i++){
for(var j = 0; j < 3; j++){
tmp = 0;
for(var k = 0; k < 3; k++){
tmp += this.data[i][k] * a.data[k][j]
//tmp += a.data[i][k] * this.data[k][j]
}
if(next[i] == undefined)next[i] = [];
next[i].push(tmp);
}
}
this.data = next;
}
};
<<
3x3行列は掛け算が出来ればそれでいい。掛ける向きがあれ? ってなったので反対のバージョンも書いてある。
えっと右から掛けるコードになってるはず
>> math
\begin{equation}
this = this \times a
\end{equation}
<<
ですね
mulは自身を書き換えます。
>> code javascript
// 4x4行列 便利クラス
function Affine(){
this.data=[[1,0,0,0],[0,1,0,0],[0,0,1,0],[0,0,0,1]];
if(arguments.length == 16){
this.data = [
[arguments[0],arguments[1],arguments[2],arguments[3]],
[arguments[4],arguments[5],arguments[6],arguments[7]],
[arguments[8],arguments[9],arguments[10],arguments[11]],
[arguments[12],arguments[13],arguments[14],arguments[15]]
];
}else if(arguments.length == 0){
}else{
throw "arguments size error";
}
}
<<
さて、こっちが本番 4x4行列 同じく何も指定しないと単位行列になります。
>> math
\begin{pmatrix}
1 & 0 & 0 & 0 \\
0 & 1 & 0 & 0 \\
0 & 0 & 1 & 0 \\
0 & 0 & 0 & 1
\end{pmatrix}
<<
ですね
>> code javascript
Affine.prototype = {
mul:function(a){
var tmp = 0;
var next = [];
for(var i = 0; i < 4; i++){
for(var j = 0; j < 4; j++){
tmp = 0;
for(var k = 0; k < 4; k++){
tmp += this.data[i][k] * a.data[k][j]
//tmp += a.data[i][k] * this.data[k][j]
}
if(next[i] == undefined)next[i] = [];
next[i].push(tmp);
}
}
this.data = next;
},
<<
(とちゅうでぶった切りましたが) これも3x3の時と同じ 掛け算。
こちらも右から掛けるコードになってるはず
>> math
\begin{equation}
this = this \times a
\end{equation}
<<
ですね
mulは自身を書き換えます。
>> code javascript
shift:function(x,y,z){
this.mul(new Affine(
1,0,0,x,
0,1,0,y,
0,0,1,z,
0,0,0,1
));
},
<<
アフィン変換がすばらしいので、並行移動とか超簡単。
>> math
\begin{equation}
this = this \times
\begin{pmatrix}
1 & 0 & 0 & x\\
0 & 1 & 0 & y\\
0 & 0 & 1 & z\\
0 & 0 & 0 & 1
\end{pmatrix}
\end{equation}
<<
ですね。
アフィン変換?って何? と思う人もちょっと手元で行列書いて掛け算してみてください。たしかに平行移動してますんで。
はじめは二次元で 3x3行列でやったほうが計算も楽だし直感的かなと思います。
>> code javascript
// ラジアン → 度
r:function(r){
return r/180.0 * Math.PI;
},
<<
ありがちなユーテリティ関数。bvhファイルのの角度がdegreeだったので扱いやすいラジアンに変換できる便利関数を用意しました。
>> code javascript
rotateX:function(t){
this.mul(new Affine(
1,0,0,0,
0,Math.cos(this.r(t)),-Math.sin(this.r(t)),0,
0,Math.sin(this.r(t)), Math.cos(this.r(t)),0,
0,0,0,1
));
},
<<
回転。3次元の場合どの軸で回すかによって処理が変わりますのでそれぞれ作る。
X軸回転の場合は
>> math
\begin{equation}
this = this \times
\begin{pmatrix}
1 & 0 & 0 & 0\\
0 & cos\theta & -sin\theta & 0\\
0 & sin\theta & cos\theta & 0\\
0 & 0 & 0 & 1
\end{pmatrix}
\end{equation}
<<
です。 ってこれも(当然)僕は覚えてなくて、調べて手で掛け算して検算しました。
回転も自身を書き換えます。
>> code javascript
rotateY:function(t){
this.mul(new Affine(
Math.cos(this.r(t)),0,Math.sin(this.r(t)),0,
0,1,0,0,
-Math.sin(this.r(t)),0,Math.cos(this.r(t)),0,
0,0,0,1
));
},
<<
あとは作業なんだけどY軸回転
>> math
\begin{equation}
this = this \times
\begin{pmatrix}
cos\theta & 0 & sin\theta & 0\\
0 & 1 & 0 & 0\\
-sin\theta & 0 & cos\theta & 0\\
0 & 0 & 0 & 1
\end{pmatrix}
\end{equation}
<<
>> code javascript
rotateZ:function(t){
this.mul(new Affine(
Math.cos(this.r(t)),-Math.sin(this.r(t)),0,0,
Math.sin(this.r(t)),Math.cos(this.r(t)),0,0,
0,0,1,0,
0,0,0,1
));
},
<<
最後にZ軸回転
>> math
\begin{equation}
this = this \times
\begin{pmatrix}
cos\theta & -sin\theta & 0 & 0\\
sin\theta & cos\theta & 0 & 0\\
0 & 0 & 1 & 0\\
0 & 0 & 0 & 1
\end{pmatrix}
\end{equation}
<<
>> code javascript
rotateYXZ:function(y,x,z){
throw "Error";
},
<<
組み合わせて一気に回すやつつくろうとしてやめた残骸。(今は呼ぶとエラーが出ますw)
>> code javascript
clone:function(){
var ret = [];
for(var i = 0; i < this.data.length; i++){
ret[i] = this.data[i].concat([]);
}
return ret;
},
<<
回転、移動は破壊的(自身を変更する)ので時にはコピーをとりたくなるかも。
ってことでディープコピーを用意。
(今見たらmulでぜんぶ取り替えてるからディープコピー不要な気がしてきた…)
>> code javascript
calc:function(){
var tmp = 0;
var ans = [];
var a = [0, 0, 0, 1];
for(var i = 0; i < 4; i++){
tmp = 0;
for(var j = 0; j < 4; j++){
tmp += this.data[i][j] * a[j]
}
ans.push(tmp);
}
return ans;
}
<<
これが今の変換した結果を取り出す? 関数。
>> math
\begin{equation}
this \times
\begin{pmatrix}
0 \\
0 \\
0 \\
1
\end{pmatrix}
\end{equation}
<<
を配列で返します。
>> code javascript
};
<<
はい。これでAffineクラスは終了。これがあれば3次元も怖くない!
ちょっとこのAffineクラスで遊んでみよう → {{link /projects/perfume/inside/affine affineで遊ぶ}}
さて Affineの使い方もわかった所で、実際のperfumeを踊らす処理を見て行きましょう。
>> code javascript
$(function(){
<<
jQueryのおまじない。 この中の処理はdomreadyのタイミングで走ります。
>> code javascript
var canv = document.getElementById('canv');
var ctx = canv.getContext("2d");
var user = "ina_ani";
var img;
<<
キャンバスの初期化。変数の初期化。
本当はgetContextできるかとか調べたほうがいいとおもうけど、そもそもCanvasが動かないブラウザとか今回は無視なので問題なし!
>> code javascript
if(document.location.hash){
user = document.location.hash.replace("#","");
}
$('#user').val(user);
<<
ハッシュからアイコンの読み込み。 URLの後ろに #user_name ってすることでいきなりそのユーザのアイコンになるようにしています。 こうしておくとTwitterに貼りやすいしね。
>> code javascript
// OKボタンに仕掛ける
$('#ok').bind('click',function(){
var u = $('#user').val();
document.location.hash = u;
user = u;
reloadImg();
});
<<
okボタンがクリックされたらテキストボックスの内容をユーザ名だと思って画像を探しに行きます。 reloadImgはこのあと書いてあります
>> code javascript
function reloadImg(){
img = new Image();
img.src="http://gadgtwit.appspot.com/twicon/"+user;
}
reloadImg();
<<
Twitterの画像を読み込む。 ちょっと野良APIを使わせてもらってます。
関数作ってすぐ呼んでます。 OKボタンでも使いたかったので関数にした感じです。
>> code javascript
// スペースをn個
function spc(n){
ret = "";
for(var i = 0; i < n; i++){
ret += " ";
}
return ret;
}
<<
デバッグ用にインデントが簡単に作りたかった
>> code javascript
// ヒエラルキ部ダンプ(デバッグ用)
function dump(hier,d){
console.log(spc(d) + "TYPE:" + hier.type);
console.log(spc(d) + "OFFSET:" + hier.offset);
console.log(spc(d) + "CHANNELS:" + hier.channels);
console.log(spc(d) + "RAW:" + hier.raw);
if(hier.data){
for(var i = 0; i < hier.data.length; i++){
dump(hier.data[i], d + 1);
}
}
}
<<
データの読み込みがうまく言ってるか見るためのコード 本番では使ってません。
>> code javascript
// 左のスペースを取り除く
function rstrip(s){
return s.replace(/^\s+/,"");
}
<<
データにインデントがついてるのを取っ払う
さてここからデータ構造の読み込みです。bvhのデータ構造をJavaScriptの連想配列に入れていきます。
bvhのデータは
|ヒエラルキ|ボーンの親子関係、親からのオフセット、モーションデータの内容
|モーション|↑のモーションでーたの数字列
てなかんじの2つの部分に分かれていて、まずはヒエラルキ部分を読み込もうってのがこの関数です。
生データを読みたい人はこちら→ http://inajob.no-ip.org:10080/perfume/aachan.bvh
雰囲気を知ってもらうためにすこしだけ抜粋します
>>
HIERARCHY
ROOT Hips
{
OFFSET 0.000000 0.000000 0.000000
CHANNELS 6 Xposition Yposition Zposition Yrotation Xrotation Zrotation
JOINT Chest
{
OFFSET 0.000000 10.678932 0.006280
CHANNELS 3 Yrotation Xrotation Zrotation
JOINT Chest2
{
OFFSET 0.000000 10.491159 -0.011408
CHANNELS 3 Yrotation Xrotation Zrotation
JOINT Chest3
{
OFFSET 0.000000 9.479342 0.000000
CHANNELS 3 Yrotation Xrotation Zrotation
JOINT Chest4
{
OFFSET 0.000000 9.479342 0.000000
(略)
<<
まぁこんな感じのデータです。
なんとなく見たら雰囲気わかるよね? CHANNELSってところが変数?プレースホルダ になってて、ここに入るべく値は時々刻々と変化する値で、それはこれに続くモーション部に格納されています。
モーション部の雰囲気はこんな感じ
>>
MOTION
Frames: 2820
Frame Time: 0.025000
-0.207611 83.315683 -17.340600 0.000000 -5.826342 0.000000 0.000000 11.116423 0.000000 0.000000 -5.290081 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 12.665069 0.000000 0.000000 0.575450 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 2.337306 0.000000 0.000000 -2.337306 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 -2.337306 0.000000 0.000000 2.337306 0.000000 0.000000 0.000000 8.578828 0.000000 0.000000 1.905768 0.000000 0.000000 -4.658254 0.000000 0.000000 0.000000 0.000000 0.000000 8.578828 0.000000 0.000000 1.905768 0.000000 0.000000 -4.658254 0.000000 0.000000 0.000000 0.000000
-170.054294 81.764214 -341.516381 22.972828 -6.370998 -0.475595 1.690447 4.629088 -0.050206 0.062898 0.169733 0.001048 0.103375 0.278956 0.001820 1.621565 4.364098 0.086351 -3.410426 15.874543
(略 以降数字が続く)
<<
ああ目が痛い…。
これが上のヒエラルキ部のCHANNELSと対応しています。
1行にはたしか72個ほどの数字がスペース区切りで入っていて、先頭から順に
Hips.Xpositon, Hips.Yposition, Hips.Zposition, Hips.Yrotation, Hips.Xrotation Hips.Zrotation, Chest.Yrotation, Chest.Xrotation, Chest.Zrotation, Chest2.Yrotation .....
と、ヒエラルキ部の出現順に各パラメータの値が入っているようです。(目で見た感じなのでもしかしたらちがうかも、でも一応動いてます)
ということでデータの内容はわかったので解析だー
ちゃんと構文解析すると言うよりは、Perlののりでパターンマッチしてがりがりっと読み込んでます。あと興味のないものは無視するという男らしいパーサーになってます(手抜き)
>> code javascript
// ヒエラルキ部のの解析
function analysisHier(hier,obj){
<<
引数はヒエラルキ部が行ごとに格納されたhierと、返り値の代わりのobjです。 hierから情報をよみとって objに詰めていく感じです。
>> code javascript
var target;
var m;
//console.log("IN:" + obj.raw);
while(true){
if(hier.length == 0)return hier;
target = rstrip(hier[0]);
hier.shift();
if(target == undefined)return obj;
//console.log(target);
<<
このwhileで読みたいだけ読み込みます。
hierから1行取り出して、エラーチェックとかゴミ取りとかします。
>> code javascript
var ret;
if(target.indexOf("ROOT") != -1){
ret = {type:'ROOT',data:[]};
obj.data.push(ret);
hier = analysisHier(hier, ret);
<<
ROOTって文字があった場合 ルートオブジェクトを作ります。
残りの処理は再帰でまかせます
>> code javascript
}else if(target.indexOf("JOINT") != -1){
m = target.match("^JOINT ([a-zA-Z0-9]+)");
ret = {type:'JOINT',name : RegExp.$1,raw: target ,data:[]};
obj.data.push(ret);
hier = analysisHier(hier, ret);
<<
JOINTって文字があった場合 ジョイントオブジェクトを作ります。
ジョイントにはなまえがあるので、それも覚えておきます。 子供がいればdataってところに入れたいのでその入れ物だけ用意します。 rawってのはデバッグ用ですJOINTの行の文字列がまるまる入ってます。
で引き続きの処理は再帰で任せます
>> code javascript
}else if(target.indexOf("OFFSET") != -1){
m = target.match(/^OFFSET ([\-0-9.]+) ([\-0-9.]+) ([\-0-9.]+)/);
obj.offset = [parseFloat(RegExp.$1),parseFloat(RegExp.$2),parseFloat(RegExp.$3),RegExp.$1,RegExp.$2,RegExp.$3];
<<
OFFSETって文字があったら、objのOffsetが記述してあるはずなので、 読み込んで、obj.offsetに入れます。
>> code javascript
}else if(target.indexOf("End Site") != -1){
m = target.match("^End Site");
ret = {type:'End Site'};
obj.data.push(ret);
hier = analysisHier(hier, ret);
<<
おっと書いてなかったけど End Siteってのがもうこれ以上子供はいません最後の子供です って意味らしいので、これがあったらとりあえずそれを今見てるobj.dataにいれて最後の子供を対象に再帰します。
>> code javascript
}else if(target.indexOf("CHANNELS") != -1){
m = target.match("^CHANNELS ([0-9]+) ([a-zA-Z ]+)");
obj.channels = [RegExp.$1].concat(RegExp.$2.split(" "));
<<
CHANNELSって文字があったら プレースホルダに関する情報が入っているはずなので、それを格納します。 まずプレースホルダの数、次にそのプレースホルダの名前が書いてあるはずなので、 obj.channelsに配列にして入れておきます
>> code javascript
}else if(target.indexOf("}") != -1){
//console.log("OUT")
return hier;
<<
"}"があったら 今見てるobjの記述はおわりで、親に戻る必要があるのでreturnします。 hierを返す必要
は… どこまで読み込んだかはhierが知っているのでそれも返却します。
(hierはshiftしてるだけだから同じ物をさしてれば別に返さなくてもいいような気がする)
>> code javascript
}
<<
whileここまで、 満足するまでhierを読み取り続けます
>> code javascript
}
return hier;
<<
ここにはこないね… なくていいね
>> code javascript
}
<<
これでヒエラルキ部の解析関数は終了
さて次はframeです。
この関数は↑で作った構造を順にたどっていきながらモーションデータの数字を当てはめて 3次元の絶対座標を求める関数です。線を引きたい関係で、 親子関係もちゃんと持ったものを返します。
>> code javascript
// ヒエラルキ部たどりなおし 描画用の構造を作る
function frame(hier, d, ar, af){
<<
引数は hierがさきほど作った構造(さっきと同じ名前だけど役割が違う…)
dが深さ
arがモーションデータが入っている配列(モーションデータは数字の列だったよね)
afは今の変換座標が入ってます
>> code javascript
var ret = {name:null,pos:null,data:[]};
<<
arに入れていく結果のひな形。posってところに座標[x,y,x]がはいります。
>> code javascript
var ph = [];
if(hier.channels){
for(var i = 0; i < hier.channels[0]; i ++){
//console.log(spc(d) + "PLACE HOLDER:" + ar[0]);
ph.push(ar[0]);
ar.shift();
}
}
<<
CHANNELSに書いてあったプレースホルダの数だけモーションデータを読み込みます。
>> code javascript
var nextAf = new Affine();
nextAf.data = af.data; // copy
<<
あら、やっぱりclone使わなくてよかったんだよねw
今の座標系から、次の座標系を求めます
>> code javascript
// OFFSET 計算
if(hier.offset){
nextAf.shift(hier.offset[0],hier.offset[1],hier.offset[2]);
}
<<
OFFSETがあればまずそれに従って座標系を平行移動させます。
>> code javascript
// ROTATE 計算
if(hier.channels){
if(hier.channels[0] == 3){
nextAf.rotateY(ph[0]);
nextAf.rotateX(ph[1]);
nextAf.rotateZ(ph[2]);
}else{ // 手抜き root のoffsetを無視
nextAf.rotateY(ph[3]);
nextAf.rotateX(ph[4]);
nextAf.rotateZ(ph[5]);
}
}
<<
次に回転、 座標系を回転させます。
ここはかなり手抜きしていて、
今回のデータはかならず YXZの順で入っていて、ROOT以外のすべてのJOINTのCHANNELSはYXZの回転データが入っているということをしっているので、 CHANNELSの詳しい内容を調べずにいきなり回転させています。
CHANNELSの数が3でない場合(elseのとき)ってのはROOTだけで、ROOTはpositionX positonY positionZ rotateY rotateX rotateZ の6つのプレースホルダーがあります。
もうpositionはざっくりと無視します。(だからこのperfume.jsだとその場で踊っているみたいな感じになっちゃってます)
>> code javascript
ret.name = hier.name;
// colorは今は使ってない
ret.pos = (nextAf.calc().concat([hier.name=="Head"?"red":"black",nextAf])); // x,y,z,a,color,name, affine
<<
retに具体的な値を入れます。 posには座標をとか言ってましたが、 まぁ何でも入れちゃえ っていう(ひどい)ことになってますね
えっと
[絶対座標X,絶対座標Y,絶対座標Z,色,affine] ですか。 なんか後ろに変なのがついてるね まぁ多分そんなに使ってない
>> code javascript
if(hier.data){
var tmp;
for(var i = 0; i < hier.data.length; i++){
tmp = frame(hier.data[i], d + 1,ar,nextAf);
ret.data.push(tmp);
}
}
<<
で、子供の数だけ再帰します。 nextAfを渡しているので、 親で変更した後の座標系で子供は計算できます。
>> code javascript
return ret;
<<
親に自分のデータを返します。データ構造と制御構造が綺麗に一致するのでこういう場合は再帰がらくですねー
>> code javascript
}
<<
frame終了。 あとは画面に描くだけだね!
と、お楽しみはおいておいて、何故かここからAjaxでモーションデータを取ってくる処理になります。
描画のルーチンはもうちょっと先になります。 我慢しましょう。
>> code javascript
$.ajax({
url: "aachan.bvh",
<<
jQueryに頼ります。 ajaxで同じサーバにあるaachan.bvhを読み込みます。
公開されているデータは3人分あるのでどれでもいいんですが…
>> code javascript
success:function(data){
var list = data.split(/[\r\n]+/);
var hier = [];
var moti = [];
var MODE_HIERARCHY = 0;
var MODE_MOTION = 1;
var mode = -1;
<<
aachan.bvhが正しく読み込まれるとsuccessが呼び出されます。
改行でぶった切ってlistに入れます。
ヒエラルキ部と、モーション部をそれぞれ配列に詰め込みたいので準備します。
>> code javascript
for(var i = 0;i < list.length; i++){
if(list[i].indexOf("HIERARCHY") != -1){
mode = MODE_HIERARCHY;
continue;
<<
HIERARCHYって書いてあるとヒエラルキモードに入ります(まぁ先頭に書いてあるんだけどねw)
>> code javascript
}
<<
MOTIONって書いてあるとモーションモードに入ります
>> code javascript
if(list[i].indexOf("MOTION") != -1){
mode = MODE_MOTION;
continue;
}
switch(mode){
case MODE_HIERARCHY:
hier.push(list[i]);
break;
case MODE_MOTION:
moti.push(list[i]);
break;
}
<<
モードに応じて適切な方にpushします
>> code javascript
}
// load ここまで
<<
ロードできたね
>> code javascript
// デバッグ用
global.hier = hier;
global.moti = moti;
<<
globalを覗いてデバッグしていた名残 使ってないです
>> code javascript
// hier 解析
var obj = {type:"NULL",data:[]};
var h = analysisHier(hier, obj);
<<
さっき見たanalysisHierで ヒエラルキ部を解析します。
hには… たぶん空の配列だよね? つかわないです(なんで書いたっw)
objに解析済みのオブジェクトが入ってます。
>> code javascript
//console.log(obj);
//dump(obj, 0);
<<
デバッグの名残
でいよいよお待ちかねの描画
>> code javascript
function show(ctx, ret){
<<
ctxがキャンバスのコンテキストです(これを通してCanvasに描きます)
retは frameの返り値、 親子関係があって絶対座標が格納されてるオブジェクトですね。
>> code javascript
var pos = ret.pos; // xyza color name affine
var data = ret.data;
var af = ret.pos[5];
var a,b;
a = 0; // X ?
b = 1; // Y ?
<<
まぁてきとーに変数を準備。
a,bってのはどっちから見るかの方向。
3次元のデータを2次元に投影するための最も簡単な方法、1軸を無視する。という作戦です。
遠くのものは小さくならないけどまぁいいや。
>> code javascript
ctx.fillRect(pos[a], -pos[b], 5, 5);
if(ret.name == undefined){
// 端っこの点は大きめに
ctx.fillRect(pos[a]-5, -pos[b]-5, 10, 10);
//ctx.beginPath();
//ctx.arc(pos[a], -pos[b], 7, 0, Math.PI*2, false)
//ctx.fill();
}
<<
あ、計算の都合でY座標がひっくり返っちゃったので すべてのY座標に-1を掛けてますw
四角の点を書きます。 端っこの点(End site)には名前が無いので、それで識別して大きめの点を書いてます。
コメントアウトしてるのは 円バージョン。 どっちがいいかなー とか思って両方書いてます
>> code javascript
// boneの名前を描画
ctx.fillText(ret.name,pos[a]+100,-pos[b]+10);
<<
デバッグ用に名前を100ピクセル右側に書いてます。
>> code javascript
for(var i = 0; i < ret.data.length; i++){
ctx.beginPath();
ctx.moveTo(pos[a],-pos[b]);
ctx.lineTo(ret.data[i].pos[a], -ret.data[i].pos[b]);
ctx.stroke();
show(ctx, ret.data[i]);
}
<<
親→子に線を引いてから 子供を描画します。 ここも再帰。
データが再帰的なので、処理も再帰的になりますね。
ちょっとここから見苦しいコードが…
顔画像を当てはめる部分
>> code javascript
var afa,afb,afc;
var afz,afy;
var tmpa,tmpb,tmpc;
if(ret.name == "Head"){
// Twitterの顔をつける
afa = new Affine();
afb = new Affine();
afc = new Affine();
<<
顔画像を貼り付けるための変換行列を計算します。
>> code javascript
//ctx.fillRect(pos[a]-10, -pos[b]-10, 20, 20);
ctx.fillStyle="red";
var scale = 10;
afz = new Affine33(
-1/2,1/2,0,
1/2,0,1/2,
0,-1/2,1/2
); // 逆行列
afa.data = af.clone();
afa.shift(-scale,scale,0);
tmpa = afa.calc();
afb.data = af.clone();
afb.shift(scale,scale,0);
tmpb = afb.calc();
afc.data = af.clone();
afc.shift(-scale,-scale,0);
tmpc = afc.calc();
<<
なんかheadの変換行列からもっとスマートに求められる気がしてきた…(誰か教えて)
まぁいいや、とりあえずやった方法を説明します。
headの部分をXY方向に広げた四角形の一部の三角形の座標を求めます。
三角形なので3点あるね。 tmpa,tmpb,tmpc
>> code javascript
afy = new Affine33( // 中間構造
tmpa[a],tmpb[a],tmpc[a],
-tmpa[b],-tmpb[b],-tmpc[b],
1,1,1
);
afy.mul(afz);
<<
でこれを3x3行列に入れて、おまじないの行列を掛ける… と 求めたい顔画像の変換行列が出てきます。
と、何も説明になってないので、たねあかしすると…
>> math
\begin{equation}
\begin{pmatrix}
a & b & c\\
d & e & f\\
0 & 0 & 1\\
\end{pmatrix}
\begin{pmatrix}
-1 & 1 & -1\\
1 & 1 & -1\\
1 & 1 & 1\\
\end{pmatrix}
=
\begin{pmatrix}
tmpa_x & tmpb_x & tmpc_x\\
tmpa_y & tmpb_y & tmpc_y\\
1 & 1 & 1\\
\end{pmatrix}
\end{equation}
<<
ある変換を通した時
|(-1,1) |->| (tmpa[x],tmpa[y])
|(1,1) |->| (tmpb[x],tmpb[y])
|(-1,-1) |->| (tmpc[x],tmpc[y])
となるような変換を計算しよう!
(左に書いてあるのが何も変換していない時の三角形の頂点の座標、右は今の顔の向きを考慮した三角形の頂点の座標)
そういう変換がわかれば
その変換行列を使って 画像を変換して貼りつければ顔の部分にはりつくよね!
(画像は四角形で 本来は三角形2枚で計算するんだろうけど、まぁ誤差なので するー)
で↑の式で、a,b,c,d,e,fがわかればいいんだからえっと
>> math
\begin{equation}
\begin{pmatrix}
a & b & c\\
d & e & f\\
0 & 0 & 1\\
\end{pmatrix}
=
\begin{pmatrix}
tmpa_x & tmpb_x & tmpc_x\\
tmpa_y & tmpb_y & tmpc_y\\
1 & 1 & 1\\
\end{pmatrix}
\begin{pmatrix}
-1 & 1 & -1\\
1 & 1 & -1\\
1 & 1 & 1\\
\end{pmatrix}
^{-1}
\end{equation}
<<
だね。
これで右辺が既知なので後は逆行列を求めれば…
って 僕出し方忘れちゃったよ!!(なんか図を書いてやるつあるじゃん?) とかちょっとショックをうけつつ手でちまちま計算
で出てきたのが
>> math
\begin{equation}
\begin{pmatrix}
-1 & 1 & -1\\
1 & 1 & -1\\
1 & 1 & 1\\
\end{pmatrix}
^{-1}
=
\begin{pmatrix}
-1/2 & 1/2 & 0\\
1/2 & 0 & 1/2\\
0 & -1/2 & 1/2\\
\end{pmatrix}
\end{equation}
<<
ふう。 これでいいね。あとは掛け算して出てきた値が欲しかった変換行列だ。
変数名afyってやつだね
とあとはCanvasにおまかせです。
Canvasは3x3の変換行列を使うこともできるので本当にお任せ。
>> code javascript
ctx.save();
ctx.strokeStyle = "red";
//console.log(afy.data[0],afy.data[1],afy.data[2]);
ctx.transform(afy.data[0][0],afy.data[1][0],afy.data[0][1],afy.data[1][1],afy.data[0][2],afy.data[1][2]);
<<
saveってやっておくとあとで簡単にrestoreできるので、まずsave。これで今の座標系をCanvasが覚えておいてくれます
それから、先ほど求めた変換行列を使ってcanvasの座標系を設定。
これ以降は歪んだ世界に描画することになります
>> code javascript
ctx.lineWidth = "0.1";
ctx.beginPath();
ctx.moveTo(-1,1);
ctx.lineTo(1,1);
ctx.lineTo(1,-1);
ctx.lineTo(-1,-1);
ctx.lineTo(-1,1);
ctx.stroke();
<<
顔の位置に四角形を書く場合も、座標系が顔の方向に歪んでいるので、こんな感じでok
>> code javascript
ctx.rotate(Math.PI);
ctx.drawImage(img,-1,-1,2,2);
ctx.strokeStyle = "black";
<<
貼り付ける位置に少し工夫が必要(原点中心)だけど、画像だって普通に書けば、顔の角度にフィットする。変換行列さまさまだねー。
>> code javascript
ctx.restore();
<<
このままだと後の描画がぜんぶ歪んだ座標系で行われてしまうので、restoreします。
Canvas便利だねー
>> code javascript
ctx.fillStyle="black";
}
}
<<
描画の処理はここまで、 もう満足?
あとはタイマーとかなんで流し読みでいいかと
>> code javascript
function draw(n){
<<
drawはフレーム番号nを取ってshowを呼ぶ関数です。 データの準備とかします。
>> code javascript
if(moti.length <= n +2)return;
var af = new Affine();
var ar = moti[2 + n].split(" ");
for(var i = 0; i < ar.length; i ++){
ar[i] = parseFloat(ar[i]);
}
<<
モーションデータを今さらながら数字に変換します。 はじめにやっても良かったな…
2+ しているのは モーション部の先頭にはフレームレートなどが書いてある行があって、そこを無視するためです(いいかげんですw)
>> code javascript
var ret;
ret = frame(obj, 0, ar,af);
ctx.lineWidth = "1";
show(ctx, ret);
<<
で、そのフレームでの各頂点の座標を計算して(frame)、表示します(show)
>> code javascript
}
<<
順番が前後しちゃったけどタイマー。 ここがアニメーションのキモですね。
setIntervalで25msに一度処理を走らせています
>> code javascript
var timer = 1;
setInterval(function(){
//if(timer > 10)return;
//ctx.clearRect(0,0,500,500);
ctx.fillStyle = "rgba(255,255,255,0.2)";
ctx.fillRect(0,0,500,500);
ctx.fillStyle = "black";
<<
canvasは毎フレーム消してあげないと、どんどん重ね描きされてしまうので、まず画面を消す必要があります。
clearRectすると綺麗に画面が消えるので、普通はclearRectします。
が今回は アルファ付きの白い四角形で画面を塗りつぶしてます。 これでモーションブラーのような残像の残ったような効果が出ます。
>> code javascript
ctx.save();
ctx.translate(200,200);
ctx.scale(2,2);
<<
倍率やオフセットをいいかんじに合わせます。
(transformで3x3行列を与えるような座標変換もできるし、こんなふうにtranslateやscaleみたいなわかりやすい名前の座標変換もできます)
>> code javascript
draw(timer);
ctx.restore();
<<
描画します。さっき定義したdrawを呼んでますね。
それから↑でsaveしてたのでrestoreします。
うまくやればここのsave-restoreはしなくていいかなーとも思いますが、毎回元に戻したほうが考えやすいのでやってるだけです。
>> code javascript
timer++;
},25)
//console.log(obj);
}
});
<<
タイマーの処理ここまで お疲れ様でした。
>> code javascript
});
<<
これで$()の中身ぜんぶ終了って、 ここにぜんぶ書いちゃったか! って感じですね。
まぁ無名関数にくるんでるおかげでグローバル空間を汚さないし 中で何やってもいいじゃないですかw。
っていうことでソースコードの解説終了。
これでみんなPerfumeを踊らせられるね!
5643382
wiki
1371357793