gl-reactを使って画像(テクスチャ)をY軸周りに回転させることを考えます。
直接関係はないけどGLSLの練習【Crop, Tiling】の続きです。
一般的に画像を回転するというとZ軸周りの回転です。
この回転自体は単純な三角関数で可能ですし、拡大縮小、平行移動、せん断を含めてもアフィン変換で簡単に描画できます。
ただY軸周りの回転となると射影変換 (透視変換 / 透視投影)が必要です。
GLSLを書きつつも基本的な三角関数と線形代数をベースに書いていきます。
基本的な計算
まずはZ軸を0として画像の三次元座標を考え、Y軸周りに回転させます。
回転行列自体は色々と解説が転がっているので、忘れたらwikiでも参考に。
続いて同次座標系で射影(透視投影)を行います。
透視投影に関しては詳しい説明は見つけにくいですがこちらのPDFなんかわかりやすいです。
ここでw=1-x/d cosθ
とすると(x, y) => (x cosθ/w, y/w)
になります。
ここでd
が十分大きいとw≒1
になり平行投影になることを覚えておきます。
GLSLを書いてみる
まずはd=1000
として平行投影で動作を見ます。
基本的な事ですが変換の中心を(0, 0)
にして変換後に戻す処理が必要です。
今回の座標は[0-1]
なのでvec2(0.5)
を引いて後で足すことにします。
1 2 3 4 5 6 7 8 9 10 11 12 |
precision highp float; varying vec2 uv; uniform sampler2D t; uniform float th; void main() { vec2 p = uv - vec2(0.5); float w = 1.0 + p.x*sin(th)/1000.0; float x = p.x*cos(th)/w + 0.5; float y = p.y/w + 0.5; if(x<0.0||x>1.0||y<0.0||y>1.0)discard; gl_FragColor = texture2D(t, vec2(x, y)); } |
想定では画像が中心に向かって小さくなって反対側に伸びるはずですが真逆です。
これはuvの座標に変換後のテクスチャをとっているせいです。
簡単に言うと考え方が逆。
(x, y) => (x cosθ/w, y/w)
の逆で(xw/cosθ, yw) => (x, y)
と考えます。
1 2 3 4 5 6 7 8 9 10 11 12 |
precision highp float; varying vec2 uv; uniform sampler2D t; uniform float th; void main() { vec2 p = uv - vec2(0.5); float w = 1.0 + p.x*sin(th)/1000.0; float x = p.x*w/cos(th) + 0.5; float y = p.y*w + 0.5; if(x<0.0||x>1.0||y<0.0||y>1.0)discard; gl_FragColor = texture2D(t, vec2(x, y)); } |
これでOK。視点dを1000から1に変更してみます。
1 2 3 4 5 6 7 8 9 10 11 12 |
precision highp float; varying vec2 uv; uniform sampler2D t; uniform float th; void main() { vec2 p = uv - vec2(0.5); float w = 1.0 + p.x*sin(th)/1.0; float x = p.x*w/cos(th) + 0.5; float y = p.y*w + 0.5; if(x<0.0||x>1.0||y<0.0||y>1.0)discard; gl_FragColor = texture2D(t, vec2(x, y)); } |
よし上手くいった、と思いきやどこか変です。
回転してこちら側に来た辺が来た方向に戻っていくように見えます。
w
をよく見ると分かるんですが、π/2
、3π/2
でsin(th)
が1付近で増減することを考えるとy
がこうなってしまうのはしかたない。
どこかで計算を間違えたか、途中式を横着して結果だけ逆算したのがまずかったか。
最初からやり直す前にじーっと式を見ているとπ/2
、3π/2
でsin(th)
の符号が反転すればうまくいきそうに見えます。だったらcos
の符号を使ってみましょう。
1 2 3 4 5 6 7 8 9 10 11 12 |
precision highp float; varying vec2 uv; uniform sampler2D t; uniform float th; void main() { vec2 p = uv - vec2(0.5); float w = 1.0 + p.x*sin(th)*sign(cos(th))/1.0; float x = p.x*w/cos(th) + 0.5; float y = p.y*w + 0.5; if(x<0.0||x>1.0||y<0.0||y>1.0)discard; gl_FragColor = texture2D(t, vec2(x, y)); } |
上手くいきました。ちゃんと回っているように見えます。
途中躓きましたが三角関数をある程度理解していれば意外と何とかなりますね。
雑記:Reactでの時間利用
Reactで時間経過による描画を行うのは思ったより面倒です。
gl-reactのtimeLoopサンプルを見るとかなりややこしいことをしています。
今回のサンプルではReactのフックを使って簡易的なタイムループを実装しました。
1 2 3 4 5 6 |
const [th, setTh] = useState(0.0); useEffect(() => { setTimeout(() => { setTh(th + Math.PI / 24); }, 200); }, [th]); |