paint-brush
WebGL에서 WebGPU로 마이그레이션~에 의해@dmitrii
7,681 판독값
7,681 판독값

WebGL에서 WebGPU로 마이그레이션

~에 의해 Dmitrii Ivashchenko13m2023/12/20
Read on Terminal Reader

너무 오래; 읽다

이 가이드에서는 주요 차이점, 높은 수준의 개념 및 실용적인 팁을 다루면서 WebGL에서 WebGPU로의 전환을 설명합니다. WebGPU가 웹 그래픽의 미래로 떠오르면서 이 기사는 소프트웨어 엔지니어와 프로젝트 관리자 모두에게 귀중한 통찰력을 제공합니다.
featured image - WebGL에서 WebGPU로 마이그레이션
Dmitrii Ivashchenko HackerNoon profile picture

곧 출시될 WebGPU로 이동한다는 것은 단순히 그래픽 API를 전환하는 것 이상의 의미를 갖습니다. 이는 웹 그래픽의 미래를 향한 한 걸음이기도 합니다. 하지만 이 마이그레이션은 준비와 이해를 통해 더 나은 결과를 얻을 수 있으며, 이 문서를 통해 준비할 수 있습니다.


안녕하세요 여러분, 제 이름은 Dmitrii Ivashchenko이고 MY.GAMES의 소프트웨어 엔지니어입니다. 이 글에서는 WebGL과 곧 출시될 WebGPU의 차이점에 대해 논의하고 마이그레이션을 위해 프로젝트를 준비하는 방법을 설명하겠습니다.


콘텐츠 개요

  1. WebGL 및 WebGPU의 타임라인

  2. WebGPU의 현재 상태와 향후 계획

  3. 높은 수준의 개념적 차이점

  4. 초기화

    • WebGL: 컨텍스트 모델

    • WebGPU: 장치 모델

  5. 프로그램 및 파이프라인

    • WebGL: 프로그램

    • WebGPU: 파이프라인

  6. 유니폼

    • WebGL 1의 유니폼

    • WebGL 2의 유니폼

    • WebGPU의 유니폼

  7. 셰이더

    • 셰이더 언어: GLSL 대 WGSL

    • 데이터 유형 비교

    • 구조

    • 함수 선언

    • 내장된 기능

    • 셰이더 변환

  8. 컨벤션 차이점

  9. 텍스처

    • 뷰포트 공간

    • 클립 공백

  10. WebGPU 팁과 요령

    • 사용하는 파이프라인 수를 최소화합니다.

    • 파이프라인을 미리 생성

    • 렌더 번들 사용

  11. 요약


WebGL 및 WebGPU의 타임라인

다른 많은 웹 기술과 마찬가지로 WebGL의 뿌리는 꽤 먼 과거로 거슬러 올라갑니다. WebGPU로의 전환 뒤에 숨은 역학과 동기를 이해하려면 먼저 WebGL 개발의 역사를 간략히 살펴보는 것이 도움이 됩니다.


  • OpenGL 데스크탑(1993) OpenGL의 데스크탑 버전이 출시되었습니다.
  • WebGL 1.0(2011) : 이는 2007년에 자체적으로 도입된 OpenGL ES 2.0을 기반으로 하는 최초의 안정적인 WebGL 릴리스입니다. 이는 웹 개발자에게 추가 플러그인 없이 브라우저에서 직접 3D 그래픽을 사용할 수 있는 기능을 제공했습니다.
  • WebGL 2.0(2017) : 첫 번째 버전 이후 6년 만에 출시된 WebGL 2.0은 OpenGL ES 3.0(2012)을 기반으로 했습니다. 이 버전에는 다양한 개선 사항과 새로운 기능이 추가되어 웹에서의 3D 그래픽이 더욱 강력해졌습니다.


