トナカイと白ひげのおじいさんに学ぶUVアニメーション

この記事は Tech KAYAC Advent Calendar 2021 の24日目の記事です。

こんにちは!
意匠部のおばらです。

今日はUVアニメーションのお話です。

f:id:tsmallfield:20211224082820p:plain

目次

UVとは?

3Dの世界でUVといえばUV座標のことです。
UV座標は頂点の3次元座標がテクスチャ上のどの位置に対応するかを表す2次元座標。
きれいなUVだね〜とか、UVがずれてるじゃんとか、UV展開しといて〜、とかそんな感じで使います。

ところでUVってどういう意味なのでしょう?
なにかの頭文字でしょうか?

なぜ`U`と`V`なのか

むかしむかしあるところに、3次元の座標をアルファベットの最後の3文字 X, Y, Z で表らわそうと決めた働き者のトナカイがいました。
彼は職業柄、自分の位置を3次元空間上で把握する必要があったのです。

🦌「よ〜し、3次元の座標を X, Y, Z で表すぞ〜!」
🎅🏻「( ˘ω˘ )zzZ」 f:id:tsmallfield:20211223094706p:plain

ところがしばらくすると3次元ではなく4次元の計算もしたくなりました。
位置情報だけではなく自分がどこを向いてるのかという回転情報も把握する必要があったのです。
オイラー角なら3次元で事足りますがジンバルロックに遭遇して道(空?)に迷ったら一大事。
ですから4次元のクォータニオンで回転情報を表す必要がありました。
しかしXYZの次にまだアルファベットがあればよかったものの、御存知の通り Z までしかありません。
そこでトナカイは近くにいた白ひげのおじいさんに相談しました。

🦌「恐れながら申し上げます、、、」
🎅🏻「うむ。遠慮なく申してみろ」
🦌「4次元座標を表すためにXYZに加えてもう一つアルファベットが欲しいのでございます。。」
🎅🏻「えっ、、、w」
🎅🏻「おぬしはちと見通しが甘いのではないかな」
🦌「も、申し訳ございません。。」
🎅🏻「まぁよい」
🎅🏻「X, Y, Z の次は、、」
🦌「a?」
🎅🏻「Xの前のWを使うのじゃw」
🦌「XYZWだと順番があべこべでございますが。。」
🎅🏻「・・・そ、そんな細かいことを気にするでない」
🦌「ww」
f:id:tsmallfield:20211223094729p:plain

さらにその後、地図(テクスチャ)上の2次元座標を表す2つのアルファベットが欲しくなりました。
3次元空間の座標と区別したいので X, Y 以外の別のアルファベットが必要になったのです。
トナカイはまた白ひげのおじいさんに相談しました。

🦌「2次元のテクスチャ座標を表すアルファベットが欲しいのですが。。」
🎅🏻「XYがあるじゃろ」
🦌「それが、、3次元空間のX, Yと区別したいのでございます。。」
🎅🏻「うむ、それは一理あるのぉ。。」
🎅🏻「Wの次は、、 UV の出番じゃな」
🦌「なるほど〜 u(^ ^ )v」

f:id:tsmallfield:20211223094750p:plain

はい、以上がテクスチャ座標がUV座標と呼ばれる理由です。たぶん。
ちなみに2次元座標を S, T で表す場合もあります。。

🦌「XY でも UV でもないアルファベットで2次元座標を表したいのですが。。」
🎅🏻「な、なるほど、、今度はそうきおったか。。」
🎅🏻「え〜とU, V の次だから、、 ST を使うのじゃ」
🦌「はいぃ〜 (T T)」

f:id:tsmallfield:20211223094810p:plain

そういえば色は R(Red), G(Green), B(Blue), A(Alpha) で表しますね。
単語の頭文字をとったほうがわかりやすいと気づいたのでしょうか?

🦌「XYZW以外のアルファベットで表したい4次元の情報があるですが。。」
🎅🏻「ふむ。それは何じゃ?」
🦌「色を ①赤、②緑、③青、④透明度 の4つの値で表したいのでございます。。」
🎅🏻「う〜む、きりがないのぉ」
🎅🏻「じゃあ、S, T, の次だから、R, Q, P, O 。。
🎅🏻「(いやまてよ、ここでしれっと軌道修正しておこうかのぉ、、)」
🎅🏻「頭文字で R(Red), G(Green), B(Blue), A(Alpha) とするのはどうじゃ?」
🦌「わかりやすいし覚えやすいです〜!o(> <)b」
🎅🏻「わしゃ天才じゃな!」
f:id:tsmallfield:20211223094849p:plain

