2021년 3월 12일 금요일

3D graphic - Decal

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)) 을 반복하면 됨. 


 

 

List