최근 몇 년 동안 개발자에게 더 많은 제어력과 유연성을 제공하는 새로운 그래픽 API에 대한 관심이 급증했습니다.


  • Vulkan(2016) : Khronos 그룹에서 만든 이 크로스 플랫폼 API는 OpenGL의 "후계자"입니다. Vulkan은 그래픽 하드웨어 리소스에 대한 낮은 수준의 액세스를 제공하여 그래픽 하드웨어를 더 잘 제어할 수 있는 고성능 애플리케이션을 허용합니다.
  • D3D12(2015) : 이 API는 Microsoft에서 만들었으며 Windows 및 Xbox 전용입니다. D3D12는 D3D10/11의 후속 제품으로 개발자에게 그래픽 리소스에 대한 더 심층적인 제어 기능을 제공합니다.
  • Metal(2014) : Apple에서 만든 Metal은 Apple 장치용 독점 API입니다. Apple 하드웨어의 최대 성능을 염두에 두고 설계되었습니다.


WebGPU의 현재 상태와 향후 계획

현재 WebGPU는 버전 113부터 Google Chrome 및 Microsoft Edge 브라우저를 통해 Windows, Mac, ChromeOS 등 다양한 플랫폼에서 사용할 수 있습니다. Linux 및 Android에 대한 지원도 가까운 시일 내에 제공될 예정입니다.


WebGPU를 이미 지원(또는 실험적 지원을 제공)하는 엔진은 다음과 같습니다.


  • Babylon JS : WebGPU를 완벽하게 지원합니다.
  • ThreeJS : 현재 실험적으로 지원됩니다.
  • PlayCanvas : 개발 중이지만 전망이 매우 밝습니다.
  • Unity : 매우 초기의 실험적인 WebGPU 지원이 버전 2023.2 알파에서 발표되었습니다.
  • Cocos Creator 3.6.2 : WebGPU를 공식적으로 지원하여 이 분야의 선구자 중 하나입니다.
  • 구성 : 현재 Windows, macOS 및 ChromeOS용 v113+에서만 지원됩니다.



이를 고려하면 WebGPU로의 전환이나 적어도 그러한 전환을 위한 프로젝트 준비는 가까운 미래에 시기적절한 단계로 보입니다.


높은 수준의 개념적 차이점

이제 초기화부터 시작하여 WebGL과 WebGPU 간의 상위 수준 개념적 차이점을 살펴보겠습니다.

초기화

그래픽 API 작업을 시작할 때 첫 번째 단계 중 하나는 상호 작용을 위해 기본 개체를 초기화하는 것입니다. 이 프로세스는 WebGL과 WebGPU 간에 다르며 두 시스템 모두에 몇 가지 특성이 있습니다.

WebGL: 컨텍스트 모델

WebGL에서는 이 개체를 "컨텍스트"라고 하며, 이는 본질적으로 HTML5 캔버스 요소에 그리기 위한 인터페이스를 나타냅니다. 이 컨텍스트를 얻는 것은 매우 간단합니다.

 const gl = canvas.getContext('webgl');


WebGL의 컨텍스트는 실제로 특정 캔버스에 연결되어 있습니다. 즉, 여러 캔버스에서 렌더링해야 하는 경우 여러 컨텍스트가 필요합니다.

WebGPU: 장치 모델

WebGPU는 '디바이스'라는 새로운 개념을 도입합니다. 이 장치는 사용자가 상호 작용할 GPU 추상화를 나타냅니다. 초기화 프로세스는 WebGL보다 조금 더 복잡하지만 더 많은 유연성을 제공합니다.

 const adapter = await navigator.gpu.requestAdapter(); const device = await adapter.requestDevice(); const context = canvas.getContext('webgpu'); context.configure({ device, format: 'bgra8unorm', });


이 모델의 장점 중 하나는 하나의 장치가 여러 캔버스에서 렌더링할 수 있거나 전혀 렌더링할 수 없다는 것입니다. 이는 추가적인 유연성을 제공합니다. 예를 들어 하나의 장치가 여러 창이나 컨텍스트에서 렌더링을 제어할 수 있습니다.



프로그램 및 파이프라인

WebGL과 WebGPU는 그래픽 파이프라인을 관리하고 구성하는 다양한 접근 방식을 나타냅니다.

WebGL: 프로그램