そしてなんと、4次元座標を XYZW でも RGBA でもなく S, T, に P, Q を加えて STPQ で表す場合も。。。

🦌「さらに別のアルファベットでも4次元座標を表したいのでございますが。。」
🎅🏻「・・・」
🎅🏻「もう知らん!じゃあ、Rはもう色で使っちゃったから新たにP, Q を使えぃ!」
🎅🏻「STPQ をくっつけて STPQ じゃ!」
🦌「・・・」
🎅🏻「なにか不満でもあるのかな?」
🦌「い、いえ、、とんでもございません!」
f:id:tsmallfield:20211223094953p:plain

だんだん頭がこんがらがってきました。
ここまでの流れをわかりやく GIF にしてみます。
f:id:tsmallfield:20211223095048g:plain

実際にこういった経緯だった定かではございませんし、一部、いやだいぶ想像のお話ですが
こう考えると覚えやすいですよね!

ちなみにシェーダ(GLSL)でこんなベクトルがあった時

vec4 hoge = vec4(1, 2, 3, 4);

それぞれの4つの要素には
x, y, z, w
でも
r, g, b, a
でも
s, t, p, q
でもどれでもアクセスできちゃいます。便利。
(※ でも u, v はなぜか使えません。。)
(※ ベクトルのコンストラクタは自動で型変換をしてくれるのでintを渡してもOKです)

本題

調子に乗って昔話が長くなりました。
ごめんなさい。
話を本題に戻します。

UVアニメーションとは頂点座標(XYZ)ではなくテクスチャ上の座標を動かすテクニックです。
例えば、以下のような乗り物を考えます。

f:id:tsmallfield:20211224035702j:plain

