[Ray Tracing] 월드좌표 도형에 텍스춰링 하는 방법
텍스처링(Texture Mapping)은 3D 그래픽스에서 객체의 표면에 이미지를 입혀 시각적 디테일을 추가하는 기술입니다. 간단히 말해, 3D 모델의 표면에 2D 이미지를 적용하는 과정입니다.
이번 개념 설명과 실습에서는 이해하기 쉽게 하나의 사각형에 이미지를 입혀볼 것입니다.
사각형에 이미지를 입히는 방법은 일단 월드 좌표를 텍스쳐 좌표로 맞추고, 해당 이미지 좌표의 색을 픽셀 값에 넣어주는 것입니다. 여기서 이미지 좌표의 해당 값을 가져오는 과정을 샘플링(Sampling)이라고 합니다.
여기서 텍스쳐 좌표란 사각형(월드좌표로 표현됨)을 x,y[0,1][0,1]로 변환한 좌표계입니다.
이미지 좌표란 가져올 이미지의 좌표, 예를들어 4*4화소의 이미지가 있다면 [0,4][0,4]가 이미지 좌표입니다.
Ray를 쏴서 사각형의 어떤 지점에 맞았다고 합시다. 그 지점은 P입니다. 그 사각형의 텍스쳐좌표[0,1][0,1]에 해당하는 값을 이미지 좌표에서 가져와서 P에 해당하는 픽셀에 값을 넣어 줍시다. 다음 그림을 보며 이해해봅시다.
왼쪽은 사각형의 좌표를 월드좌표(v0, v1, v2, v3)로 나타낸 것이고, 오른쪽은 사각형을 텍스쳐 좌표로 변환한 값(uv0, uv1, uv2, uv3)로 나타낸 것입니다. 여기서 유의할 점은 v0과 uv0, v1과 uv1, v2와 uv2, v3와 uv3은 대응 되어야 한다는 점입니다. 즉 꼭짓점의 위치가 바뀌면 안됩니다. 코드의 형태는 다음과 같습니다.
class Square : public Object
{
public:
Triangle triangle1, triangle2;
Square(vec3 v0, vec3 v1, vec3 v2, vec3 v3, vec2 uv0 = vec2(0.0f), vec2 uv1 = vec2(0.0f), vec2 uv2 = vec2(0.0f), vec2 uv3 = vec2(0.0f))
: triangle1(v0, v1, v2), triangle2(v0, v2, v3)
{
triangle1.uv0 = uv0;
triangle1.uv1 = uv1;
triangle1.uv2 = uv2;
triangle2.uv0 = uv0;
triangle2.uv1 = uv2;
triangle2.uv2 = uv3;
}
..
//사각형 생성
auto square = make_shared<Square>(vec3(-2.0f, 2.0f, 2.0f), vec3(2.0f, 2.0f, 2.0f), vec3(2.0f, -2.0f, 2.0f), vec3(-2.0f, -2.0f, 2.0f),
vec2(0.0f, 0.0f), vec2(1.0f, 0.0f), vec2(1.0f, 1.0f), vec2(0.0f, 1.0f));
이제 이미지 좌표를 봅시다. 이미지 좌표를 이해하기 쉽게, 4*4이미지를 생각해봅시다. 다음 그림은 4*4 이미지입니다.
이것을 사각형에 넣기 위해선 어떻게 해야할까요? 일단 텍스쳐 좌표로 변환해야합니다. 그전에 이걸 이해해야하는데요.
맨 왼쪽위는 모두 어두운 빨간색으로 색이 입혀져야합니다. 즉 텍스쳐 좌표의 (0~0.25f,0)부분은 모두 어두운 빨간색으로 표현되어야 합니다. 그러기 위해선 위 그림에서 각 사각형안의 점들을 이미지 좌표의 한 점이라고 봐야합니다. 예를들어 맨 왼쪽 위의 점은 (0,0)입니다. 맨 오른쪽 위는 (3,0)입니다. 잘리지 않고 다 변환하기 위해서 끝부분을 0.5만큼 간격을 벌려줘야합니다. 즉 형변환시 xy [-0.5, width - 1 + 0.5] x [-0.5, height - 1 + 0.5]이렇게 되어야 한다는 뜻입니다.
따라서 변환 코드는 다음과 같습니다.
vec3 SamplePoint(const vec2 &uv) // Nearest sampling이라고 부르기도 함
{
// 텍스춰 좌표의 범위 uv [0.0, 1.0] x [0.0, 1.0]
// 이미지 좌표의 범위 xy [-0.5, width - 1 + 0.5] x [-0.5, height - 1 + 0.5]
// 배열 인덱스의 정수 범위 ij [0, width-1] x [0, height - 1]
vec2 xy = uv * vec2(float(width), float(height)) - vec2(0.5f);
int i = round(xy.x);
int j = round(xy.y);
return GetClamped(i, j);
}
vec3 GetClamped(int i, int j)
{
i = glm::clamp(i, 0, width - 1);
j = glm::clamp(j, 0, height - 1);
/*return pixels[i+width*j]; 로 한번에 가져오지 않는 이유는
pixels의 색들을 굳이 vec3(float,float,float)하면 12바이트가돼서
pixels의 색의 값들만 image에 저장해서 쓰는 것입니다.
r, g, b이렇게 3개이므로 channels은 3이 되고요
*/
const float r = image[(i + width * j) * channels + 0] / 255.0f;
const float g = image[(i + width * j) * channels + 1] / 255.0f;
const float b = image[(i + width * j) * channels + 2] / 255.0f;
return vec3(r, g, b);
}
한번더 이해를 위해 SamplePoint코드를 다음과 같이 변경 한 후 결과를 보겠습니다.
vec3 SamplePoint(const vec2 &uv) // Nearest sampling이라고 부르기도 함
{
// 텍스춰 좌표의 범위 uv [0.0, 1.0] x [0.0, 1.0]
// 이미지 좌표의 범위 xy [-0.5, width - 1 + 0.5] x [-0.5, height - 1 + 0.5]
// 배열 인덱스의 정수 범위 ij [0, width-1] x [0, height - 1]
vec2 xy = uv * vec2(float(width-1), float(height-1));
int i = round(xy.x);
int j = round(xy.y);
return GetClamped(i, j);
}
이걸 이해해야만 제대로 이해했다고 볼 수 있습니다.
다음은 이미지를 텍스쳐링할 때 interporation(보간)효과를 내보겠습니다. 가장 가까운 4개의 점들을 Barycentric coordinates(무게중심좌표계)를 이용해서 보간하는 것입니다. 두개의 점들과 세개의 점들은 이전블로그 https://pdy0930.tistory.com/57 에서 이해할 수 있습니다.
그렇다면 4개의 점들은 어떻게 이해할까요? 일단 위 두개의 점들의 무게중심 좌표 betw1, 밑에 두개의 점들의 무게중심 좌표 betw2, 그리고 betw1과 betw2의 좌표 betw3을 계산하면 됩니다. 다음 그림을 보며 이해해봅시다.
이때 C00을 먼저 구하면 되는데, SamplePoint에서는 round를 써서 반올림하여 구했습니다. 여기서는 반올림으로 이미지 좌표의 해당 점을 구하는게 아니라, floor를 통해 내려서 바로 왼쪽위를 구해서 기준을 잡습니다. 그래야만 dx,dy가 음수가 되지 않고, 또한 가장 가까운 4개의 점을 맞출 수 있습니다.
코드로 표현하면 다음과 같습니다.
vec3 InterpolateBilinear(
const float &dx,
const float &dy,
const vec3 &c00,
const vec3 &c10,
const vec3 &c01,
const vec3 &c11)
{
const vec3 betw1 = c00 * (1 - dx) + c10 * dx;
const vec3 betw2 = c01 * (1 - dx) + c11 * dx;
const vec3 betw3 = betw1 * (1 - dy) + betw2 * dy;
return betw3;
}
vec3 SampleLinear(const vec2 &uv)
{
// 텍스춰 좌표의 범위 uv [0.0, 1.0] x [0.0, 1.0]
// 이미지 좌표의 범위 xy [-0.5, width - 1 + 0.5] x [-0.5, height - 1 + 0.5]
const vec2 xy = uv * vec2(float(width), float(height)) - vec2(0.5f);
const int i = floor(xy.x);
const int j = floor(xy.y);
const float dx = xy.x - i;
const float dy = xy.y - j;
return InterpolateBilinear(dx, dy, GetClamped(i, j), GetClamped(i + 1, j), GetClamped(i, j + 1), GetClamped(i + 1, j + 1));
}
결과는 다음과 같습니다.
강의 출처 :
https://honglab.co.kr/courses/graphicspt1
HongLab 홍정모 연구소
홍정모의 컴퓨터 그래픽스 새싹코스
honglab.co.kr
홍정모 그래픽스 Part1 - 텍스춰링