1.0 빈 캔버스 만들기

빈 캔버스를 만드는 것은 처음에는 흥미롭지 않게 느껴질 수 있지만, WebGPU 프로그래밍과 관련된 모든 프로젝트의 필수적인 시작점입니다. 이 섹션에서는 책 전체의 모든 프로그래밍 연습을 위한 기반을 마련할 것입니다.

시작하려면, 우리의 기반이 될 기본적인 HTML 파일을 정의할 것입니다. 이 파일은 의도적으로 최소한의 내용만 포함하도록 간단하게 작성되었습니다:

<html>
<body>
</body>
</html>

이 파일을 보기 위해 로컬 HTTP 서버를 설정할 것입니다. 저는 Python의 내장 HTTP 서버를 사용하는 것을 권장합니다. 이를 실행하려면 다음 명령을 실행하세요:

python3 -m http.server

이 HTTP 서버의 기본 포트는 8000입니다. 따라서 http://localhost:8000으로 이동하면 Chrome 브라우저에 HTML 페이지가 로드되는 것을 볼 수 있습니다. WebGPU는 비교적 새로운 기능이므로 최신 버전의 Chrome을 사용하고 있는지 확인하세요.

HTTPS를 사용하지 않는 도메인에서 WebGPU 페이지를 제공하는 경우, Chrome이 WebGPU 작동을 막을 수 있습니다. WebGPU는 보안 컨텍스트 내에서만 작동하도록 설계되었기 때문입니다. 그러나 localhost는 HTTP를 통해 액세스하더라도 보안 컨텍스트로 간주되는 특별한 도메인이므로 로컬 개발이 더 쉬워집니다.

Linux 사용자 참고: 이 글을 쓰는 시점에서 WebGPU는 Linux용 Chrome의 실험적 기능이며 수동으로 활성화해야 합니다. Chrome에서 WebGPU를 활성화하려면 터미널에서 다음 명령으로 Chrome을 시작하세요:

google-chrome --enable-unsafe-webgpu --enable-features=Vulkan,UseSkiaRenderer

환경 설정이 완료되었으므로 이제 WebGPU 프로젝트 코딩을 시작할 수 있습니다. 앞서 언급했듯이, 우리의 주요 프로그래밍 작업은 다양한 파이프라인을 정의하고 리소스를 효율적으로 관리하는 데 중점을 둘 것입니다. 그러나 먼저 렌더링된 요소가 없는 배경인 빈 캔버스를 만드는 기본부터 시작하겠습니다.

HTML 파일에 캔버스 태그를 추가하는 것부터 시작하겠습니다. 이 요소는 3D 콘텐츠가 렌더링될 영역으로 사용됩니다. WebGL 또는 2D 웹 그래픽스 경험이 있다면 이 단계는 익숙할 것입니다:

<html>
<body>
    <canvas id="canvas" width="640" height="480"></canvas>
</body>
</html>

이제 JavaScript 코드를 담을 스크립트 태그를 포함하도록 진행하겠습니다. 우리의 첫 번째 작업은 WebGPU의 가용성을 확인하는 것입니다. 이 기능이 새롭기 때문에, 오래된 브라우저는 이를 지원하지 않을 수 있으며 콘텐츠 렌더링을 거부할 것입니다. 프로덕션 환경에서는 적절한 오류 메시지를 표시하여 이 시나리오를 제대로 처리하는 것이 중요합니다. 그러나 이 튜토리얼에서는 오류를 기록하고 반환하여 간단하게 유지할 것입니다.

<html>

<body>
    <canvas id="canvas" width="640" height="480"></canvas>
</body>
<script>
    async function webgpu() {
        if (!navigator.gpu) {
            console.error("WebGPU is not available.");
            return;
        }
    }
    webgpu();
</script>
</html>

많은 WebGPU 함수가 비동기적 특성을 가지므로, 우리는 코드를 webgpu()라는 비동기 함수 안에 캡슐화할 것입니다. 먼저, navigator.gpu가 정의되지 않았는지 확인하는 검사를 수행합니다. 이 조건은 WebGPU를 지원하지 않는 이전 브라우저에 적용됩니다.

