CSS Transform に関する Tips

はいどうも!
バーチャル Youtuber ではない面白法人カヤックのごんです。
CSS Transform の Tips をやっていきます。

1. CSS Transform は後ろから適用される

transform: rotate(45deg) translateX(100px);

は、45度回転してから X 方向に 100px 移動、ではなく
X 方向に 100px 移動してから、45度回転 です。

これは百聞は一見にしかずなので、下にデモを貼ります。

なんで逆にしちゃったんですかね。

2. CSS Transform のパーセント表記は要素の幅に依存する

CSS Transform Translate のパーセント表記は、
DOM 要素とは異なり、親要素ではなく、自身の幅に対する割合を指します。

これも百聞は一見にしかずなので、デモを貼ります。

これは、特に要素の中央寄せする際に非常に便利な性質です。
Flexboxmargin: auto によるセンタリングと使い分けて利用しましょう。

3. CSS Animation は CSS Transform の記述する順番によって変わる

CSS Animation で、CSS Transform の値を変化させる場合、
記述する順番に依って、動き方が変わります。

百聞は一見にしかずとも言いますし、デモを貼ります。

違いがわかりますか?
左の猫ちゃんはまっすぐこっちに駆け寄ってくるのに対し、
右の猫ちゃんは蛇行しながらにじり寄ってきています。
耳の先に着目すると違いがよく分かります。

4. matrix は CSS Animation でうまく補間されたりされなかったりする

我こそは算術の学徒、CSS Transform など行列で書いてくれるわ。
などと意気込んで、matrix にまとめると、
意図通りに動いたり動かなかったりします。

まずは百聞は一見にしかず、デモを御覧いただきたい。

ややこしいので、おとなしく translate や rotate を使うのが吉です。
matrix の Decompose や Recompose についての詳しい仕様を知りたい人は、
W3C の Specification を読みましょう。

5. canvas の translate や scale は CSS Transfrom とほぼ同じ

canvas の Context2D には、translate や scale, rotate と言ったメソッドが生えており、
これは全く、CSS Transform のように扱え、具合が良いです(一見略)。

終わりに

百聞は一見にしかずのしかずってなんなんですかね。
100倍ってすごいな。100倍だぞ100倍

www.kayac.com

面白法人カヤックでは、この目で見たものしか信じないエンジニアを募集中です。
本当に大切なものは目に見えないんだよ。
必要なことは全てソースコードにコメントで書け。
鎌倉でも働けるのでまずはオフィスを100回見に来てください!


面白法人カヤックアドベントカレンダー、
明日は新卒の入江さんが書きます。
よろしくお願いいたします。

【WebGL】シェーダーを使って3D空間でスプライトアニメーションさせる

この記事は、Tech KAYAC Advent Calendar 2017 の19日目の記事です。


こんにちは!カヤックのクライアントワークチーム・フロントエンドエンジニアのふかぽん です。WebGLを用いた3Dコンテンツを制作させていただくことが多いです。

過去のアドベントカレンダーではこんな記事を書かせていただきました。

2015 ... WebGLも怖くない!canvasライブラリを効率良く学ぶオススメの順番

2016 ... 【脱・gulp】npm-scriptsでシンプルなフロントエンド開発環境を作る


目次

はじめに

今回はタイトルのように、WebGLでスプライトアニメーションを実装する方法をご紹介していきます。

先日お仕事でWebGL開発をしている際、3D空間上にエフェクトとしてスプライトアニメーションを多用する機会がありいろいろと方法を調べていたので、順を追って解説していければなと思います。

完成形はこちらです!

f:id:takumifukasawa:20171218154647g:plain

jsdo.it

f:id:takumifukasawa:20171218154349p:plain

↑↑ こちらがスプライト画像です。1-16までの連番になっています。

WebGLでスプライトアニメーションさせる方法

他にもいくつかあるかと思いますが、ぱっと思いつく方法はこれらです。

  1. コマ画像(スプライト画像のフレームごと)を使いたい枚数分読み込んで画像ごとに板ポリゴンを作り、時間経過で板ポリゴンの表示非表示を切り替えていく

  2. 板ポリゴンにコマ画像のテクスチャを複数渡し、時間経過ごとにテクスチャ自体を切り替えていく

  3. スプライト画像のテクスチャを用い、テクスチャ座標(以下、UV座標)を時間経過ごとにずらしてスプライトアニメーションさせる


1番の場合、実装によってはドローコールが余計に増えてしまう上に、動かしたいコマ画像が多ければ多いほど画像のリクエスト数やテクスチャ数が増えて、メモリを圧迫する一要因になってしまうデメリットがあります。

2番の場合も、リクエスト数やテクスチャ数が増えてしまう点で同様のデメリットがあります。

サイト制作をする上でリクエスト数や描画負荷は抑えていきたい部分なので、これらのデメリットは避けたいところですよね。

1. UV座標をずらしてフレームを表示する

3番の、スプライト画像を用いてUV座標を時間経過ごとにずらしていく方法であれば、画像枚数は抑えられるかつ、ドローコールが多大に増加することもないので、ブラウザの負荷を抑えることのできる実装となります。

今回のサンプルではWebGLの一般的なライブラリであるThreejsを使っているのですが、スプライト画像によるアニメーション機能は含まれていないため自分でシェーダーを書く必要があります。

順番を追うためにまずはアニメーションさせず、「6」だけ表示させてみましょう。

1-1. UV座標を拡大縮小させる

