1.6 Uniforms 이해하기

이번 튜토리얼에서는 WebGPU 셰이더의 uniform 개념을 살펴보겠습니다. Uniform은 셰이더 프로그램에 데이터를 제공하는 메커니즘으로, 셰이더 실행 전반에 걸쳐 상수로 작용합니다.

플레이그라운드 실행 - 1_06_uniforms

이전에 셰이더 프로그램에 데이터를 전달하는 데 사용했던 어트리뷰트와 uniform이 어떻게 다른지 궁금하실 겁니다. 그 차이는 의도된 사용법과 동작에 있습니다.

이러한 구분은 셰이더가 정점별 데이터(어트리뷰트)와 공유되고 변경되지 않는 데이터(uniforms)를 모두 처리할 수 있도록 하여 렌더링 프로세스에서 유연성과 효율성을 제공하므로 중요합니다.

이 예제에서는 "offset"이라는 uniform을 생성하여 정점의 위치를 일관된 양만큼 이동시킬 것입니다. 이 목적을 위해 uniform을 사용하는 것은 모든 정점에 동일한 오프셋을 적용하기를 원하기 때문에 논리적입니다. 오프셋에 어트리뷰트를 사용한다면, 모든 정점에 대해 동일한 값을 중복해서 사용해야 하므로 비효율적이고 낭비적일 것입니다. Uniform은 모든 정점에 동일한 정보를 셰이더에 전달해야 할 때 이상적인 선택입니다.

오프셋에 uniform을 사용함으로써, 지오메트리에 전역 변환을 효율적으로 적용하고, 동적 효과를 위해 오프셋 값을 쉽게 업데이트하며, 정점별 어트리뷰트에 비해 메모리 사용량과 데이터 전송을 줄일 수 있습니다. 이 예제는 WebGPU에서 uniform을 선언, 설정 및 사용하는 방법을 시연하여 유연하고 효율적인 셰이더 프로그램을 생성하는 데 있어 그 강력함을 보여줄 것입니다.

이 프로그램에서 uniform을 생성하는 데 사용된 구문을 살펴보겠습니다:

@group(0) @binding(0)
var offset: vec3;

var 선언은 offset이 uniform 변수임을 나타내며, 셰이더에게 이 변수가 uniform 버퍼에서 제공되어야 함을 알려줍니다. @binding(0) 어노테이션은 정점 어트리뷰트의 @location(0)과 유사한 목적을 가집니다. 이는 uniform 버퍼 내에서 uniform을 식별하는 인덱스입니다. 일반적인 uniform 버퍼에서는 여러 uniform 값을 묶어 놓으며, 이 인덱스는 셰이더가 올바른 값을 효율적으로 찾는 데 도움이 됩니다.

@group(0) 어노테이션은 uniform이 어떻게 구성되는지와 관련이 있습니다. 이 간단한 경우에는 모든 uniform을 단일 그룹(그룹 0)에 배치했습니다. 그러나 더 복잡한 셰이더의 경우, 여러 그룹을 사용하는 것이 유리할 수 있습니다. 예를 들어, 애니메이션 장면을 렌더링할 때 자주 변경되는 카메라 매개변수와 일정한 객체 색상이 있을 수 있습니다. 이를 다른 그룹으로 분리함으로써 자주 변경되는 데이터만 업데이트하여 성능을 최적화할 수 있습니다.

@vertex
fn vs_main(
    @location(0) inPos: vec3,
    @location(1) inColor: vec3
) -> VertexOutput {
    var out: VertexOutput;
    out.clip_position = vec4(inPos + offset, 1.0);
    out.color = inColor;
    return out;
}

offset uniform을 정의한 후, 셰이더에서의 사용은 간단합니다. 각 정점의 위치에 offset 값을 추가하여 그 위치를 효과적으로 이동시킵니다. 이 uniform을 통해 셰이더 코드에서 중복이나 불필요한 작업 없이 모든 정점에 일관된 변환을 적용할 수 있습니다.

셰이더를 수정한 후, uniform 버퍼를 생성하고 JavaScript 측에서 데이터를 제공해야 합니다. 어트리뷰트 데이터와 마찬가지로 uniform도 GPU 버퍼에 제공됩니다. 이 과정을 자세히 살펴보겠습니다:

const uniformData = new Float32Array([
    0.1, 0.1, 0.1
]);

let uniformBuffer = createGPUBuffer(device, uniformData, GPUBufferUsage.UNIFORM);

먼저, uniform 값을 저장할 uniformData 버퍼를 생성합니다. 이 예제에서는 오프셋을 나타내는 3요소 벡터를 포함합니다. GPUBufferUsage.UNIFORM 플래그를 사용하여 uniformBuffer를 생성하여 그 목적을 나타냅니다. 그런 다음, 헬퍼 함수를 사용하여 GPU uniform 버퍼에 데이터를 채웁니다.

let uniformBindGroupLayout = device.createBindGroupLayout({
    entries: [
        {
            binding: 0,
            visibility: GPUShaderStage.VERTEX,
            buffer: {}
        }
    ]
});

다음으로, 셰이더 코드의 uniform 그룹 정의에 해당하는 uniform 그룹의 형식을 설명하기 위해 uniform 바인딩 그룹 레이아웃을 생성합니다. 이 예제에서 레이아웃은 단일 uniform 값에 해당하는 하나의 항목을 가집니다. 바인딩 인덱스는 셰이더의 인덱스와 일치하며, 이 uniform을 정점 셰이더에서 사용하므로 가시성은 VERTEX로 설정됩니다. 마지막으로, 빈 버퍼 설정 객체는 기본값을 사용하겠다는 의미입니다.

let uniformBindGroup = device.createBindGroup({
    layout: uniformBindGroupLayout,
    entries: [
        {
            binding: 0,
            resource: {
                buffer: uniformBuffer
            }
        }
    ]
});

그런 다음, 레이아웃과 실제 데이터 저장소를 연결하는 uniform 바인딩 그룹을 생성합니다. 여기서는 uniform 버퍼를 바인딩 0의 리소스로 제공합니다.

const pipelineLayoutDesc = { bindGroupLayouts: [uniformBindGroupLayout] };
• • •
passEncoder.setBindGroup(0, uniformBindGroup);

파이프라인 레이아웃 설명자에 uniform 바인딩 그룹 레이아웃을 포함합니다. 마지막으로, 렌더 명령을 인코딩할 때 setBindGroup을 사용하여 그룹 ID와 해당 바인딩 그룹을 지정합니다.

이러한 단계를 통해 uniform 버퍼를 성공적으로 생성하고, 레이아웃을 정의하고, GPU 파이프라인의 셰이더에 uniform 데이터를 제공했습니다. 결과는 동일한 삼각형이지만, uniform 값에 따라 약간 오프셋됩니다. 코드 샘플에서 오프셋 값을 조정하여 삼각형의 위치에 어떤 영향을 미치는지 실험해 보세요.

GitHub에 의견 남기기