if (!navigator.gpu) {
    console.error("WebGPU is not available.");
    showWarning("WebGPU support is not available. A WebGPU capable browser is required to run this sample.");
    throw new Error("WebGPU support is not available");        
}

const adapter = await navigator.gpu.requestAdapter();
if (!adapter) {
    console.error("Failed to request Adapter.");
    return;
}

이어서, navigator.gpu를 통해 어댑터를 획득하고, 그 다음 어댑터를 통해 장치를 획득합니다. 단일 핸들(glContext라고 함)로 상호 작용이 충분했던 WebGL에 비해 이 과정이 다소 장황하게 느껴질 수 있습니다. 여기서 navigator.gpu는 WebGPU 영역의 진입점 역할을 합니다. 어댑터는 본질적으로 WebGPU API를 구현하는 소프트웨어 구성 요소의 추상화입니다. 이는 이전에 소개된 드라이버 개념과 유사합니다. 그러나 WebGPU는 GPU 드라이버가 직접 제공하는 것이 아니라 웹 브라우저에 의해 구현되는 API라는 점을 고려할 때, 어댑터는 브라우저 내의 WebGPU 소프트웨어 계층으로 간주될 수 있습니다. Chrome의 경우 어댑터는 "Dawn" 하위 시스템에서 제공됩니다. 여러 어댑터가 존재할 수 있으며, 다른 벤더의 다양한 구현을 제공하거나 실제 렌더링 기능 없이 상세한 디버그 로그를 생성하는 디버그 지향 더미 어댑터를 포함할 수 있다는 점에 주목할 가치가 있습니다. 그 다음으로 어댑터는 해당 어댑터의 인스턴스인 장치를 생성합니다. 어댑터를 클래스에, 장치를 해당 클래스에서 인스턴스화된 객체에 비유할 수 있습니다.

사양은 어댑터 요청 직후 장치를 요청할 필요가 있음을 강조합니다. 어댑터의 유효 기간이 제한적이기 때문입니다. 어댑터 무효화의 내부 작동 방식은 내부 작동을 알지 못하면 다소 모호하지만, 소프트웨어 개발자에게는 심각한 문제가 아닙니다. 어댑터 무효화의 한 예는 노트북의 전원 공급 장치를 뽑으면 어댑터가 무효화될 수 있다고 사양에 명시되어 있습니다. 노트북이 배터리 모드로 전환될 때 운영 체제는 특정 GPU 기능을 무효화하는 절전 조치를 활성화할 수 있습니다. 일부 노트북은 다른 전원 상태를 위해 듀얼 GPU를 자랑하며, 이는 전환 시 유사한 무효화를 유발할 수 있습니다. 사양에 따르면 이러한 동작의 다른 이유로는 드라이버 업데이트 등이 있습니다.

일반적으로 장치를 요청할 때는 원하는 기능 세트를 지정해야 합니다. 그러면 어댑터가 일치하는 장치로 응답합니다. 이 과정은 클래스 생성자에 매개 변수를 제공하는 것에 비유할 수 있습니다. 그러나 이 예제에서는 기본 장치를 요청합니다. 다음 장에서는 기능 플래그를 사용하여 장치를 쿼리하는 방법을 논의하고 더 포괄적인 예제를 제공할 것입니다.

let device = await adapter.requestDevice();
if (!device) {
    console.error("Failed to request Device.");
    return;
}

const context = canvas.getContext('webgpu');

const canvasConfig = {
    device: device,
    format: navigator.gpu.getPreferredCanvasFormat(),
    usage:
        GPUTextureUsage.RENDER_ATTACHMENT,
    alphaMode: 'opaque'
};

context.configure(canvasConfig);

장치를 획득한 후 다음 단계는 캔버스가 적절하게 설정되도록 컨텍스트를 구성하는 것입니다. 여기에는 색상 형식, 투명도 기본 설정 및 기타 몇 가지 옵션을 지정하는 것이 포함됩니다. 컨텍스트 구성은 캔버스 구성 구조를 제공하여 달성됩니다. 이 경우 필수 요소에 중점을 둘 것입니다.

