수학/이득우의 게임 수학 - 실습 예제

텍스처 매핑

monstro 2025. 12. 17. 11:53
728x90
반응형

- 개요

무게중심좌표는 메시에 이미지를 입히는 용도로도 활용할 수 있다

메시에 이미지를 입히기 위해 변환된 데이터텍스처라고 하며, 이미지를 입히는 작업텍스처 매핑이라고 한다

 

텍스처 예시 - 이미지 출처 : https://www.turbosquid.com/3d-models/alex-minecraft-mixamo-animatable-use-your-own-skin-3d-1738605?dd_referrer=

 

텍스처의 특징은 다음과 같다

  • 관리 방식을 통일하기 위해 이미지의 원본 해상도와 관계없이 텍스처의 가로 + 세로 크기는 1로 정규화
  • 따라서 렌더링하기 위해 텍스처를 사용하는 경우 [0, 1]의 범위로 구성된 2차원 좌표계를 사용
  • 텍스처를 구성하는 2차원 좌표계를 UV 좌표계라고 표현
  • UV 좌표계는 2차원 벡터로 구성되며 가로 정보를 U로, 세로 정보를 V로 지정

 

텍스처의 UV 좌표값

 

UV 좌표계를 위와 같이 형성하였다면 텍스처 매핑을 위해 삼각형의 세 정점UV 좌표를 추가해야 한다

이때 각 픽셀의 무게중심좌표를 계산하고 그 값을 정점의 UV 좌표 선형보간하면 픽셀의 UV 값을 얻을 수 있다

얻어온 픽셀의 UV 값과 대응하는 텍스처의 색상 정보를 가져와 최종 픽셀의 색상으로 지정하면 텍스처 매핑이 완료된다

 

- 예제

...

