MAT(Multi Angle Texture)
詳細でリアルな植物の描写がしたかった
植物ってよく見るとすごい詳細にできてますがリアルに描写するのは大変なんですよね。問題は植物のモデルで、毛の一本一本まで全部ポリゴンで表現すれば可能なのでしょうけれど重たそうです。個人的にAndroidのライブ壁紙で雪割草3Dというものを作ったのですが、壁紙ですので可能な限り処理を軽くして、なおかつ見目麗しく()する必要があります。 レンダリング済みの画像を描画するのが軽くて美しいのですが角度に対応できないので使いどころが難しいです。そこでひとつ思いついたのがここで紹介する方法です。ただし実は使い道はかなり限られます。
全ての角度から見た画像を用意する
対象のモデルの前に平面を置いて、その平面上の点をあらゆる角度から見た場合を考えます。 ある角度ではその点の向こう側にモデルが見えますし、また別の角度では何も見えないこともあります。そういった見え方を点ごとに画像に保存します。 もちろん本当に全ての点の角度を保存することはできないので有限数の角度に絞ってサンプリングします。
すると次のような画像が出来上がります。
小さな画像がたくさん集まったようなものになっています。小さな画像一つひとつが、それぞれの点をそれぞれの角度から見た色の集まりです。この画像をテクスチャとして1枚のポリゴンを描画します。点ごとに見る角度に対応した色をテクスチャから取り出して描画すれば、その角度から対象のモデルを見た場合の画像が再生されるはずです。
悲劇
そこで作ってみることにしました。まずテクスチャ用の画像を生成するプログラムを作成します。今思えば最初の三行の時点で何か嫌な予感がしてたんですが。
imageResolution = 2048;
angleResolution = 63;
mapResolution = Math.floor(this.imageResolution / this.angleResolution);
mapResolutionは描画の際に必要な値で、正直いって最初は自分でもよくわからなかったのですが、この場合は32となります。
画像を生成するプログラムは、最初は本当に全ての角度から見た場合をWebGLで描画していたのですが、処理にものすごく時間がかかりました。縦63×63×32×32回モデルを描画する必要があったからです。後からそれぞれのサンプリング点から見た場合の環境マップを生成するのと同じなのだと気づき、シェーダを工夫して高速化したりしました。
次に再生側のプログラムを作成します。とりあえず正面を作ります。UVが0.0~1.0の平面を一枚描画するだけなので、主要な処理はシェーダです。
attribute vec3 aPosition;
attribute vec3 aNormal;
attribute vec2 aTexCoord;
attribute vec3 aTangent;
uniform mat4 uPMatrix;
uniform mat4 uMVMatrix;
uniform mat4 uNormalMatrix;
varying vec2 vTexCoord;
varying vec2 vEyeDirection;
varying float vMask;
uniform float uMapResolution;
uniform float uImageSizeRate;
uniform float uAngleSamplerCenter;
uniform float uAngleSamplerUnit;
uniform sampler2D uTexture0;
void main(void) {
gl_Position = uPMatrix * uMVMatrix * vec4(aPosition, 1.0);
vTexCoord = aTexCoord;
vec3 n = (uNormalMatrix * vec4(aNormal, 1.0)).xyz; // 接空間Normal
vec3 t = (uNormalMatrix * vec4(aTangent, 1.0)).xyz; // 接空間Tangent
vec3 b = cross(n, t); // 接空間Binormal
vec3 viewVec = vec3(0.0, 0.0, -1.0);
vEyeDirection.x = -dot(t, viewVec); // 視線ベクトルの接空間変換(接空間の逆行列=転置行列をかける。)
vEyeDirection.y = dot(b, viewVec);
float eyeDirectionZ = dot(n, viewVec);
vMask = 0.0;
float pi = 3.14159265358979;
if (eyeDirectionZ < -0.0001) {
vEyeDirection.x = (atan(vEyeDirection.x, -eyeDirectionZ)) / pi * 2.0; // 視線ベクトルの角度をとる。これがピクセル毎画像上の位置となる。
vEyeDirection.y = (atan(vEyeDirection.y, -eyeDirectionZ)) / pi * 2.0;
vMask = 1.0;
}
vEyeDirection *= uAngleSamplerUnit;
}
varying vec2 vTexCoord;
varying vec2 vEyeDirection;
varying float vMask;
uniform sampler2D uTexture0;
uniform float uMapResolution;
uniform float uImageSizeRate;
uniform float uAngleSamplerCenter;
void main(void) {
vec2 sampleOrigin = floor(vTexCoord * uMapResolution) / uMapResolution; // UVからピクセル毎画像原点を取得
sampleOrigin *= uImageSizeRate; // 画像の使用されている範囲にスケール
sampleOrigin += uAngleSamplerCenter;// ピクセル毎画像の中央に移動
sampleOrigin += vEyeDirection; // 角度の位置に移動
vec4 texColor = texture2D(uTexture0, sampleOrigin);
gl_FragColor = vec4(texColor.rgb, texColor.a * vMask);
}
それでは結果を見てみましょう。
ドット絵やん…。
32*32のドット絵やん…。
ドット絵はドット絵でいいものだけど今はこれではあかん。(ワロタロは東北人です)
40964096のテクスチャなら6565になりますけど、まだちょっと小さいです。
結局これはなんだったのか
角度を少なくすれば解像度は上がるはずなので、思い切って5角度(angleResolution=5)で試してみました。
これを見てわかったのは、次のような画像を用意して、角度に応じて中から四つ取り出してバイリニアサンプリングするのと同じことだったんですね。逆に言えば、ピクセル間の補間を捨てて角度間の補間にバイリニアサンプリングを使っていたということでもあります。
また、テクスチャ画像の生成では環境マップ生成のようなことをするは必要はなく、ピクセルの再配置だけでできます。
うーん何やってたんだ自分…。
無駄だと思うけど最後までやってみる
でもせっかくなので無理やり360度版を作ってみました。
20482048の画像を6枚も使って3232ピクセルのドット絵のような何かを描画しています!
うーんできたのは嬉しいんだけど何やってんだろう自分(笑)
応用を考える
ともかく、あまり大きな画像を描画することはできないことがわかりました。しかし角度を制限すればある程度大きな画像を描画することもできます。縦横に角度をとるのではなく縦のみあるいは横のみで角度をとればさらに大きな画像を描画できます。円筒形の物体を描画するのに向いているかもしれません。
そこで、次のような毛の生えた枝を表現すべくやってみました。
まずBlenderで横方向だけの回転の画像を作ります。
この画像をピクセルごとに角度を集めた画像に変換します。8192*512ピクセルの画像です。長っ。
これを6枚のポリゴンに張り付けて描画を行います。そのままだと縦方向の回転がかかると平面に見えすぎたので、視差マッピングのような感じでテクスチャの中心あたりでUV座標をずらしたりもしています。
なんかスネ毛みたい。
でもなかなかいいんでないでしょうか。
あとはこれをなんとかして植物に使ってみたいです。