format 매개변수는 캔버스에 렌더링 결과를 위해 사용되는 픽셀 형식을 결정합니다. 지금은 기본 형식을 사용할 것입니다. usage 매개변수는 캔버스에서 제공하는 텍스처의 "버퍼 사용량"과 관련이 있습니다. 여기서 우리는 이 캔버스가 렌더링 대상으로 사용됨을 나타내기 위해 RENDER_ATTACHMENT를 지정합니다. 버퍼 사용의 복잡성은 다음 장에서 다룰 것입니다. 마지막으로, alphaMode 매개변수는 캔버스의 투명도를 조정하는 토글을 제공합니다.

let colorTexture = context.getCurrentTexture();
let colorTextureView = colorTexture.createView();

let colorAttachment = {
    view: colorTextureView,
    clearValue: { r: 1, g: 0, b: 0, a: 1 },
    loadOp: 'clear',
    storeOp: 'store'
};

const renderPassDesc = {
    colorAttachments: [colorAttachment]
};

계속해서, 우리의 초점은 렌더 패스를 구성하는 것으로 이동합니다. 렌더 패스는 색상 이미지와 깊이 이미지와 같은 지정된 렌더링 대상을 포함하는 컨테이너 역할을 합니다. 비유를 들자면, 렌더링 대상은 우리가 그림을 그리고 싶은 종이 한 장과 같습니다. 하지만 방금 구성한 캔버스와는 어떻게 다를까요?

이전에 Photoshop을 사용해 본 경험이 있다면, 캔버스를 여러 레이어가 포함된 이미지 문서로 생각해보세요. 각 레이어는 렌더링 대상에 비유될 수 있습니다. 마찬가지로 3D 렌더링에서는 단일 레이어로 렌더링을 수행할 수 없는 경우가 있으므로 여러 번 렌더링합니다. 렌더링 세션마다(렌더링 패스라고 함) 전용 렌더링 대상으로 결과를 출력합니다. 최종적으로 이 결과들을 결합하여 캔버스에 표시합니다.

우리의 첫 번째 단계는 캔버스에서 텍스처를 얻는 것입니다. 렌더링 시스템에서는 이 과정이 종종 스왑 체인(여러 프레임에 걸쳐 렌더링을 용이하게 하는 버퍼 목록)을 통해 구현됩니다. 그래픽스 하위 시스템은 이러한 버퍼를 재활용하여 지속적인 버퍼 생성의 필요성을 없앱니다. 따라서 렌더링을 시작하기 전에 캔버스에서 사용 가능한 버퍼(텍스처)를 확보해야 합니다.

이어서 텍스처에 연결된 뷰를 생성합니다. 텍스처와 텍스처 뷰의 차이점에 대해 궁금할 수 있습니다. 일반적으로 알려진 것과 달리, 텍스처는 반드시 단일 이미지가 아닐 수 있습니다. 여러 이미지를 포함할 수 있습니다. 예를 들어, 밉맵 컨텍스트에서 각 밉맵 레벨은 개별 이미지로 간주됩니다. 밉맵이 생소한 개념이라면, 그것은 다른 수준의 세부 정보를 가진 동일한 이미지의 피라미드입니다. 밉맵은 텍스처 맵 샘플링 품질을 향상시키는 데 매우 유용합니다. 밉맵은 나중에 다룰 것입니다. 핵심은 텍스처가 이미지와 동의어가 아니며, 이 컨텍스트에서는 렌더링 대상으로 단일 이미지(뷰)가 필요하다는 것입니다.

다음으로, 렌더 패스 내의 색상 대상 역할을 하는 `colorAttachment`를 생성합니다. 색상 첨부파일은 색상 정보나 픽셀을 담는 버퍼로 생각할 수 있습니다. 이전에 렌더링 대상을 종이 한 장에 비유했지만, 실제로는 단일 버퍼가 아니라 여러 버퍼로 구성되는 경우가 많습니다. 이러한 추가 버퍼는 다양한 용도로 사용되는 임시 저장 공간이며, 일반적으로 보이지 않고 픽셀을 반드시 나타내지 않는 데이터를 저장합니다. 일반적인 예로는 뷰어에 가장 가까운 픽셀을 결정하는 데 사용되는 깊이 버퍼가 있으며, 이를 통해 폐색과 같은 효과를 구현할 수 있습니다. 이 설정에 깊이 버퍼를 포함할 수도 있지만, 우리의 간단한 예제는 캔버스를 단색으로 지우는 것만을 목표로 하므로 깊이 버퍼는 불필요합니다.