ちょうどこの時期たくさんの荷物を世界中に配送する例の乗り物です。
でもこれは足まわりがカスタマイズされていますね。
🦌トナカイが引っ張るかわりに🐛履帯(キャタピラ)がついています。
トナカイはおじいさんとソリが合わなかったのでしょうか。
(※このような駆動装置は一般にキャタピラ(Caterpiller)と呼ばれますが、 当記事ではあえて「履帯(りたい)」と呼ぶことにします。

ja.wikipedia.org
さて、この履帯をJavaScriptでアニメーションさせるにはどうすればよいでしょう?
3Dのモデリングソフトでアニメーションを付けてJavaScriptで再生するのは大変そうですね。

タイヤはただくるくる回せば良いけど履帯は。。。

丸いタイヤだったらただメッシュをくるくる回せば良いです。
試しに Three.js でタイヤに見立てた8角柱のメッシュを回してみました。

See the Pen Untitled by Tohl SMALLFIELD (@tsmallfield) on CodePen.

しかし履帯の場合はそう簡単にはいきません。
そこで登場するのがUVアニメーションです。

UVアニメーション

UVアニメーションはどちらかと言うとプログラムよりもモデルデータとテクスチャに工夫が必要です。

さっそくこちらをご覧ください。 f:id:tsmallfield:20211223194418p:plain

UVはこちら。このUVがポイントです。 f:id:tsmallfield:20211223194948p:plain

そしてテクスチャ。つい趣味で作り込んでしまいます。
(※ テクスチャのサイズに関して: 条件によっては横幅と縦幅がそれぞれ2のN乗である必要がありますが正方形である必要はありません) f:id:tsmallfield:20211223195244j:plain

テクスチャをモデルにはめるとこうなります。

f:id:tsmallfield:20211223195807j:plain

ポリゴンのラインを表示させるとこんな感じ。

f:id:tsmallfield:20211223195836j:plain

実はこの履帯は社内のWebGL勉強会用に作ったミニタンクのパーツです笑

f:id:tsmallfield:20211224100945j:plain

UVアニメーションの仕組み

Blender上でUV座標を動かしてみました。
(マウスで)横方向に動かしているだけです。
頂点は動いていないのに、まるで履帯が動いているように見えますね。
これがUVアニメーションです。

f:id:tsmallfield:20211223192658g:plain

Three.js でUVアニメーション

Three.js を使いUVアニメーションで履帯を動かして動いてる風に見せてみました。
「ワイヤフレーム表示」ボタンをクリックするとワイヤフレーム表示に切り替わります。
頂点座標はアニメーションしていないことがわかると思います。

See the Pen crawler.sample.01.preview by Tohl SMALLFIELD (@tsmallfield) on CodePen.

次にUVアニメーションのロジックを説明します。

JavaScript

UV座標の移動量を入れる2次元のベクトルuvOffsetを用意します。
今回は横方向にのみUV座標を動かすため2次元である必要はありませんが、一応こうしておきます。

const uvOffset = new THREE.Vector2;

頂点シェーダでuvOffsetの情報を受け取れるようマテリアルに渡します。
説明のためシェーダは0から書きたいのでマテリアルは THREE.RawShaderMaterial を使用しています。

テクスチャはリピートされるようにしておきましょう。

texture.wrapS = THREE.RepeatWrapping;
texture.wrapT = THREE.RepeatWrapping;

はい、ここでSTが出てきましたね。
S は横方向、Tは縦方向を意味します。
(※サンプルでは横方向にしか動かさないため、 wrapTTHREE.RepeatWrapping に設定する必要は本当はありません)

const material = new THREE.RawShaderMaterial({
    uniforms: {
        texture: { value: texture },
        uvOffset: { value: uvOffset },
    },
    vertexShader,
    fragmentShader,
});

10秒で履帯がひと回りするよう毎フレームで値を更新します。

requestAnimationFrame(function render(time) {
    const DURATION = 10000;
    const x = (time % DURATION) / DURATION;
    
    uvOffset.x = x;
    ...
    requestAnimationFrame(render);
});

こうするとtは0以上1未満を10秒ごとに繰り返します。
JavaScript の説明は以上です。シンプル!

頂点シェーダ

See the Pen crawler.sample01.vertexShader.glsl.jsm by Tohl SMALLFIELD (@tsmallfield) on CodePen.

ポイントはここ!JavaScriptから渡された uvOffsetuv に足しています。

vUv = uv + uvOffset;

あとはいつもどおりです。

フラグメントシェーダ

See the Pen crawler.sample01.fragmentShader.glsl.jsm by Tohl SMALLFIELD (@tsmallfield) on CodePen.

特に変わったことはしていません。
頂点シェーダから(補間された上で)渡されたUV座標をもとにテクスチャから色を取得しています。
(※デモ用にワイヤフレーム表示かどうかで処理を変えてます)
シェーダもシンプル!

さて、無事UVアニメーションで履帯が動いている風にできましたが、やっぱり頂点をちゃんと動かしたい気も。。
特に履帯はゴツゴツとしたメカメカしい部品が動くのが格好いいのです。

そこでUVアニメーションの概念を応用してみましょう。

UVアニメーションの応用

作り直したモデルです。ディテールが増していますね。
f:id:tsmallfield:20211224015924p:plain

UV
f:id:tsmallfield:20211224023208p:plain

テクスチャ
f:id:tsmallfield:20211224023116j:plain

テクスチャをはめるとこんな感じです。
f:id:tsmallfield:20211224023139p:plain


f:id:tsmallfield:20211224023155p:plain

まずは動かしてみる

サンプルを用意しました。
履帯が動いてる風ではなく、ちゃんと個々のパーツが動いてますね!
ワイヤフレーム表示にすると、頂点がアニメーションしていることがわかると思います。

See the Pen crawler.sample02.preview by Tohl SMALLFIELD (@tsmallfield) on CodePen.

JavaScript

新たに2つのテクスチャを読み込んでいます。

f:id:tsmallfield:20211224080253p:plain

f:id:tsmallfield:20211224080304p:plain

それぞれ
・X座標の時間経過が格納されたテクスチャ
・Y座標の時間経過が格納されたテクスチャ
です。

頂点座標の時間経過をテクスチャに格納しておき、
毎フレームUV座標をちょっとずつずらしつつ頂点座標をテクスチャから取得しようという作戦です。

頂点シェーダ

See the Pen crawler.sample02.vertexShader.glsl.jsm by Tohl SMALLFIELD (@tsmallfield) on CodePen.

後ほど触れますが、テクスチャのR,G,Bチャンネルに8ビットずつ、合計24ビットの領域に数値が埋め込まれています。
それを抜き出す関数がこちらです。

float unpackFloat24FromRGB(vec3 rgb) {
    vec3 v = vec3(
        1.0,
        1.0 / 255.0,
        1.0 / 65025.0
    );
    
    return dot(rgb, v);
}

UV座標にJavaScriptから渡されたuvOffsetを足し、 その座標を元にそれぞれのテクスチャからx, y 座標を抜き出します。

float x = unpackFloat24FromRGB(
    texture2D(texPositionX, uv + uvOffset).rgb
);
    
float y = unpackFloat24FromRGB(
    texture2D(texPositionY, uv + uvOffset).rgb
);

抜き出した値は0.0〜1.0に正規化されているのでそれを元の範囲に戻します。

vec4 pos = vec4(
    position.x,
    y * POS_Y_RNG + POS_Y_MIN,
    x * POS_X_RNG + POS_X_MIN,
    1
);

(※諸事情によりX座標とZ座標を入れ替えてます)

あとはいつもどおり行列をかけて座標変換するだけです。

gl_Position = projectionMatrix * modelViewMatrix * pos;

(※お気付きかもしれませんがモデルデータのZ座標、Y座標は全く使っていません笑)

フラグメントシェーダ

See the Pen crawler.sample02.fragmentShader.glsl.jsm by Tohl SMALLFIELD (@tsmallfield) on CodePen.

こちらは全く変更なしです。

次にテクスチャに頂点座標の時間経過を格納する方法を説明します。

頂点座標の時間経過をテクスチャに格納する

まずテクスチャに頂点座標を格納する専用のモデルを別に作成します。
UVが切れ目なく敷き詰められている必要があるからです。
履帯が動いたときの通り道の形状を作るイメージです。 f:id:tsmallfield:20211224070235p:plain

このモデルを使いX座標とY座標の時間経過をテクスチャに格納するサンプルコードが以下です。

See the Pen crawler.positionBaker by Tohl SMALLFIELD (@tsmallfield) on CodePen.

(※ 生成するテクスチャのサイズは実際よりも小さめにしています)

頂点シェーダ(X座標用)

See the Pen crawler.positionBaker.x.vs.glsl.jsm by Tohl SMALLFIELD (@tsmallfield) on CodePen.

まず、各頂点のX座標を0.0〜1.0に正規化します。
(※ 正規化するための定数はBlenderでモデルの座標を見ながら手入力しています)

正規化された値をR,G,Bチャンネルに8ビットずつ、24ビットの精度でvec4に埋め込みます。
(※ XYZでもSTPでもなくRGBと書いてるのは、後に色情報として使われることになるからです)
(※ 24ビットにするのは8ビットでは精度が足りないためです)
(※ Aチャンネルを使わないのは劣化を避けるためです)

f:id:tsmallfield:20211224074508p:plain

24ビットの精度でX座標が埋め込まれたvec4varying変数で(色情報として)頂点シェーダに渡します。

フラグメントシェーダ(X座標用)

See the Pen crawler.positionBaker.x.fs.glsl.jsm by Tohl SMALLFIELD (@tsmallfield) on CodePen.

頂点シェーダから渡された(そして複数の頂点間で補間された)データを色情報として gl_FragColor に代入します。
この補間機能のおかげで、生成したテクスチャはなめらかなグラデーションとなり、結果頂点をシームレスに動かせます。

まとめ

さていかがだったでしょうか?
本当はソリも動かそうと思ったのですがここで力尽きたのでまたの機会に。。

f:id:tsmallfield:20211224090148j:plain

(※ちなみにこのソリは同僚に作らせ作ってもらいました!)

代わりにミニタンクのGIFアニでご容赦ください。。

f:id:tsmallfield:20211224101413g:plain

さて明日12月25日は意匠部デザイナー、べるの記事です!
お楽しみに!!


カヤックではデザインもしたい!JavaScriptもGLSLも書きたい!3Dモデリングもしたい! そんなアートディレクター/デザイナーを大募集中です!

www.kayac.com