WebGL에서 주요 초점은 셰이더 프로그램에 있습니다. 이 프로그램은 정점 셰이더와 조각 셰이더를 결합하여 정점을 변환하는 방법과 각 픽셀의 색상을 지정하는 방법을 정의합니다.

 const program = gl.createProgram(); gl.attachShader(program, vertShader); gl.attachShader(program, fragShader); gl.bindAttribLocation(program, 'position', 0); gl.linkProgram(program);


WebGL에서 프로그램을 생성하는 단계:


  1. Creating Shaders : 셰이더의 소스 코드가 작성되고 컴파일됩니다.
  2. 프로그램 생성 : 컴파일된 셰이더가 프로그램에 첨부된 후 연결됩니다.
  3. 프로그램 사용 : 렌더링 전 프로그램이 활성화됩니다.
  4. 데이터 전송 : 활성화된 프로그램으로 데이터가 전송됩니다.


이 프로세스를 사용하면 유연한 그래픽 제어가 가능하지만 특히 크고 복잡한 프로젝트의 경우 복잡하고 오류가 발생하기 쉽습니다.

WebGPU: 파이프라인

WebGPU는 별도의 프로그램 대신 "파이프라인"이라는 개념을 도입합니다. 이 파이프라인은 셰이더뿐만 아니라 WebGL에서 상태로 설정되는 기타 정보도 결합합니다. 따라서 WebGPU에서 파이프라인을 생성하는 것은 더 복잡해 보입니다.

 const pipeline = device.createRenderPipeline({ layout: 'auto', vertex: { module: shaderModule, entryPoint: 'vertexMain', buffers: [{ arrayStride: 12, attributes: [{ shaderLocation: 0, offset: 0, format: 'float32x3' }] }], }, fragment: { module: shaderModule, entryPoint: 'fragmentMain', targets: [{ format, }], }, });


WebGPU에서 파이프라인을 생성하는 단계:


  1. 셰이더 정의 : 셰이더 소스 코드는 WebGL에서 수행되는 방식과 유사하게 작성되고 컴파일됩니다.
  2. 파이프라인 생성 : 셰이더 및 기타 렌더링 매개변수가 파이프라인으로 결합됩니다.
  3. 파이프라인 사용 : 파이프라인은 렌더링 전에 활성화됩니다.


WebGL은 렌더링의 각 측면을 분리하는 반면 WebGPU는 더 많은 측면을 단일 객체로 캡슐화하여 시스템을 더욱 모듈화하고 유연하게 만듭니다. WebGL에서처럼 셰이더와 렌더링 상태를 별도로 관리하는 대신 WebGPU는 모든 것을 하나의 파이프라인 개체로 결합합니다. 이렇게 하면 프로세스를 더 예측하기 쉽고 오류가 발생할 가능성이 줄어듭니다.



유니폼

균일 변수는 모든 셰이더 인스턴스에 사용할 수 있는 상수 데이터를 제공합니다.

WebGL 1의 유니폼

기본 WebGL에서는 API 호출을 통해 직접 uniform 변수를 설정할 수 있는 기능이 있습니다.


GLSL :

 uniform vec3 u_LightPos; uniform vec3 u_LightDir; uniform vec3 u_LightColor;


자바스크립트 :

 const location = gl.getUniformLocation(p, "u_LightPos"); gl.uniform3fv(location, [100, 300, 500]);


이 방법은 간단하지만 각 uniform 변수에 대해 여러 API 호출이 필요합니다.

WebGL 2의 유니폼

WebGL 2가 출시되면서 이제 uniform 변수를 버퍼로 그룹화할 수 있는 기능이 생겼습니다. 여전히 별도의 균일 셰이더를 사용할 수 있지만 더 나은 옵션은 균일 버퍼를 사용하여 서로 다른 유니폼을 더 큰 구조로 그룹화하는 것입니다. 그런 다음 WebGL 1에서 정점 버퍼를 로드하는 방법과 유사하게 이 모든 균일한 데이터를 한 번에 GPU로 보냅니다. 이는 API 호출을 줄이고 최신 GPU 작동 방식에 더 가까워지는 등 여러 가지 성능 이점을 제공합니다.