colorAttachment의 매개변수를 자세히 살펴보겠습니다:

commandEncoder = device.createCommandEncoder();

passEncoder = commandEncoder.beginRenderPass(renderPassDesc);
passEncoder.setViewport(0, 0, canvas.width, canvas.height, 0, 1);
passEncoder.end();

device.queue.submit([commandEncoder.finish()]);

마지막 단계에서는 명령을 생성하여 GPU에 제출하여 실행합니다. 이 특정 명령은 간단합니다. 뷰포트 크기를 캔버스 크기와 일치하도록 설정합니다. 아무것도 그리지 않기 때문에 렌더링 대상은 우리의 loadOp에 지정된 기본 clearValue로 단순히 지워집니다.

개발 중에는 디버깅 목적으로 눈에 띄는 색상을 사용하는 것이 좋습니다. 이 경우, 우리는 더 일반적인 검은색이나 흰색 대신 빨간색을 선택합니다. 이 결정은 전략적입니다: 검은색과 흰색은 많은 컨텍스트에서 사용되는 일반적인 기본 색상입니다. 예를 들어, 기본 웹 페이지 배경은 일반적으로 흰색입니다. 흰색을 지우기 색상으로 사용하면 렌더링이 실제로 발생하고 있는지 또는 캔버스가 전혀 없는지 혼동을 줄 수 있습니다. 생생한 빨간색을 선택함으로써, 렌더링 작업이 실제로 진행되고 있다는 명확한 시각적 지표를 보장합니다.

이 접근 방식은 성공적인 실행에 대한 명확한 신호를 제공하여 개발 프로세스 중에 발생할 수 있는 모든 문제를 식별하고 해결하기 쉽게 만듭니다.

GPU 코드 디버깅은 CPU 코드보다 훨씬 더 어렵습니다. GPU 작업의 병렬 특성 때문에 GPU 실행에서 로그를 생성하는 것은 복잡합니다. 이러한 복잡성은 또한 중단점을 설정하고 실행을 일시 중지하는 것과 같은 전통적인 디버깅 방법을 비현실적으로 만듭니다. 이러한 맥락에서 색상은 귀중한 디버깅 도구가 됩니다. 서로 다른 의미에 고유한 색상을 연결함으로써 결과를 정확하게 해석하는 능력을 향상시킬 수 있습니다. 다음 장으로 진행하면서 색상이 GPU 프로그래밍에서 필수적인 디버깅 보조 도구로 어떻게 사용되는지 보여주는 다양한 예제를 탐색할 것입니다.

또한 숙련된 그래픽 프로그래머는 코드 가독성, 유지보수성 및 디버깅 가능성을 향상시키기 위해 다른 전략을 사용합니다.

  1. 설명적인 변수 이름: 그래픽 API는 소스 전체에 걸쳐 겉보기에 반복되는 코드 블록으로 장황할 수 있습니다. 변수에 대한 상세하고 설명적인 이름을 사용하면 코드를 효율적으로 식별하고 탐색하는 데 도움이 됩니다.

  2. 점진적 개발: 간단하게 시작하고 점차 복잡성을 높이는 것이 좋습니다. 종종 이는 더 정교한 효과를 추가하기 전에 단색 객체를 먼저 렌더링하는 것을 의미합니다.

  3. 일관된 코딩 패턴: 코드에서 일관된 패턴을 설정하고 따르는 것은 가독성을 크게 향상시키고 오류를 줄일 수 있습니다.

  4. 모듈식 설계: 복잡한 렌더링 작업을 더 작고 관리하기 쉬운 함수나 모듈로 분해하면 코드를 이해하고 유지보수하기 더 쉬워집니다.

이러한 관행을 채택함으로써 개발자는 그래픽스 프로그래밍이 제시하는 고유한 문제에도 불구하고 더 견고하고 가독성 있고 쉽게 디버깅할 수 있는 GPU 코드를 만들 수 있습니다.

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

이 장의 코드는 빨간색으로 렌더링된 빈 캔버스를 생성합니다. 플레이그라운드를 사용하여 코드를 조작해 보세요. 배경을 다른 색상으로 변경해 보세요.

GitHub에 의견 남기기