5.0 스텐실 버퍼
스텐실 버퍼는 깊이 버퍼와 밀접하게 관련되어 있습니다. 스텐실 테스트가 활성화되면 렌더링 파이프라인은 스텐실 버퍼의 내용에 기반하여 가시성 테스트를 수행합니다. 가시성 테스트의 정확한 공식은 설정을 통해 확인할 수 있습니다. 일반적으로 이 과정은 두 단계를 포함합니다: 첫 번째 단계는 스텐실 버퍼를 채우고, 두 번째 단계는 실제 렌더링을 처리합니다. 가시성 테스트를 통과한 장면의 부분만 렌더링됩니다.
플레이그라운드 실행 - 5_00_stencil스텐실 버퍼는 거울이나 포털과 같은 특정 게임 효과를 만드는 데 특히 유용합니다. 예를 들어, 거울 효과를 렌더링하려면 먼저 거울의 모양을 스텐실 버퍼에 그립니다. 그런 다음 스텐실 테스트를 활성화한 상태에서 거울의 시점에서 장면을 렌더링합니다. 비유클리드 세계 엔진(Non-Euclidean Worlds Engine)과 같은 흥미로운 착시 게임들은 고급 스텐실 버퍼 기술을 활용합니다.
앞서 언급했듯이, 스텐실 버퍼를 사용하는 것은 일반적으로 두 단계를 포함합니다. 첫 번째 단계는 스텐실 버퍼를 채우는 것인데, 이는 프래그먼트 셰이더에서 스텐실 버퍼에 직접 쓸 수 없기 때문에 직관적이지 않게 보일 수 있습니다. 대신 스텐실 버퍼는 깊이 버퍼와 유사하게 렌더링의 부산물로 채워집니다. 스텐실 버퍼에 기록되는 값은 미리 설정됩니다.
이 튜토리얼에서는 비유클리드 세계 엔진 프로젝트와 유사한 간단한 포털 효과를 만들 것입니다. 개념은 간단합니다: 먼저 가상 게이트웨이(단순한 평면)를 렌더링하여 스텐실 버퍼를 채웁니다. 다음으로 스텐실 가시성 테스트를 활성화한 상태로 장면을 렌더링합니다. 마지막으로 게이트웨이의 프레임을 렌더링합니다.
먼저 스텐실 버퍼를 채우는 데 사용되는 셰이더 코드를 살펴보겠습니다. 이 셰이더는 가능한 한 간단하게 설계되었습니다. 클립 좌표를 계산하고 객체를 투명 픽셀로 렌더링합니다. 여기서 목표는 보이는 것을 렌더링하는 것이 아니라 스텐실 값을 생성하는 것입니다. 깊이 버퍼와 유사하게 스텐실 버퍼 쓰기가 활성화되어 있어 프래그먼트가 생성될 때마다 스텐실 값도 스텐실 버퍼에 캐시됩니다.
@group(0) @binding(0)
var modelView: mat4x4;
@group(0) @binding(1)
var projection: mat4x4;
struct VertexOutput {
@builtin(position) clip_position: vec4,
};
@vertex
fn vs_main(
@location(0) inPos: vec3
) -> VertexOutput {
var out: VertexOutput;
var wldLoc:vec4 = modelView * vec4(inPos, 1.0);
out.clip_position = projection * wldLoc;
return out;
}
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4 {
return vec4( 0.0,0.0,0.0,0.0);
}
다음으로, 파이프라인 생성 시 스텐실 쓰기가 어떻게 활성화되는지 살펴보겠습니다:
const pipelineDesc = {
layout,
vertex: {
module: shaderModule,
entryPoint: 'vs_main',
buffers: [positionBufferLayoutDesc, normalBufferLayoutDesc]
},
fragment: {
module: shaderModule,
entryPoint: 'fs_main',
targets: [colorState]
},
primitive: {
topology: 'triangle-strip',
frontFace: 'ccw',
cullMode: 'none'
},
depthStencil: {
depthWriteEnabled: true,
depthCompare: 'less',
format: 'depth24plus-stencil8',
stencilFront: {
compare: "less",
passOp: "keep",
},
stencilBack: {
compare: "less",
passOp: "keep",
}
}
};
`format` `depth24plus-stencil8`는 깊이에 24비트를, 스텐실에 픽셀당 8비트를 지정합니다. 이전에 설정하지 않았던 `stencilFront` 및 `stencilBack` 필드는 각각 전면 및 후면 프래그먼트에 대한 가시성 테스트가 어떻게 수행되는지를 정의합니다. 각 필드는 가시성 테스트를 위한 비교 방법과 스텐실 버퍼를 업데이트하기 위한 작업을 요구합니다. 이 예제에서는 비교 방법이 'always'로 설정되어 가시성 테스트가 항상 통과됨을 의미합니다.
정의해야 할 세 가지 작업 유형은 `passOp`, `failOp`, `depthFailOp`입니다. 이들은 스텐실 테스트가 통과할 때, 실패할 때, 또는 깊이 테스트가 실패할 때 스텐실 버퍼를 업데이트하는 방법을 결정합니다. 기본 작업은 `keep`이며, 이는 기존 스텐실 버퍼 값을 유지합니다. 이 예제에서는 `replace` 작업을 사용하는데, 이는 기존 값을 나중에 지정할 새 값으로 대체합니다.
스텐실 테스트 구성에 `depthFailOp`가 왜 있는지 궁금할 수 있습니다. 스텐실 테스트가 깊이 테스트와 무슨 관련이 있을까요? 그 이유는 렌더링 중에 스텐실 테스트가 깊이 테스트보다 먼저 발생하기 때문입니다. 어떤 경우에는 프래그먼트가 스텐실 테스트는 통과했지만 깊이 테스트는 실패할 수 있습니다. `depthFailOp` 작업은 이러한 상황을 처리하는 방법을 지정할 수 있도록 합니다.
본질적으로, 우리가 논의한 파이프라인 구성은 포털 지오메트리를 스텐실 버퍼에 렌더링합니다. 이는 포털로 덮인 모든 픽셀에 지정된 스텐실 값이 할당된다는 의미입니다.
스텐실 참조 값을 설정하는 방법은 다음과 같습니다:
const passEncoder = commandEncoder.beginRenderPass(renderPassDesc);
passEncoder.setViewport(0, 0, canvas.width, canvas.height, 0, 1);
passEncoder.setStencilReference(0xFF);
stencil.encode(passEncoder);
passEncoder.end();
이 코드 스니펫에서 `setStencilReference` 함수는 스텐실 참조 값을 `0xFF`로 설정하는 데 사용됩니다. 스텐실 버퍼의 각 픽셀에 8비트만 할당되어 있음을 고려할 때, 이 참조 값은 `0xFF`와 일치하는 픽셀만 스텐실 테스트를 통과하도록 보장합니다. 포털로 덮이지 않은 픽셀은 `0`으로 설정됩니다.
두 번째 렌더링 단계에서는 실제 장면(주전자와 바닥 평면 포함)을 그릴 것입니다. 하지만 장면이 포털을 통해서만 보이도록 하고 싶습니다. 이를 위해 첫 번째 단계에서 채워진 스텐실 버퍼를 사용하여 가시성 테스트를 수행합니다.
const pipelineDesc = {
layout,
vertex: {
module: shaderModule,
entryPoint: 'vs_main',
buffers: [positionBufferLayoutDesc, normalBufferLayoutDesc]
},
fragment: {
module: shaderModule,
entryPoint: 'fs_main',
targets: [colorState]
},
primitive: {
topology: 'triangle-list',
frontFace: 'ccw',
cullMode: 'none'
},
depthStencil: {
depthWriteEnabled: true,
depthCompare: 'less',
format: 'depth24plus-stencil8',
stencilFront: {
compare: "less",
passOp: "keep",
},
stencilBack: {
compare: "less",
passOp: "keep",
}
}
};
업데이트된 파이프라인 설명에서 `compare` 함수는 `less`로 설정되어 있습니다. 이는 스텐실 참조 값이 스텐실 버퍼에 현재 저장된 값보다 작으면 스텐실 테스트가 통과됨을 의미합니다. `passOp`는 `keep`으로 설정되어 있어 이 렌더링 단계에서 스텐실 버퍼가 변경되지 않음을 의미합니다. 이 구성은 렌더링 작업을 수행하는 동안 스텐실 버퍼의 값이 보존되도록 합니다.
const pipelineDesc = {
layout,
vertex: {
module: shaderModule,
entryPoint: 'vs_main',
buffers: [positionBufferLayoutDesc]
},
fragment: {
module: shaderModule,
entryPoint: 'fs_main',
targets: [colorState]
},
primitive: {
topology: 'triangle-list',
frontFace: 'ccw',
cullMode: 'none'
},
depthStencil: {
depthWriteEnabled: true,
depthCompare: 'less',
format: 'depth24plus-stencil8',
stencilFront: {
compare: "always",
passOp: "replace",
},
stencilBack: {
compare: "always",
passOp: "replace",
}
}
};
const passEncoder2 = commandEncoder.beginRenderPass(renderPassDesc2);
passEncoder2.setViewport(0, 0, canvas.width, canvas.height, 0, 1);
passEncoder2.setStencilReference(0x0);
plane.encode(passEncoder2);
teapot.encode(passEncoder2);
passEncoder2.end();
다시 말하지만, 커맨드 버퍼를 생성할 때 스텐실 참조 값을 설정해야 합니다. 이번에는 0으로 설정합니다. 0은 0xFF보다 작으므로 스텐실 버퍼 값이 0xFF인 곳은 스텐실 테스트가 통과됩니다. 스텐실 버퍼 값이 0(참조 값과 동일)인 영역은 스텐실 테스트가 실패합니다.
마지막 렌더링 단계에서는 포털의 프레임을 렌더링하려고 합니다. 이 경우 스텐실 테스트를 수행하지 않으므로 비교 함수를 `always`로 설정합니다. 또한 이 단계가 스텐실 버퍼를 변경하지 않도록 기존 스텐실 값을 유지하기 위해 `keep` 작업을 사용합니다.
const pipelineDesc = {
layout,
vertex: {
module: shaderModule,
entryPoint: 'vs_main',
buffers: [positionBufferLayoutDesc, normalBufferLayoutDesc]
},
fragment: {
module: shaderModule,
entryPoint: 'fs_main',
targets: [colorState]
},
primitive: {
topology: 'triangle-list',
frontFace: 'ccw',
cullMode: 'none'
},
depthStencil: {
depthWriteEnabled: true,
depthCompare: 'less',
format: 'depth24plus-stencil8',
stencilFront: {
compare: "always",
passOp: "keep",
},
stencilBack: {
compare: "always",
passOp: "keep",
}
}
};
이 마지막 렌더링 단계에서는 스텐실 테스트를 수행하지 않고 스텐실 버퍼를 수정하지 않으므로 스텐실 참조 값은 관련이 없습니다.