1. Decal 이란
Mesh 표면에 특정 크기의 이미지를 스티커처럼 붙이는 기법.
총알자국, 타이어자국 등에 많이 쓰임.
2. 기본적 수학
여기서 잘 쓰이는 것은 plane normal 과 point 의 관계임.
plane 상의 임의의 점 m 과 임의의 점 p 가 있다고 하면,
Dot(p - m, plane_normal) 은 plane_normal 의 직선에 p - m 을 투영한 것임.
p - m - (Dot(p - m, plane_normal) 은 plane 위에 p - m 을 투영한 것임.
Picking
bool Math::RayTriangleIntersection(const Vector3& ray_pos, const Vector3& ray_dir, const Vector3& p0, const Vector3& p1, const Vector3& p2, Vector3& out_hit_pos, Vector3& out_hit_norm, float& farT) {
// Find the triangles normal Vector3 v10 = p1 - p0; Vector3 v20 = p2 - p0; Vector3 normal = Vector3::Cross(v10, v20).Normalize(); const float d = Vector3::Dot(normal, ray_dir); if (abs(d) >= 0.0001f) { // let intersect point as p. // let point of (p0 - ray_pos) projected by normal as t // dot(normal, all points in triangle - ray_pos) is same as normal is orthogonal with faces. // dot(normal, ray_dir) = cos(theta) // = dot(normal, p0 - ray_pos) / (p - ray_pos) // = dot(normal, p - ray_pos) / (p - ray_pos) // so hitT is (p - ray_pos) float hitT = Vector3::Dot(normal, p0 - ray_pos) / d; if (hitT > 0.0f && hitT < farT) { Vector3 hit_pos = ray_pos + ray_dir * hitT; Vector3 vh0 = hit_pos - p0; // v10 v20 u = vh0 // v10 v20 v vh0 // // dot1020 dot2020 u = dot20h0 // dot1010 dot1020 v dot10h0 // // u = 1 dot1020 -dot2020 dot20h0 // v det -dot1010 dot1020 dot10h0 float dot1010 = Vector3::Dot(v10, v10); float dot2020 = Vector3::Dot(v20, v20); float dot1020 = Vector3::Dot(v10, v20); float dot20h0 = Vector3::Dot(v20, vh0); float dot10h0 = Vector3::Dot(v10, vh0); const float det = 1.0f / (dot1020 * dot1020 - dot1010 * dot2020); float u = (dot1020 * dot20h0 - dot2020 * dot10h0) * det; float v = (dot1020 * dot10h0 - dot1010 * dot20h0) * det; if (u >= 0.0f && u <= 1.0f && v >= 0.0f && v <= 1.0f && u + v <= 1.0f) { farT = hitT; out_hit_pos = hit_pos; out_hit_norm = normal; return true; } } return false; }
응용하면 Picking 도 계산할 수 있음.
ray_point, ray_dir 이 있다고 하고 mesh 의 face 를 이루는 v0, v1, v2 가 있다고 하면,
우리는 그 face 를 확장한 평면을 가로지르는 점 p 를 찾아야 함.
만약 p = u * (v1 - v0) + v * (v2 - v0) 인 uv 가 0~1 사이면서 합이 1 보다 작으면 face 내부에 p 가 있는 것임.
다시 정리하면 v0, v1, v2 를 우리는 알고 있고 따라서 plane_normal 도 알고 있으며, ray_pos, ray_dir 역시 주어져 있음.
dot(ray_pos - p, plane_normal) = dot(ray_pos - v0, plane_normal) 이 성립함. 왜냐하면 p 는 평면에 있기 때문에 어떤 plane 위의 점과 바꿔도 되기 때문임.
dot(ray_dir, plane_normal) = dot(ray_pos - v0, plane_normal) / len(p - ray_pos) 이 성립함. p - ray_pos 을 normal 에 투영한 길이가 dot(ray_pos - v0, plane_normal) 이며, dot 은 cos(theta) 를 의미하기 때문에 투영된길이/빗면 가 되기 때문임.
그럼 dot(ray_pos - v0, plane_normal) / dot(ray_dir, plane_normal) = 빗면의 길이가 성함.
위 값을 t 라고 하면 ray_pos + t * ray_dir = p 가 됨.
3. Decal Square
void DecalGenGS() { uint n_vertex = 0; Vector4 new_vertices[MAX_NEW_VERT] = {100000,100000, 100000, 100000, 100000, 100000}; Vector3 new_normals[MAX_NEW_VERT] = {0,}; float dots[MAX_NEW_VERT] = { 0 }; new_vertices[0] = {-1, 0, -1, 1}; new_vertices[1] = {0, 2, 0, 1}; new_vertices[2] = {1, 0, 1, 1}; new_normals[0] = {-1, 0, 0}; new_normals[1] = {-1, 0, 0}; new_normals[2] = {-1, 0, 0}; float decal_size = 0.5; Vector3 hit_point = { 0,0,0 }; Vector3 hit_normal = { 1, 0, 0 }; Vector4 clip_planes[6] = { {+1, 0, 0, -Vector3::Dot({+1,0,0}, hit_point) + decal_size/2}, {-1, 0, 0, -Vector3::Dot({-1,0,0}, hit_point) + decal_size/2}, {0, +1, 0, -Vector3::Dot({0,+1,0}, hit_point) + decal_size/2}, {0, -1, 0, -Vector3::Dot({0,-1,0}, hit_point) + decal_size/2}, {0, 0, +1, -Vector3::Dot({0,0,+1}, hit_point) + decal_size/2}, {0, 0, -1, -Vector3::Dot({0,0,-1}, hit_point) + decal_size/2}, }; // Make sure the triangle is not facing away from the hit ray Vector3 AB = new_vertices[1].xyz() - new_vertices[0].xyz(); Vector3 AC = new_vertices[2].xyz() - new_vertices[0].xyz(); Vector3 face_normal = Vector3::Cross(AB, AC); n_vertex = 3 * (Vector3::Dot(face_normal, hit_normal) > 0.01); // 0 이면 이후에서 알아서 걸러짐 // Clip the triangle with each one of the planes for (uint iCurPlane = 0; iCurPlane < 6; iCurPlane++) { // First check the cull status for each vertex for (uint i = 0; i < MAX_NEW_VERT; i++) { dots[i] = Vector4::Dot(clip_planes[iCurPlane], new_vertices[i]); } // Calculate the new vertices based on the culling status PolyPlane(dots, new_vertices, new_normals, n_vertex, clip_planes[iCurPlane]); }
...
}
어찌 돌아가는지 볼려고 c++로 대충 작성해서 hlsl 으로 포팅할 필요가 있음.
실제론 geometry shader 이고 결과값을 rendering 하는게 아니라 output stream 으로 보냄.
위에서 주의해야할 것은 clip_planes 의 w 값임. -Dot(plane_normal, hit_point) + decal_size/2 가 들어있는데 clip_planes 는 나중에 Vector4(vertex, 1) 과 Dot 을 할 것임. 그러면 Dot(plane_normal, vertex - hit_point) + decal_size 가 되고 앞부분은 위에서 언급한 수학적 지식에 대응함. 1보다 크면 plane 바깥이고(노멀기준) 작으면 plane 안임. 여기서 decal_size 는 Dot 의 결과값이 normal 에 투영한 길이이므로 이 길이에서 증가한다고 생각하면 육면체의 안쪽면에 대응함을 알 수 있음.
void PolyPlane(float4 vertices[MAX_NEW_VERT], float3 normals[MAX_NEW_VERT], float dots[MAX_NEW_VERT], uint n_vertex, float4 plane, out float4 out_vertices[MAX_NEW_VERT], out float4 out_normals[MAX_NEW_VERT], out uint out_n_vertex) { out_vertices = (float4[MAX_NEW_VERT]) 100000.0f; out_n_vertex = 0; for (uint i = 0; i < n_vertex; i++) { if (dots[i] >= 0) // dots 의 부호가 바뀌면 면과 접한다는 소리 { out_vertices[out_n_vertex] = vertices[i]; out_normals[out_n_vertex] = normals[i]; out_n_vertex++; if (dots[(i + 1) % n_vertex] < 0) // 부호바뀜 { PlaneSegIntersec(vertices[i], vertices[(i + 1) % n_vertex], normals[i], normals[(i + 1) % n_vertex], plane, out_vertices[out_n_vertex], out_normals[out_n_vertex]); out_n_vertex++; } } else if (dots[(i + 1) % n_vertex] >= 0) { PlaneSegIntersec(vertices[i], vertices[(i + 1) % n_vertex], normals[i], normals[(i + 1) % n_vertex], plane, out_vertices[out_n_vertex], out_normals[out_n_vertex]); out_n_vertex++; } } }
위 함수에서 고려사항은 2가지임.
첫째는 dots 결과값이 음수이면 기존의 vertex 버림. 앞에서 말했듯이 음수이면 육면체 바깥이기 때문에 버리는 것.
둘째는 dots 의 결과값의 부호가 바뀌면 plane 과 접하는 정점을 추가함.
이걸 6개의 면에서 수행하면 육면체 내부에 있는 vertex 를 모두 육면체에 투영시킬 수 있음. 그리고 외부에 있는 정점은 버릴 수 있음. 만약 외부에 있다고 해도 정점을 잇는 선이 육면체와 접하면 이 점을 이용함. mesh 의 모든 vertex 에 대해 이를 수행하게 됨.
void PlaneSegIntersec(float4 p1, float4 p2, float3 norm1, float3 norm2, float4 plane, out float4 intersect_pos, out float3 intersect_norm) { float3 segDir = p2.xyz - p1.xyz; float segDist = length(segDir); segDir = segDir / segDist; // dot(plane.xyz, segDir) = cos(theta) = -dot(plane, p1) / len(p1 - any point of plane) // p1 -> p2 가 dot 부호가 반대라 - 붙인것. // dot(plane, p1) 은 벡터인데 segDir 과 진행방향이 이 상황에선 반대라서 segDir 방향으로 움직이기 위해 이렇게 됨. // dist = len(p1 - any point of plane) float dist = -dot(plane, p1) / dot(plane.xyz, segDir); intersect_pos = float4(p1.xyz + dist * segDir, 1.0); intersect_norm = normalize(lerp(norm1, norm2, dist / segDist)); }
이는 면과 직선이 접하면 접하면 정점을 구하는 함수임.
여기서 plane 은 plane 의 normal 을 말함.
위에서 picking 할 때와 -부호 붙인거 제외하면 똑같음.
dot(plane.xyz, segDir) 에서 segDir 이 육면체 안에서 바깥으로 가는 직선이 되므로 plane 과 반대방향이라 음수가 됨. 그래서 - 를 붙여 양수로 만드는 것. (길이를 구하는 거니까)
// Add the new triangles to the stream for (uint i = 1; i < n_vertex - 1 && i > 0; i++) { output.position = new_vertices[0]; output.normal = new_normals[0]; output.uv.x = dot(new_vertices[0], clip_planes[1]); // -right output.uv.y = dot(new_vertices[0], clip_planes[3]); // up output.uv = output.uv / decal_size; TriStream.Append(output); output.position = new_vertices[i]; output.normal = new_normals[i]; output.uv.x = dot(new_vertices[i], clip_planes[1]); output.uv.y = dot(new_vertices[i], clip_planes[3]); output.uv = output.uv / decal_size; TriStream.Append(output); output.position = new_vertices[i + 1]; output.normal = new_normals[i + 1]; output.uv.x = dot(new_vertices[i + 1], clip_planes[1]); output.uv.y = dot(new_vertices[i + 1], clip_planes[3]); output.uv = output.uv / decal_size; TriStream.Append(output); TriStream.RestartStrip(); }
위의 DecalGenGS 의 나머지 부분임.
결과값이 삼각형에서 시작해서 정점이 추가되어 다각형이 만들어짐.
이걸 다시 삼각형으로 쪼개는데 코드대로 (v0, vi v(i+1)) 을 반복하면 됨.