GLSL :

 layout(std140) uniform ub_Params { vec4 u_LightPos; vec4 u_LightDir; vec4 u_LightColor; };


자바스크립트 :

 gl.bindBufferBase(gl.UNIFORM_BUFFER, 1, gl.createBuffer());


WebGL 2에서 대규모 균일 버퍼의 하위 집합을 바인딩하려면 bindBufferRange 라는 특수 API 호출을 사용할 수 있습니다. WebGPU에는 setBindGroup API를 호출할 때 오프셋 목록을 전달할 수 있는 동적 균일 버퍼 오프셋이라는 유사한 기능이 있습니다.



WebGPU의 유니폼

WebGPU는 훨씬 더 나은 방법을 제공합니다. 이러한 맥락에서 개별 uniform 변수는 더 이상 지원되지 않으며 작업은 uniform 버퍼를 통해서만 수행됩니다.


WGSL :

 [[block]] struct Params { u_LightPos : vec4<f32>; u_LightColor : vec4<f32>; u_LightDirection : vec4<f32>; }; [[group(0), binding(0)]] var<uniform> ub_Params : Params;


자바스크립트 :

 const buffer = device.createBuffer({ usage: GPUBufferUsage.UNIFORM, size: 8 });


최신 GPU는 여러 개의 작은 블록보다는 하나의 큰 블록에 데이터를 로드하는 것을 선호합니다. 매번 작은 버퍼를 다시 만들고 다시 바인딩하는 대신 하나의 큰 버퍼를 만들고 그 중 다른 부분을 다양한 그리기 호출에 사용하는 것이 좋습니다. 이 접근 방식은 성능을 크게 향상시킬 수 있습니다.


WebGL은 호출할 때마다 전역 상태를 재설정하고 최대한 단순해지려고 노력하는 것이 더욱 중요합니다. 반면 WebGPU는 보다 객체 지향적이고 리소스 재사용에 중점을 두어 효율성을 높이는 것을 목표로 합니다.


WebGL에서 WebGPU로 전환하는 것은 방법의 차이로 인해 어려워 보일 수 있습니다. 그러나 중간 단계로 WebGL 2로의 전환부터 시작하면 생활이 단순화될 수 있습니다.



셰이더

WebGL에서 WebGPU로 마이그레이션하려면 API뿐만 아니라 셰이더에도 변경이 필요합니다. WGSL 사양은 이러한 전환을 원활하고 직관적으로 수행하는 동시에 최신 GPU의 효율성과 성능을 유지하도록 설계되었습니다.

셰이더 언어: GLSL 대 WGSL

WGSL은 WebGPU와 기본 그래픽 API 사이의 가교 역할을 하도록 설계되었습니다. GLSL에 비해 WGSL은 좀 더 장황해 보이지만 구조는 여전히 친숙합니다.


다음은 텍스처에 대한 예제 셰이더입니다.


GLSL :

 sampler2D myTexture; varying vec2 vTexCoord; void main() { return texture(myTexture, vTexCoord); }


WGSL :

 [[group(0), binding(0)]] var mySampler: sampler; [[group(0), binding(1)]] var myTexture: texture_2d<f32>; [[stage(fragment)]] fn main([[location(0)]] vTexCoord: vec2<f32>) -> [[location(0)]] vec4<f32> { return textureSample(myTexture, mySampler, vTexCoord); } 



데이터 유형 비교

아래 표는 GLSL과 WGSL의 기본 데이터 유형과 행렬 데이터 유형을 비교한 것입니다.



GLSL에서 WGSL로 전환하면 데이터 크기를 더욱 엄격하게 입력하고 명시적으로 정의하여 코드 가독성을 향상시키고 오류 가능성을 줄일 수 있습니다.



구조

구조체 선언 구문도 변경되었습니다.


GLSL:

 struct Light { vec3 position; vec4 color; float attenuation; vec3 direction; float innerAngle; float angle; float range; };