// 렌더하는 함수
void SoftRenderer::Render2D()
{
    // 렌더하기 위해 가져오는 레퍼런스
    auto& r = GetRenderer();
    const auto& g = Get2DGameEngine();
    const auto& texture = g.GetTexture(GameEngine::BaseTexture);

    // 배경에 격자 그리기
    DrawGizmo2D();

    // 그릴 메시 데이터의 선언 -> 렌더링할 정사각형의 절반 크기 / 정점의 개수 / 삼각형의 개수
    static constexpr float squareHalfSize = 0.5f;
    static constexpr size_t vertexCount = 4;
    static constexpr size_t triangleCount = 2;

    // 정점 버퍼를 생성 -> 첫 번째 정점(-0.5, -0.5) / 두 번째 정점(-0.5, 0.5) / 세 번째 정점(0.5, 0.5) / 네 번째 정점(0.5, -0.5)
    // 정점별 색상 정보는 기본값으로 설정
    // 추가적으로 정점별로 UV 값을 설정
    static constexpr std::array<Vertex2D, vertexCount> rawVertices = {
        Vertex2D(Vector2(-squareHalfSize, -squareHalfSize), LinearColor(), Vector2(0.125f, 0.75f)),
        Vertex2D(Vector2(-squareHalfSize, squareHalfSize), LinearColor(), Vector2(0.125f, 0.875f)),
        Vertex2D(Vector2(squareHalfSize, squareHalfSize), LinearColor(), Vector2(0.25f, 0.875f)),
        Vertex2D(Vector2(squareHalfSize, -squareHalfSize), LinearColor(), Vector2(0.25f, 0.75f))
    };

    // 인덱스 버퍼를 생성 
    // 첫 번째 삼각형의 정점 순회 : 첫 번째 정점 -> 두 번째 정점 -> 세 번째 정점
    // 두 번째 삼각형의 정점 순회 : 첫 번째 정점 -> 세 번째 정점 -> 네 번째 정점
    static constexpr std::array<size_t, triangleCount * 3> indices = {
        0, 1, 2,
        0, 2, 3
    };

    // 크기 변환 행렬
    Vector3 sBasis1(currentScale, 0.f, 0.f);
    Vector3 sBasis2(0.f, currentScale, 0.f);
    Vector3 sBasis3 = Vector3::UnitZ;
    Matrix3x3 sMatrix(sBasis1, sBasis2, sBasis3);

    // 회전 변환 행렬
    float sin = 0.f, cos = 0.f;
    Math::GetSinCos(sin, cos, currentDegree);
    Vector3 rBasis1(cos, sin, 0.f);
    Vector3 rBasis2(-sin, cos, 0.f);
    Vector3 rBasis3 = Vector3::UnitZ;
    Matrix3x3 rMatrix(rBasis1, rBasis2, rBasis3);

    // 이동 변환 행렬 
    Vector3 tBasis1 = Vector3::UnitX;
    Vector3 tBasis2 = Vector3::UnitY;
    Vector3 tBasis3(currentPosition.X, currentPosition.Y, 1.f);
    Matrix3x3 tMatrix(tBasis1, tBasis2, tBasis3);

    // 모든 아핀 변환을 곱한 합성 행렬 생성, 크기-회전-이동 순으로 적용
    Matrix3x3 finalMatrix = tMatrix * rMatrix * sMatrix;

    // 행렬 곱이 적용된 각 정점을 렌더링에 사용할 정점 버퍼에 저장
    // 추가적으로 UV값도 각 정점에 적용
    static std::vector<Vertex2D> vertices(vertexCount);
    for (size_t vi = 0; vi < vertexCount; ++vi)
    {
        vertices[vi].Position = finalMatrix * rawVertices[vi].Position;
        vertices[vi].UV = rawVertices[vi].UV;
    }

    // 변환된 정점들 간의 선 그리기
    for (size_t ti = 0; ti < triangleCount; ++ti)
    {
        // 삼각형을 구성하는 3개의 점을 배열 tv에 저장
        size_t bi = ti * 3;
        std::array<Vertex2D, 3> tv = { vertices[indices[bi]] , vertices[indices[bi + 1]], vertices[indices[bi + 2]] };

        // 세 점의 좌표 중에서 최소 X/Y 값을 minPos 변수에 저장 + 최대 X/Y 값을 maxPos 변수에 저장
        Vector2 minPos(Math::Min3(tv[0].Position.X, tv[1].Position.X, tv[2].Position.X), Math::Min3(tv[0].Position.Y, tv[1].Position.Y, tv[2].Position.Y));
        Vector2 maxPos(Math::Max3(tv[0].Position.X, tv[1].Position.X, tv[2].Position.X), Math::Max3(tv[0].Position.Y, tv[1].Position.Y, tv[2].Position.Y));

       // 첫 번째 점에서 두 번째 점으로 향하는 벡터 u와 첫번째 점에서 세 번째 점으로 향하는 벡터 v 생성
        Vector2 u = tv[1].Position - tv[0].Position;
        Vector2 v = tv[2].Position - tv[0].Position;

        // 무게중심좌표 계산식에서의 공통 분모 생성
        float udotv = u.Dot(v);
        float vdotv = v.Dot(v);
        float udotu = u.Dot(u);
        float denominator = udotv * udotv - vdotv * udotu;

        // 공통분모가 0인 퇴화삼각형이라면 Render하지 않음
        if (denominator == 0.0f)
        {
            continue;
        }

        // 1/공통분모를 계산
        float invDenominator = 1.f / denominator;

        // minPos 변수와 maxPos 변수로 구성된 데카르트 좌표계의 벡터 좌표를 스크린 좌표계의 픽셀로 변환
        ScreenPoint lowerLeftPoint = ScreenPoint::ToScreenCoordinate(_ScreenSize, minPos);
        ScreenPoint upperRightPoint = ScreenPoint::ToScreenCoordinate(_ScreenSize, maxPos);

        // 두 점이 화면 밖을 벗어나는 경우 클리핑 처리
        lowerLeftPoint.X = Math::Max(0, lowerLeftPoint.X);
        lowerLeftPoint.Y = Math::Min(_ScreenSize.Y, lowerLeftPoint.Y);
        upperRightPoint.X = Math::Min(_ScreenSize.X, upperRightPoint.X);
        upperRightPoint.Y = Math::Max(0, upperRightPoint.Y);

        // 삼각형을 둘러싼 사각형 영역의 픽셀을 모두 순회
        for (int x = lowerLeftPoint.X; x <= upperRightPoint.X; ++x)
        {
            for (int y = upperRightPoint.Y; y <= lowerLeftPoint.Y; ++y)
            {
                // 벡터의 내적을 사용하기 위해 스크린 좌표계의 픽셀을 다시 데카르트 좌표계의 벡터로 변환
                ScreenPoint fragment = ScreenPoint(x, y);
                Vector2 pointToTest = fragment.ToCartesianCoordinate(_ScreenSize);

                // 무게중심좌표의 분자값 계산에 사용할 벡터의 내적값 계산
                Vector2 w = pointToTest - tv[0].Position;
                float wdotu = w.Dot(u);
                float wdotv = w.Dot(v);

                // 최종 무게중심좌표 계산
                // s 스칼라 + t 스칼라 + (1 - s - t) 스칼라의 값 계산
                float s = (wdotv * udotv - wdotu * vdotv) * invDenominator;
                float t = (wdotu * udotv - wdotv * udotu) * invDenominator;
                float oneMinusST = 1.f - s - t;

                // 계산한 세개의 무게중심좌표의 값이 [0, 1]의 범위내에 있는 경우에만 점을 찍음
                // 이때 각 정점에 적용된 UV 값을 무게중심좌표로 선형 보간하여 반영 + UV 값에 해당하는 텍스처의 색상 정보를 반영
                if (((s >= 0.f) && (s <= 1.f)) && ((t >= 0.f) && (t <= 1.f)) && ((oneMinusST >= 0.f) && (oneMinusST <= 1.f)))
                {
                    Vector2 targetUV = tv[0].UV * oneMinusST + tv[1].UV * s + tv[2].UV * t;
                    r.DrawPoint(fragment, texture.GetSample(targetUV));
                }
            }
        }
    }

    // 현재 위치, 크기, 각도를 화면에 출력
    r.PushStatisticText(std::string("Position : ") + currentPosition.ToString());
    r.PushStatisticText(std::string("Scale : ") + std::to_string(currentScale));
    r.PushStatisticText(std::string("Degree : ") + std::to_string(currentDegree));
}

...

 

동일하게 [0, 1]의 범위내에 있는 픽셀들을 순회하면서 픽셀을 그리도록 구성하였다

다만 이번에는 픽셀의 정점에 부여된 UV 값을 사용하여 텍스처에서 대응하는 색상을 가져와 텍스처 매핑한다

 

- 최종 실행 결과

728x90
반응형