f:id:takumifukasawa:20171218165226p:plain

まずUV座標のスケールを各フレームの大きさに合わせるために拡大縮小をします。サンプルの画像では縦横4個ずつなので、縦横それぞれ1/4に縮小する拡大縮小行列を用意します。

WebGLにおいてUV座標の原点は左下にあるので、上図のように縮小されることになります。

mat3 scaleMat = mat3(
  1.0 / 4.0, 0.0, 0.0, // 横列のセルの数分、横に縮小(フレーム4つ分縮小)
  0.0, 1.0 / 4.0, 0.0, // 縦列のセルの数分、縦に縮小(フレーム4つ分縮小)
  0.0, 0.0, 1.0
);

1-2. UV座標を平行移動させる

f:id:takumifukasawa:20171218170222p:plain

つづいて、UV座標を指定のセルの位置に平行移動させる行列を作ります。「6」のフレームは横列の左から2つ目に、縦列の下から3つ目に位置するので、拡大縮小後のUV座標は上図のような位置に移動されます。

注意するのは、縦列で下から数えている点です。これは前述のように、UV座標の原点が左下に位置しているためです。

mat3 translateMat = mat3(
  1.0, 0.0, 1.0, // 横に平行移動(横列の左から2つ目のフレームに平行移動)
  0.0, 1.0, 2.0, // 縦に平行移動(縦列の下から3つ目のフレームに平行移動)
  0.0, 0.0, 1.0
);    

1-3. 座標変換を適用する

先に求めた拡大縮小行列と平行移動行列の2つの行列をかけ合わせて、UV座標をずらす用の変換行列を作ります。WebGLの行列計算は列オーダーなので、掛け合わせる順番に注意が必要です。

こちらが頂点シェーダーの全容になります。今回は、変換行列ともともとのUV座標をフラグメントシェーダーに渡すようにしました。

varying vec2 v_uv;
varying mat3 v_spriteMat;
  
void main(void) {
  mat3 scaleMat = mat3(
    1.0 / 4.0, 0.0, 0.0, // 横列のフレームの数分、横に縮小(フレーム4つ分縮小)
    0.0, 1.0 / 4.0, 0.0, // 縦列のフレームの数分、縦に縮小(フレーム4つ分縮小)
    0.0, 0.0, 1.0
  );
    
  mat3 translateMat = mat3(
    1.0, 0.0, 1.0, // 横に平行移動(横列の左から2つ目のフレームに平行移動)
    0.0, 1.0, 2.0, // 縦に平行移動(縦列の下から3つ目のフレームに平行移動)
    0.0, 0.0, 1.0
  );    
    
  v_spriteMat = translateMat * scaleMat; // UV座標をずらす用の変換行列を作成
  v_uv = uv;
  
  gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.);
}

1-4. 座標変換後のUV座標を元に画像の色を取得する

こちらはフラグメントシェーダーをまるっと記載したものです。頂点シェーダーから渡されたUV座標と変換行列を掛け合わせて各セルのUV座標を算出し、texture2Dで色を取得しています。頂点シェーダーと比べてシンプルですね。

これでまずは「6」のフレームを表示することができました!!!!!

「6」を表示するサンプルのリンクは以下になります。

uniform sampler2D u_texture;
  
varying vec2 v_uv;
varying mat3 v_spriteMat;

void main(void) {
  vec3 uv = vec3(v_uv, 1.0);
  uv *= v_spriteMat;
  vec4 smpColor = texture2D(u_texture, uv.xy / uv.z);

  gl_FragColor = smpColor;
}

f:id:takumifukasawa:20171218154142g:plain

jsdo.it

2. ループでアニメーション

いよいよアニメーションをさせる実装に入ります。さきほどの頂点シェーダーの一部を変更したものがこちらです。フラグメントシェーダーに変更点はありません。

javascript側では、uniform に u_time(requestAnimationFrameの現在時間)とu_startTime(アニメーションを始めた時間)を追加して、アニメーションループ内で u_timeを更新しています。そのu_timeとu_startTimeの差分を元に現在のフレームを求め、座標変換もそれに対応させました。

uniform float u_time;
uniform float u_startTime;

varying vec2 v_uv;
varying mat3 v_spriteMat;
  
void main(void) {
  float elapsedTime = u_time - u_startTime;
        
  float colNum = 4.0;
  float rowNum = 4.0;
  float fps = 4.0;
  float frameNum = 16.0;  
    
  float frameIndex = floor(mod(elapsedTime * fps, frameNum));    
    
  mat3 scaleMat = mat3(
    1.0 / colNum, 0.0, 0.0,
    0.0, 1.0 / rowNum, 0.0,
    0.0, 0.0, 1.0
  );

  mat3 translateMat = mat3(
    1.0, 0.0, mod(frameIndex, colNum),
    0.0, 1.0, rowNum - (floor(frameIndex / colNum) + 1.0),
    0.0, 0.0, 1.0
  );    
    
  v_spriteMat = translateMat * scaleMat;
  v_uv = uv;
  
  gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
} 

3. 完成!

これでスプライト画像のアニメーションが完成しました!!!!!!!

完成形は以下のリンクになります。前述のjavascript側の変更もリンク先からご確認いただけます。

jsdo.it

f:id:takumifukasawa:20171218154647g:plain

最後に

カヤックではWebGLを書きたいフロントエンドエンジニアを大大大募集中です。

明日はフロントエンドエンジニアの大先輩 ゴンさん の記事です。

ありがとうございました!