最初に
一時期、自分の席の周りで車のレンダリングの話をしているのが聞こえてきた時期がありました。さすがに、GTとかのレベルには到底及びませんが、自分で実装してみたくなりました。
どう実装しようかなとおもっていたところで、Layered Car Paint Shaderを見つけました。DirectX9時代の内容ですが、まずは取っ掛かりとしてこれを試してみようと思いました。
しかし、1つ問題がありました。Flakes Normal Mapをどうやって作るのか、という問題です。
Flakes Normal Mapについて調べてみる
いろいろと調べたところ、オフラインレンダラで有名なVRayを開発しているChaosGroupがそのままドンピシャのFlakes Normal Mapを作るシェーダコードを公開していました。
「これをそのまま流用すれば、Flakes Normal Mapを作れる」と安易に考えたのですが、そうは問屋が卸しませんでした。
OSL(Open Shader Language)
公開されていたソースは OSLで記述されていました。
OSLとはOpen Shading Languageの略で、もともとはSony Picutresで開発されたオフラインレンダラ向けのプログラマブルシェーディング言語だという理解です。
最初は、「どうせほとんどGLSLと同じだろう」とタカをくくっていたのですが、1つだけGLSLに存在しない命令が存在していました。それは cellnoise という命令です。
Cellular Noise
ということで、自分で実装しなければならない、ということで、まずCellular Noiseについて調べてみましたが・・・。
うん、よくわからない。どうも、セルに区切って、距離関数であんなこと、こんなことしているらしいですが、ボロノイみたいなものでしょうか。しかし、具体的な実装のコードがいくつも見つかったので、このま使わせていただけば解決だ、と思ったのですが・・・。
いろいろ調べたCellular Noiseのコードはすべて1 or 2次元の結果を返すものでした。しかし、今回必要なのは、3次元の結果を返すものです。そこで、1次元の結果を返すものから拡張することで、対応できるのではと考え、いろいろ試してみたのですが、いまいちうまくいきませんでした。
OSL(Open Shader Language)ソースコード入手
さらに調査をしたところ、OSL(Open Shader Language)のソースコードがgithubに公開されていました。このコードを調べれば、cellnoiseの実装がわかるのではと思い、調べていくと、まさにドンピシャそのものを発見することができました。
GLSL化
というわけで、コピペしまくりではありますが、Flakes Normal Mapを生成するGLSLコードは以下のようになります。
#version 450
precision highp float;
precision highp int;
uniform vec4 u_resolution;
float bits_to_01(uint bits)
{
// divide by 2^32-1
uint div = 0xffffffff;
return bits * (1.0 / float(div));
}
uint rotl32(uint var, uint hops)
{
return (var << hops) | (var >> (32 - hops));
}
// Bob Jenkins "lookup3" hashes: http://burtleburtle.net/bob/c/lookup3.c
// It's in the public domain.
// Mix up the bits of a, b, and c (changing their values in place).
void bjmix(inout uint a, inout uint b, inout uint c)
{
a -= c; a ^= rotl32(c, 4); c += b;
b -= a; b ^= rotl32(a, 6); a += c;
c -= b; c ^= rotl32(b, 8); b += a;
a -= c; a ^= rotl32(c, 16); c += b;
b -= a; b ^= rotl32(a, 19); a += c;
c -= b; c ^= rotl32(b, 4); b += a;
}
// Mix up and combine the bits of a, b, and c (doesn't change them, but
// returns a hash of those three original values). 21 ops
uint bjfinal(uint a, uint b, uint c)
{
c ^= b; c -= rotl32(b, 14);
a ^= c; a -= rotl32(c, 11);
b ^= a; b -= rotl32(a, 25);
c ^= b; c -= rotl32(b, 16);
a ^= c; a -= rotl32(c, 4);
b ^= a; b -= rotl32(a, 14);
c ^= b; c -= rotl32(b, 24);
return c;
}
uint inthash(uvec4 k)
{
int N = 4;
// now hash the data!
uint len = N;
uint a = 0xdeadbeef + (len << 2) + 13;
uint b = 0xdeadbeef + (len << 2) + 13;
uint c = 0xdeadbeef + (len << 2) + 13;
a += k[0];
b += k[1];
c += k[2];
bjmix(a, b, c);
a += k[3];
c = bjfinal(a, b, c);
return c;
}
vec3 hash3(uvec4 k)
{
int N = 4;
vec3 result;
k[N - 1] = 0;
result.x = bits_to_01(inthash(k));
k[N - 1] = 1;
result.y = bits_to_01(inthash(k));
k[N - 1] = 2;
result.z = bits_to_01(inthash(k));
return result;
}
vec3 cellnoise(vec3 p)
{
uvec4 iv;
iv[0] = uint(floor(p.x));
iv[1] = uint(floor(p.y));
iv[2] = uint(floor(p.z));
vec3 result = hash3(iv);
return result;
}
// https://docs.chaosgroup.com/display/OSLShaders/Flakes+normal+map
uniform float flake_scale = 50.0; // Smaller values zoom into the flake map, larger values zoom out.
uniform float flake_size = 0.5; // Relative size of the flakes
uniform float flake_size_variance = 0.7; // 0.0 makes all flakes the same size, 1.0 assigns random size between 0 and the given flake size
uniform float flake_normal_orientation = 0.5; // Blend between the flake normals (0.0) and the surface normal (1.0)
void flakes(
float u,
float v,
out vec3 result,
out float alpha)
{
float safe_flake_size_variance = clamp(flake_size_variance, 0.1, 1.0);
vec3 cellCenters[9] = {
vec3(0.5, 0.5, 0.0),
vec3(1.5, 0.5, 0.0),
vec3(1.5, 1.5, 0.0),
vec3(0.5, 1.5, 0.0),
vec3(-0.5, 1.5, 0.0),
vec3(-0.5, 0.5, 0.0),
vec3(-0.5, -0.5, 0.0),
vec3(0.5, -0.5, 0.0),
vec3(1.5, -0.5, 0.0)
};
vec3 position = vec3(u, v, 0.0);
position = flake_scale * position;
vec3 base = floor(position);
vec3 nearestCell = vec3(0.0, 0.0, 1.0);
int nearestCellIndex = -1;
for (int cellIndex = 0; cellIndex < 9; ++cellIndex) {
vec3 cellCenter = base + cellCenters[cellIndex];
vec3 centerOffset = cellnoise(cellCenter) * 2.0 - 1.0;
centerOffset[2] *= safe_flake_size_variance;
centerOffset = normalize(centerOffset);
cellCenter += 0.5 * centerOffset;
float cellDistance = distance(position, cellCenter);
if (cellDistance < flake_size && cellCenter[2] < nearestCell[2]) {
nearestCell = cellCenter;
nearestCellIndex = cellIndex;
}
}
result = vec3(0.5, 0.5, 1.0);
alpha = 0.0;
vec3 I = vec3(0, 0, 1);
if (nearestCellIndex != -1) {
vec3 randomNormal = cellnoise(base + cellCenters[nearestCellIndex] + vec3(0.0, 0.0, 1.5));
randomNormal = 2.0 * randomNormal - 1.0;
randomNormal = faceforward(randomNormal, I, randomNormal);
randomNormal = normalize(mix(randomNormal, vec3(0.0, 0.0, 1.0), flake_normal_orientation));
result = vec3(0.5*randomNormal[0] + 0.5, 0.5*randomNormal[1] + 0.5, randomNormal[2]);
alpha = 1.0;
}
}
void main()
{
vec2 uv = gl_FragCoord.xy / u_resolution.xy;
vec3 result;
float alpha;
flakes(
uv.x, uv.y,
result,
alpha);
gl_FragColor.xyz = result;
gl_FragColor.w = alpha;
}
下は結果の図です。解像度は、512x512で出力しています。
パラメータは、flake_scale = 100.0, flake_size = 0.5, flake_size_variance = 0.7, flake_normal_orientation = 0.5をセットしています。
いまいちこれが正しいのかどうかわかりませんが、それっぽくなっているのではないでしょうか。
カーペイント
車のモデルでないので、それぽく見えずにいまいちうまくいっているのかわかりません。
やっぱり、ちょっと古い手法なのか、かなりいまいちな気もします・・・。