1.7 텍스처 다루기
유니폼에 대한 기초를 다졌으니, 이제 매력적인 텍스처 맵의 세계를 탐험해 봅시다. 텍스처 맵은 기본적으로 3D 지오메트리 메시를 감싸는 이미지입니다. 지금까지 우리의 렌더링은 단색 삼각형이나 그라데이션 색상의 삼각형으로 다소 기본적인 수준이었습니다. 이는 게임이나 고급 그래픽 애플리케이션에서 흔히 볼 수 있는 풍부함과 복잡성이 부족했습니다.
플레이그라운드 실행 - 1_07_working_with_textures컴퓨터 그래픽스와 게임에서는 다양한 이미지를 지오메트리에 적용하여 생생하고 상세한 비주얼을 만드는 것이 일반적입니다. 이 튜토리얼에서는 이 효과를 달성하는 방법을 보여줄 것입니다. 셰이더에 텍스처를 구현하는 것은 유니폼에 의존하며, 이것이 우리가 이전 장에서 텍스처 매핑의 기초를 다지기 위해 유니폼을 다룬 이유입니다.
@group(0) @binding(0)
var offset: vec3;
struct VertexOutput {
@builtin(position) clip_position: vec4,
@location(0) tex_coords: vec2,
};
@vertex
fn vs_main(
@location(0) inPos: vec3,
@location(1) inTexCoords: vec2
) -> VertexOutput {
var out: VertexOutput;
out.clip_position = vec4(inPos + offset, 1.0);
out.tex_coords = inTexCoords;
return out;
}
셰이더 코드의 변경 사항을 살펴보겠습니다. 정점 셰이더에서는 텍스처 좌표를 위해 @location(1)에 inTexCoords라는 새 속성을 도입했습니다. 각 정점에는 이제 특정 텍스처 좌표가 연결됩니다. 이 좌표를 프래그먼트 단계로 전달하기 위해 VertexOutput 구조체에 새 출력 값 tex_coords를 추가했습니다. 이는 각 프래그먼트에 대한 텍스처 좌표의 적절한 보간을 보장합니다.
@group(0) @binding(1)
var t_diffuse: texture_2d;
@group(0) @binding(2)
var s_diffuse: sampler;
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4 {
return textureSample(t_diffuse, s_diffuse, in.tex_coords);
}
프래그먼트 단계에서 이 좌표는 텍스처 이미지에서 색상을 가져오는 데 사용됩니다. 좌표가 텍스처 픽셀 사이에 위치하면 보간이 적용됩니다. 정확한 보간 방법은 구성할 수 있으며, linear와 nearest가 일반적인 선택지입니다.
프래그먼트 셰이더는 두 개의 새로운 유니폼을 도입합니다. 첫 번째는 texture_2d 타입의 t_diffuse로, 적용하려는 실제 텍스처 맵 또는 이미지를 나타냅니다. 두 번째는 sampler 타입의 s_diffuse인데, 이는 덜 직관적으로 보일 수 있습니다.
샘플러는 값을 제공하기보다는 동작을 정의합니다. 이는 주어진 텍스처 좌표를 기반으로 텍스처의 색상을 샘플링하는 방법을 지시합니다. 텍스처 좌표가 픽셀 사이에 떨어질 때 보간이 필요합니다. 샘플러를 사용하면 원하는 보간 방법을 지정할 수 있습니다. 다양한 방법은 서로 다른 결과를 산출합니다. 일부는 부드럽지만 약간 흐릿한 효과를 내는 반면, 다른 일부는 선명하지만 앨리어싱 아티팩트가 나타날 수 있습니다. 선택은 특정 사용 사례에 따라 달라지며, 향후 챕터에서 다양한 샘플러 유형을 더 자세히 탐구할 수 있습니다.
이러한 셰이더 업데이트가 완료되었으니, 이제 JavaScript 코드에서 이 새로운 유니폼에 값을 공급하는 방법을 살펴보겠습니다.
const response = await fetch('../data/baboon.png');
const blob = await response.blob();
const imgBitmap = await createImageBitmap(blob);
WebGPU 프로그래밍의 JavaScript 측면을 살펴보겠습니다. 첫 번째 작업은 이미지를 생성하는 것입니다. 우리는 fetch API를 사용하여 서버에서 PNG 이미지를 로드합니다. 서버가 응답하면 응답 본문에서 블롭을 생성합니다. 이 블롭으로부터 이미지 비트맵을 생성합니다.
const textureDescriptor = {
size: { width: imgBitmap.width, height: imgBitmap.height },
format: 'rgba8unorm',
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT
};
const texture = device.createTexture(textureDescriptor);
device.queue.copyExternalImageToTexture({ source: imgBitmap }, { texture }, textureDescriptor.size);
비트맵이 준비되었으니 다음 단계는 텍스처를 생성하는 것입니다. 먼저 텍스처 디스크립터를 정의합니다. 이 디스크립터는 텍스처의 너비와 높이, 그리고 픽셀 형식을 필요로 합니다. 사용 플래그의 경우, 셰이더에서 이 텍스처를 사용하려 하므로 TEXTURE_BINDING 플래그를 설정해야 합니다. 또한 COPY_DST 및 RENDER_ATTACHMENT 플래그를 포함하는 것도 필수적입니다. 이 둘은 copyExternalImageToTexture 헬퍼 함수에 의해 요구됩니다. WebGPU 사양에서 그 이유를 명시적으로 자세히 설명하지는 않지만, 이 함수가 GPU 명령 제출의 기반이 되며, 이미지 데이터를 GPU로 로드하기 위해 텍스처에 렌더링하는 것을 포함할 수 있다고 추론할 수 있습니다. 텍스처에 렌더링하는 개념은 나중에 더 자세히 살펴보겠지만, 지금은 copyExternalImageToTexture를 사용하여 이미지를 텍스처로 복사할 때마다 이 두 가지 추가 텍스처 사용이 필요하다는 것을 기억하는 것이 중요합니다.
const sampler = device.createSampler({
addressModeU: 'repeat',
addressModeV: 'repeat',
magFilter: 'linear',
minFilter: 'linear',
mipmapFilter: 'linear',
});
let shaderModule = shaderModuleFromCode(device, 'shader');
이제 샘플러를 만들어 보겠습니다. 첫 두 매개변수인 addressModeU와 addressModeV는 요청된 텍스처 좌표가 텍스처 범위를 벗어날 경우 텍스처를 어떻게 샘플링해야 하는지를 지정합니다. 모든 텍스처 이미지에서 텍스처 좌표 (0,0)은 왼쪽 상단 모서리에 해당하고, (1,1)은 오른쪽 하단 모서리에 해당합니다. 따라서 x와 y 모두 [0,1] 범위에 있는 모든 점은 텍스처의 한 지점에 매핑될 수 있습니다. 하지만 좌표가 이 범위를 벗어나면 어떻게 될까요? 이때 이 두 가지 설정이 적용됩니다. 일반적인 옵션으로는 clamp-to-edge, repeat, mirror-repeat이 있습니다. clamp-to-edge는 기본적으로 1보다 큰 모든 값을 1로, 0보다 작은 모든 값을 0으로 고정합니다. repeat은 좌표를 감싸 타일링 효과를 만듭니다. 즉, 1보다 큰 값 v는 v-1이 됩니다. mirror-repeat은 0과 1 경계를 기준으로 좌표를 뒤집어, 1보다 큰 값 v가 1 - (v - 1)이 되도록 합니다.
우리 경우에는 repeat을 선택했습니다. 이는 좌표가 텍스처 크기를 벗어날 경우 텍스처를 반복해야 함을 나타냅니다. 이를 통해 좌표가 텍스처의 차원을 초과하더라도 색상을 샘플링할 수 있습니다. 필터의 경우, linear 필터를 선택했습니다. 이는 이중 선형 보간을 활용하여 부드러운 샘플링 결과를 제공합니다. 그러나 이 선택이 때때로 약간 흐릿한 결과를 초래할 수 있다는 점에 유의해야 합니다. 이러한 선택을 더 잘 이해하기 위해 향후 장에서 더 많은 예시를 살펴볼 것입니다.
const positionAttribDesc = {
shaderLocation: 0, // @location(0)
offset: 0,
format: 'float32x3'
};
const positionBufferLayoutDesc = {
attributes: [positionAttribDesc],
arrayStride: 4 * 3, // sizeof(float) * 3
stepMode: 'vertex'
};
const texCoordsAttribDesc = {
shaderLocation: 1, // @location(1)
offset: 0,
format: 'float32x2'
};
const texCoordsBufferLayoutDesc = {
attributes: [texCoordsAttribDesc],
arrayStride: 4 * 2, // sizeof(float) * 3
stepMode: 'vertex'
};
const positions = new Float32Array([
1.0, -1.0, 0.0,
-1.0, -1.0, 0.0,
0.0, 1.0, 0.0
]);
let positionBuffer
= createGPUBuffer(device, positions, GPUBufferUsage.VERTEX);
const texCoords = new Float32Array([
1.0,
1.0,
// 🔴
0.0,
1.0,
0.5,
0.0,
]);
let texCoordsBuffer
= createGPUBuffer(device, texCoords, GPUBufferUsage.VERTEX);
const uniformData = new Float32Array([
0.1, 0.1, 0.1
]);
let uniformBuffer
= createGPUBuffer(device, uniformData, GPUBufferUsage.UNIFORM);
let uniformBindGroupLayout = device.createBindGroupLayout({
entries: [
{
binding: 0,
visibility: GPUShaderStage.VERTEX,
buffer: {}
},
{
binding: 1,
visibility: GPUShaderStage.FRAGMENT,
texture: {}
},
{
binding: 2,
visibility: GPUShaderStage.FRAGMENT,
sampler: {}
}
]
});
let uniformBindGroup = device.createBindGroup({
layout: uniformBindGroupLayout,
entries: [
{
binding: 0,
resource: {
buffer: uniformBuffer
}
},
{
binding: 1,
resource: texture.createView()
},
{
binding: 2,
resource:
sampler
}
]
});
나머지 코드 구조는 대부분 동일합니다. 여전히 유니폼 버퍼를 생성하지만, 이제는 오프셋 값만 포함합니다. 바인드 그룹을 생성할 때, @binding(0)의 경우 유니폼 버퍼를 통해 오프셋을 계속 제공합니다. 하지만 텍스처의 경우 텍스처 뷰를 리소스로 사용합니다. 그리고 샘플러의 경우 샘플러를 리소스로 간단히 제공합니다.
passEncoder = commandEncoder.beginRenderPass(renderPassDesc);
passEncoder.setViewport(0, 0, canvas.width, canvas.height, 0, 1);
passEncoder.setPipeline(pipeline);
passEncoder.setBindGroup(0, uniformBindGroup);
passEncoder.setVertexBuffer(0, positionBuffer);
passEncoder.setVertexBuffer(1, texCoordsBuffer);
passEncoder.draw(3, 1);
passEncoder.end();
나머지 코드는 이전 패턴을 따릅니다. 한 가지 주목할 만한 차이점은 @location(1)에 대해 setVertexBuffer를 호출하여 텍스처 좌표 버퍼로 텍스처 좌표를 제공한다는 점입니다.
이러한 변경 사항을 적용함으로써, 텍스처를 셰이더에 효과적으로 통합하여 시각적으로 더욱 매력적인 그래픽스를 위한 무한한 가능성을 열었습니다.