したいですよね。
でもよくある方法だとなんか面白くないのでちょっと変なことを考えました。
基本的にはやっぱり marching cubes だとは思うんですが、線が綺麗に出すぎるイメージがありまし。
何故綺麗かというと点が綺麗に並んでるからだと思うので、点を雑に並べればいいと思いました。
三次元単体である三角錐の各頂点で距離場をサンプリングするとしたとき、場の線形性を仮定すると形成される表面 (値が0となる平面) を決定できます。
つまり空間を適当に三次元単体に分割してあげて、各単体で断面を構成してあげればそれっぽくなる気がする。というわけです。距離場は大凡線形ですし。
単体分割と言えば ドロネー図 ですが、今回は3次元なのでちょっと実装が大変そうです。
とは言え元々の目的である「乱雑さ」を確保するためにはやらざるを得ません。どうしよう。
ところで、ここに Light Probe Interpolation Using Tetrahedral Tesselations というスライドがあります。
えー。Unityはある物体への光の寄与を決定するために高々周囲4つの light probe をサンプリングしますが、これは単体分割された空間をてくてく歩いていくことで実装されているらしいですね。
ということは内部的に light probe からなる3次元ドロネー図を構成しているはずです。羨ましいなー。
…えーと。
なんとこの関数、普通に使えます。しかもインタフェースが完璧。
というわけでUnityには3次元ドロネー図を計算してくれる神託が居るので、あとはにゅってやってあげるとやりたかったことができます。
三角錐の断面は三角形か四角形なので、各単体に2ポリゴン (6頂点) 割り当ててあげます。4つの頂点位置はPositionとNormalとUVとTangentに突っ込みました。
Vector3[] pos = new Vector3[pointCount]; // assign positions... int[] tetraIndices; Vector3[] tetraPositions; Lightmapping.Tetrahedralize(pos, out tetraIndices, out tetraPositions); for(int i=0;i<tetraIndices.Length;i+=4) { Vector3 p0 = tetraPositions[tetraIndices[i+0]]; Vector3 p1 = tetraPositions[tetraIndices[i+1]]; Vector3 p2 = tetraPositions[tetraIndices[i+2]]; Vector3 p3 = tetraPositions[tetraIndices[i+3]]; Vector4 ta = new Vector4(p2.x, p2.y, p2.z, p3.x); Vector4 uv = new Vector2(p3.y, p3.z); int N = vertices.Count; for(int j=0;j<6;j++) { vertices.Add(p0); normals.Add(p1); tangents.Add(ta); uvs.Add(uv); } uv2s.Add(new Vector2(0,0)); uv2s.Add(new Vector2(0,1)); uv2s.Add(new Vector2(0,2)); uv2s.Add(new Vector2(1,0)); uv2s.Add(new Vector2(1,1)); uv2s.Add(new Vector2(1,2)); indices.Add(N+0); indices.Add(N+1); indices.Add(N+2); indices.Add(N+3); indices.Add(N+4); indices.Add(N+5); }
断面はまぁ適当に計算します。
float3 le(float3 a, float3 b, float p, float q) { // p * (1-t) + q * t = 0 float t = - p / (q - p); return lerp(a, b, t); } void el(float3 p0, float3 p1, float3 p2, float3 p3, int2 loc, out float3 position, out float3 normal) { float3 verts[4]; verts[0] = mul(UNITY_MATRIX_M, float4(p0,1)); verts[1] = mul(UNITY_MATRIX_M, float4(p1,1)); verts[2] = mul(UNITY_MATRIX_M, float4(p2,1)); verts[3] = mul(UNITY_MATRIX_M, float4(p3,1)); float val[4]; val[0] = field(verts[0]); val[1] = field(verts[1]); val[2] = field(verts[2]); val[3] = field(verts[3]); int c = 0; for (int i = 0; i < 4; i++) if (val[i] < 0) c++; if (c == 0 || c == 4) { position = normal = 0; return; } #define SORT(i,j) if(val[i] > val[j]) { float3 tv = verts[i]; verts[i] = verts[j]; verts[j] = tv; float3 ti = val[i]; val[i] = val[j]; val[j] = ti; } SORT(0, 2); SORT(1, 3); SORT(0, 1); SORT(2, 3); SORT(1, 2); #undef SORT float3 el[4]; if (c == 2) { el[0] = le(verts[0], verts[2], val[0], val[2]); el[1] = le(verts[0], verts[3], val[0], val[3]); el[2] = le(verts[1], verts[2], val[1], val[2]); el[3] = le(verts[1], verts[3], val[1], val[3]); position = el[loc.x + loc.y]; normal = nf(position); return; } if (loc.x == 1) { position = normal = 0; return; } if(c == 1) { float3 tv = verts[0]; float3 ti = val[0]; el[0] = le(tv, verts[1], ti, val[1]); el[1] = le(tv, verts[2], ti, val[2]); el[2] = le(tv, verts[3], ti, val[3]); el[3] = 0; } else { float3 tv = verts[3]; float3 ti = val[3]; el[0] = le(tv, verts[0], ti, val[0]); el[1] = le(tv, verts[1], ti, val[1]); el[2] = le(tv, verts[2], ti, val[2]); el[3] = 0; } position = el[loc.y]; normal = nf(position); }
唯一気をつけなきゃいけないのが、どうやら Tetrahedralize は入力頂点位置が近すぎると同一視するみたいなので「全体に一定スケールで拡大してから分割させて」「出力位置を縮小して使う」必要があることがある点です。light probe をどうにかする為と考えればまぁ当然ですね。
はい。
何かに使えるかな~と思って作ったんですが、まぁ当分使わなそうなのと。あと Lightmapping.Tetrahedralize
という関数があってウケる、という話がしたくて書きました。
使ったり使わなかったりすると良いと思います。
おわり。