WGSL:

 struct Light { position: vec3<f32>, color: vec4<f32>, attenuation: f32, direction: vec3<f32>, innerAngle: f32, angle: f32, range: f32, };


WGSL 구조에서 필드를 선언하기 위한 명시적 구문을 도입하면 더 명확성에 대한 요구가 강조되고 셰이더의 데이터 구조에 대한 이해가 단순화됩니다.



함수 선언

GLSL :

 float saturate(float x) { return clamp(x, 0.0, 1.0); }


WGSL :

 fn saturate(x: f32) -> f32 { return clamp(x, 0.0, 1.0); }


WGSL의 함수 구문 변경은 선언 및 반환 값에 대한 접근 방식의 통합을 반영하여 코드를 더욱 일관되고 예측 가능하게 만듭니다.



내장된 기능

WGSL에서는 많은 내장 GLSL 함수의 이름이 바뀌거나 대체되었습니다. 예를 들어:



WGSL에 내장된 기능의 이름을 바꾸면 이름이 단순화될 뿐만 아니라 더욱 직관적이게 되어 다른 그래픽 API에 익숙한 개발자가 전환 프로세스를 쉽게 할 수 있습니다.



셰이더 변환

프로젝트를 WebGL에서 WebGPU로 변환하려는 경우 **[Naga](https://github.com/gfx-rs/naga)와 같이 GLSL을 WGSL로 자동 변환하는 도구가 있다는 것을 아는 것이 중요합니다. /)**, GLSL을 WGSL로 변환하기 위한 Rust 라이브러리입니다. WebAssembly의 도움으로 브라우저에서 바로 작동할 수도 있습니다.


Naga가 지원하는 엔드포인트는 다음과 같습니다.



컨벤션 차이점

텍스처

마이그레이션 후에는 뒤집힌 이미지 형태로 놀라운 일을 겪을 수 있습니다. OpenGL에서 Direct3D로(또는 그 반대로) 애플리케이션을 포팅한 적이 있는 사람들은 이미 이 고전적인 문제에 직면했습니다.


OpenGL 및 WebGL의 맥락에서 텍스처는 일반적으로 시작 픽셀이 왼쪽 하단 모서리에 해당하는 방식으로 로드됩니다. 그러나 실제로는 많은 개발자가 왼쪽 상단부터 이미지를 로드하여 이미지 반전 오류가 발생합니다. 그럼에도 불구하고 이 오류는 다른 요인에 의해 보상될 수 있으며 궁극적으로 문제를 완화할 수 있습니다.



OpenGL과 달리 Direct3D 및 Metal과 같은 시스템은 전통적으로 왼쪽 위 모서리를 텍스처의 시작점으로 사용합니다. 이 접근 방식이 많은 개발자에게 가장 직관적인 것으로 생각되어 WebGPU 제작자는 이 방식을 따르기로 결정했습니다.

뷰포트 공간

WebGL 코드가 프레임 버퍼에서 픽셀을 선택하는 경우 WebGPU가 다른 좌표계를 사용한다는 사실에 대비하세요. 좌표를 수정하려면 간단한 "y = 1.0 - y" 작업을 적용해야 할 수도 있습니다.



클립 공간

개발자가 개체가 예상보다 빨리 잘리거나 사라지는 문제에 직면하는 경우 이는 깊이 영역의 차이와 관련된 경우가 많습니다. WebGL과 WebGPU는 클립 공간의 깊이 범위를 정의하는 방식에 차이가 있습니다. WebGL은 -1에서 1까지의 범위를 사용하는 반면, WebGPU는 Direct3D, Metal, Vulkan과 같은 다른 그래픽 API와 유사하게 0에서 1까지의 범위를 사용합니다. 이 결정은 다른 그래픽 API로 작업하는 동안 식별된 0에서 1 사이의 범위를 사용하면 여러 가지 이점이 있기 때문에 내려졌습니다.



모델의 위치를 클립 공간으로 변환하는 주요 책임은 투영 행렬에 있습니다. 코드를 조정하는 가장 간단한 방법은 투영 행렬 출력 결과가 0에서 1 범위에 있도록 하는 것입니다. gl-matrix와 같은 라이브러리를 사용하는 경우 간단한 해결책이 있습니다. perspective 기능을 사용하는 대신 다음을 사용할 수 있습니다. perspectiveZO ; 다른 행렬 연산에도 유사한 기능을 사용할 수 있습니다.

 if (webGPU) { // Creates a matrix for a symetric perspective-view frustum // using left-handed coordinates mat4.perspectiveZO(out, Math.PI / 4, ...); } else { // Creates a matrix for a symetric perspective-view frustum // based on the default handedness and default near // and far clip planes definition. mat4.perspective(out, Math.PI / 4, …); }


그러나 때로는 기존 투영 행렬이 있고 해당 소스를 변경할 수 없는 경우도 있습니다. 이 경우 0에서 1 사이의 범위로 변환하려면 투영 행렬에 깊이 범위를 수정하는 다른 행렬을 미리 곱하면 됩니다.



WebGPU 팁과 요령

이제 WebGPU 작업에 대한 몇 가지 팁과 요령에 대해 논의하겠습니다.

사용하는 파이프라인 수를 최소화하세요.

더 많은 파이프라인을 사용할수록 더 많은 상태 전환이 발생하고 성능이 저하됩니다. 자산의 출처에 따라 이는 사소한 일이 아닐 수도 있습니다.

파이프라인을 미리 생성

파이프라인을 생성하고 즉시 사용하는 것이 효과적일 수 있지만 권장되지는 않습니다. 대신 즉시 반환하고 다른 스레드에서 작업을 시작하는 함수를 만드세요. 파이프라인을 사용할 때 실행 대기열은 보류 중인 파이프라인 생성이 완료될 때까지 기다려야 합니다. 이로 인해 심각한 성능 문제가 발생할 수 있습니다. 이를 방지하려면 파이프라인 생성과 처음 사용 사이에 약간의 시간을 두십시오.


또는 create*PipelineAsync 변형을 사용하는 것이 더 좋습니다! 파이프라인을 사용할 준비가 되면 지연 없이 Promise가 해결됩니다.

 device.createComputePipelineAsync({ compute: { module: shaderModule, entryPoint: 'computeMain' } }).then((pipeline) => { const commandEncoder = device.createCommandEncoder(); const passEncoder = commandEncoder.beginComputePass(); passEncoder.setPipeline(pipeline); passEncoder.setBindGroup(0, bindGroup); passEncoder.dispatchWorkgroups(128); passEncoder.end(); device.queue.submit([commandEncoder.finish()]); });

렌더번들 사용

렌더 번들은 사전 기록된 부분적, 재사용 가능한 렌더 패스입니다. 여기에는 대부분의 렌더링 명령(뷰포트 설정 등 제외)이 포함될 수 있으며 나중에 실제 렌더 패스의 일부로 "재생"될 수 있습니다.


 const renderPass = encoder.beginRenderPass(descriptor); renderPass.setPipeline(renderPipeline); renderPass.draw(3); renderPass.executeBundles([renderBundle]); renderPass.setPipeline(renderPipeline); renderPass.draw(3); renderPass.end();


렌더 번들은 일반 렌더 패스 명령과 함께 실행될 수 있습니다. 렌더 패스 상태는 모든 번들 실행 전후에 기본값으로 재설정됩니다. 이는 주로 그리기의 JavaScript 오버헤드를 줄이기 위해 수행됩니다. GPU 성능은 접근 방식에 관계없이 동일하게 유지됩니다.

요약

WebGPU로의 전환은 단순히 그래픽 API를 전환하는 것 이상을 의미합니다. 이는 또한 다양한 그래픽 API의 성공적인 기능과 사례를 결합하여 웹 그래픽의 미래를 향한 한 단계입니다. 이 마이그레이션을 위해서는 기술적, 철학적 변화에 대한 철저한 이해가 필요하지만 그 이점은 상당합니다.

유용한 자료 및 링크: