Việc chuyển sang WebGPU sắp tới không chỉ có nghĩa là chuyển đổi các API đồ họa. Đây cũng là một bước tiến tới tương lai của đồ họa web. Nhưng quá trình di chuyển này sẽ diễn ra tốt hơn nếu có sự chuẩn bị và hiểu biết — và bài viết này sẽ giúp bạn sẵn sàng.
Xin chào mọi người, tên tôi là Dmitrii Ivashchenko và tôi là kỹ sư phần mềm tại MY.GAMES. Trong bài viết này, chúng tôi sẽ thảo luận về sự khác biệt giữa WebGL và WebGPU sắp ra mắt, đồng thời chúng tôi sẽ trình bày cách chuẩn bị cho dự án của bạn để di chuyển.
Dòng thời gian của WebGL và WebGPU
Trạng thái hiện tại của WebGPU và những gì sắp xảy ra
Sự khác biệt về khái niệm cấp cao
Khởi tạo
• WebGL: Mô hình ngữ cảnh
• WebGPU: Model thiết bị
Chương trình và quy trình
• WebGL: Chương trình
• WebGPU: Đường dẫn
Đồng phục
• Đồng phục trong WebGL 1
• Đồng phục trong WebGL 2
• Đồng phục trong WebGPU
Trình đổ bóng
• Ngôn ngữ đổ bóng: GLSL và WGSL
• So sánh các kiểu dữ liệu
• Cấu trúc
• Khai báo hàm
• Chức năng tích hợp sẵn
• Chuyển đổi Shader
Sự khác biệt về quy ước
Kết cấu
• Không gian khung nhìn
• Khoảng trống Clip
Mẹo & thủ thuật WebGPU
• Giảm thiểu số lượng đường ống bạn sử dụng.
• Tạo trước các đường dẫn
• Sử dụng RenderBundle
Bản tóm tắt
WebGL , giống như nhiều công nghệ web khác, có nguồn gốc từ rất lâu về trước. Để hiểu động lực và động lực đằng sau việc chuyển sang WebGPU, trước tiên hãy xem nhanh lịch sử phát triển WebGL:
Trong những năm gần đây, mối quan tâm ngày càng tăng đối với các API đồ họa mới giúp cung cấp cho nhà phát triển khả năng kiểm soát và tính linh hoạt cao hơn:
Ngày nay, WebGPU có sẵn trên nhiều nền tảng như Windows, Mac và ChromeOS thông qua trình duyệt Google Chrome và Microsoft Edge, bắt đầu từ phiên bản 113. Dự kiến sẽ có hỗ trợ cho Linux và Android trong tương lai gần.
Dưới đây là một số công cụ đã hỗ trợ (hoặc cung cấp hỗ trợ thử nghiệm) cho WebGPU:
Xét đến điều này, việc chuyển đổi sang WebGPU hoặc ít nhất là chuẩn bị các dự án cho quá trình chuyển đổi như vậy dường như là một bước đi kịp thời trong tương lai gần.
Hãy thu nhỏ và xem xét một số khác biệt về mặt khái niệm cấp cao giữa WebGL và WebGPU, bắt đầu từ việc khởi tạo.
Khi bắt đầu làm việc với API đồ họa, một trong những bước đầu tiên là khởi tạo đối tượng chính để tương tác. Quá trình này khác nhau giữa WebGL và WebGPU, với một số điểm đặc biệt đối với cả hai hệ thống.
Trong WebGL, đối tượng này được gọi là "ngữ cảnh", về cơ bản đại diện cho một giao diện để vẽ trên phần tử canvas HTML5. Có được bối cảnh này khá đơn giản:
const gl = canvas.getContext('webgl');
Ngữ cảnh của WebGL thực sự được gắn với một canvas cụ thể. Điều này có nghĩa là nếu bạn cần hiển thị trên nhiều khung vẽ, bạn sẽ cần nhiều ngữ cảnh.
WebGPU giới thiệu một khái niệm mới gọi là "thiết bị". Thiết bị này đại diện cho sự trừu tượng hóa GPU mà bạn sẽ tương tác. Quá trình khởi tạo phức tạp hơn một chút so với WebGL, nhưng nó mang lại sự linh hoạt hơn:
const adapter = await navigator.gpu.requestAdapter(); const device = await adapter.requestDevice(); const context = canvas.getContext('webgpu'); context.configure({ device, format: 'bgra8unorm', });
Một trong những ưu điểm của mô hình này là một thiết bị có thể hiển thị trên nhiều khung vẽ hoặc thậm chí không hiển thị trên nhiều khung vẽ. Điều này mang lại sự linh hoạt bổ sung; ví dụ: một thiết bị có thể kiểm soát việc hiển thị trong nhiều cửa sổ hoặc ngữ cảnh.
WebGL và WebGPU thể hiện các cách tiếp cận khác nhau để quản lý và tổ chức quy trình đồ họa.
Trong WebGL, trọng tâm chính là chương trình đổ bóng. Chương trình kết hợp các trình đổ bóng đỉnh và đoạn, xác định cách chuyển đổi các đỉnh và cách tô màu từng pixel.
const program = gl.createProgram(); gl.attachShader(program, vertShader); gl.attachShader(program, fragShader); gl.bindAttribLocation(program, 'position', 0); gl.linkProgram(program);
Các bước tạo chương trình trong WebGL:
Quá trình này cho phép điều khiển đồ họa linh hoạt, nhưng cũng có thể phức tạp và dễ xảy ra lỗi, đặc biệt đối với các dự án lớn và phức tạp.
WebGPU giới thiệu khái niệm "đường ống" thay vì một chương trình riêng biệt. Đường dẫn này không chỉ kết hợp các trình đổ bóng mà còn các thông tin khác, trong WebGL, được thiết lập dưới dạng trạng thái. Vì vậy, việc tạo một đường dẫn trong WebGPU có vẻ phức tạp hơn:
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, }], }, });
Các bước để tạo quy trình trong WebGPU:
Trong khi WebGL tách biệt từng khía cạnh của kết xuất, WebGPU cố gắng gói gọn nhiều khía cạnh hơn vào một đối tượng duy nhất, làm cho hệ thống trở nên mô-đun và linh hoạt hơn. Thay vì quản lý riêng các trình đổ bóng và trạng thái kết xuất, như được thực hiện trong WebGL, WebGPU kết hợp mọi thứ vào một đối tượng đường dẫn. Điều này làm cho quá trình dễ dự đoán hơn và ít xảy ra lỗi hơn:
Các biến thống nhất cung cấp dữ liệu không đổi có sẵn cho tất cả các phiên bản đổ bóng.
Trong WebGL cơ bản, chúng tôi có khả năng đặt các biến uniform
trực tiếp thông qua lệnh gọi API.
GLSL :
uniform vec3 u_LightPos; uniform vec3 u_LightDir; uniform vec3 u_LightColor;
JavaScript :
const location = gl.getUniformLocation(p, "u_LightPos"); gl.uniform3fv(location, [100, 300, 500]);
Phương pháp này đơn giản nhưng yêu cầu nhiều lệnh gọi API cho mỗi biến uniform
.
Với sự xuất hiện của WebGL 2, giờ đây chúng ta có khả năng nhóm các biến uniform
vào bộ đệm. Mặc dù bạn vẫn có thể sử dụng các bộ đổ bóng đồng nhất riêng biệt, một lựa chọn tốt hơn là nhóm các bộ đồng phục khác nhau thành một cấu trúc lớn hơn bằng cách sử dụng bộ đệm đồng nhất. Sau đó, bạn gửi tất cả dữ liệu thống nhất này đến GPU cùng một lúc, tương tự như cách bạn có thể tải bộ đệm đỉnh trong WebGL 1. Điều này có một số lợi thế về hiệu suất, chẳng hạn như giảm lệnh gọi API và gần hơn với cách hoạt động của GPU hiện đại.
GLSL :
layout(std140) uniform ub_Params { vec4 u_LightPos; vec4 u_LightDir; vec4 u_LightColor; };
JavaScript :
gl.bindBufferBase(gl.UNIFORM_BUFFER, 1, gl.createBuffer());
Để liên kết các tập hợp con của bộ đệm đồng nhất lớn trong WebGL 2, bạn có thể sử dụng lệnh gọi API đặc biệt được gọi là bindBufferRange
. Trong WebGPU, có một thứ tương tự được gọi là độ lệch bộ đệm đồng nhất động, trong đó bạn có thể chuyển danh sách độ lệch khi gọi API setBindGroup
.
WebGPU cung cấp cho chúng tôi một phương pháp thậm chí còn tốt hơn. Trong bối cảnh này, các biến uniform
riêng lẻ không còn được hỗ trợ và công việc được thực hiện độc quyền thông qua bộ đệm 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;
JavaScript :
const buffer = device.createBuffer({ usage: GPUBufferUsage.UNIFORM, size: 8 });
GPU hiện đại thích dữ liệu được tải trong một khối lớn hơn là nhiều khối nhỏ. Thay vì tạo lại và đóng lại các bộ đệm nhỏ mỗi lần, hãy cân nhắc việc tạo một bộ đệm lớn và sử dụng các phần khác nhau của bộ đệm đó cho các lệnh rút thăm khác nhau. Cách tiếp cận này có thể làm tăng đáng kể hiệu suất.
WebGL bắt buộc hơn, đặt lại trạng thái chung với mỗi lệnh gọi và cố gắng đơn giản nhất có thể. Mặt khác, WebGPU hướng tới mục tiêu hướng đối tượng hơn và tập trung vào việc tái sử dụng tài nguyên, điều này dẫn đến hiệu quả.
Việc chuyển đổi từ WebGL sang WebGPU có vẻ khó khăn do sự khác biệt về phương pháp. Tuy nhiên, bắt đầu bằng việc chuyển đổi sang WebGL 2 như một bước trung gian có thể đơn giản hóa cuộc sống của bạn.
Việc di chuyển từ WebGL sang WebGPU yêu cầu những thay đổi không chỉ về API mà còn cả về trình đổ bóng. Thông số kỹ thuật WGSL được thiết kế để giúp quá trình chuyển đổi này diễn ra suôn sẻ và trực quan, đồng thời duy trì hiệu suất và hiệu suất cho các GPU hiện đại.
WGSL được thiết kế để trở thành cầu nối giữa WebGPU và API đồ họa gốc. So với GLSL, WGSL có vẻ dài dòng hơn một chút nhưng cấu trúc vẫn quen thuộc.
Đây là một ví dụ về đổ bóng cho kết cấu:
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); }
Bảng dưới đây so sánh các kiểu dữ liệu cơ bản và ma trận trong GLSL và WGSL:
Việc chuyển đổi từ GLSL sang WGSL thể hiện mong muốn gõ chặt chẽ hơn và xác định rõ ràng về kích thước dữ liệu, điều này có thể cải thiện khả năng đọc mã và giảm khả năng xảy ra lỗi.
Cú pháp khai báo cấu trúc cũng đã thay đổi:
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, };
Việc giới thiệu cú pháp rõ ràng để khai báo các trường trong cấu trúc WGSL nhấn mạnh mong muốn rõ ràng hơn và đơn giản hóa việc hiểu cấu trúc dữ liệu trong trình đổ bóng.
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); }
Việc thay đổi cú pháp của các hàm trong WGSL phản ánh sự thống nhất trong cách tiếp cận khai báo và trả về giá trị, làm cho mã trở nên nhất quán và dễ đoán hơn.
Trong WGSL, nhiều hàm GLSL tích hợp đã được đổi tên hoặc thay thế. Ví dụ:
Việc đổi tên các hàm tích hợp trong WGSL không chỉ đơn giản hóa tên của chúng mà còn làm cho chúng trực quan hơn, điều này có thể tạo điều kiện thuận lợi cho quá trình chuyển đổi của các nhà phát triển quen thuộc với các API đồ họa khác.
Đối với những người đang dự định chuyển đổi dự án của mình từ WebGL sang WebGPU, điều quan trọng cần biết là có các công cụ giúp tự động chuyển đổi GLSL sang WGSL, chẳng hạn như **[Naga](https://github.com/gfx-rs/naga /)**, là thư viện Rust để chuyển đổi GLSL sang WGSL. Nó thậm chí có thể hoạt động ngay trong trình duyệt của bạn với sự trợ giúp của WebAssugging.
Dưới đây là các điểm cuối được Naga hỗ trợ:
Sau khi di chuyển, bạn có thể gặp bất ngờ ở dạng hình ảnh bị lật. Những người đã từng chuyển ứng dụng từ OpenGL sang Direct3D (hoặc ngược lại) đều đã gặp phải vấn đề kinh điển này.
Trong ngữ cảnh OpenGL và WebGL, họa tiết thường được tải theo cách sao cho pixel bắt đầu tương ứng với góc dưới cùng bên trái. Tuy nhiên, trên thực tế, nhiều nhà phát triển tải hình ảnh bắt đầu từ góc trên cùng bên trái, dẫn đến lỗi lật ảnh. Tuy nhiên, lỗi này có thể được bù đắp bằng các yếu tố khác, cuối cùng giải quyết được vấn đề.
Không giống như OpenGL, các hệ thống như Direct3D và Metal theo truyền thống sử dụng góc trên bên trái làm điểm bắt đầu cho họa tiết. Cho rằng cách tiếp cận này có vẻ trực quan nhất đối với nhiều nhà phát triển, những người tạo ra WebGPU đã quyết định làm theo phương pháp này.
Nếu mã WebGL của bạn chọn pixel từ bộ đệm khung, hãy chuẩn bị cho thực tế là WebGPU sử dụng hệ tọa độ khác. Bạn có thể cần áp dụng thao tác "y = 1,0 - y" đơn giản để sửa tọa độ.
Khi nhà phát triển gặp phải vấn đề trong đó các đối tượng bị cắt bớt hoặc biến mất sớm hơn dự kiến, điều này thường liên quan đến sự khác biệt trong miền độ sâu. Có sự khác biệt giữa WebGL và WebGPU trong cách chúng xác định phạm vi độ sâu của không gian clip. Trong khi WebGL sử dụng phạm vi từ -1 đến 1 thì WebGPU sử dụng phạm vi từ 0 đến 1, tương tự như các API đồ họa khác như Direct3D, Metal và Vulkan. Quyết định này được đưa ra do một số lợi ích của việc sử dụng phạm vi từ 0 đến 1 đã được xác định khi làm việc với các API đồ họa khác.
Trách nhiệm chính trong việc chuyển đổi vị trí mô hình của bạn thành không gian clip nằm ở ma trận chiếu. Cách đơn giản nhất để điều chỉnh mã của bạn là đảm bảo rằng kết quả đầu ra ma trận chiếu của bạn nằm trong phạm vi từ 0 đến 1. Đối với những người sử dụng các thư viện như gl-matrix, có một giải pháp đơn giản: thay vì sử dụng hàm phối perspective
, bạn có thể sử dụng perspectiveZO
; các chức năng tương tự có sẵn cho các hoạt động ma trận khác.
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, …); }
Tuy nhiên, đôi khi bạn có thể có ma trận chiếu hiện có và bạn không thể thay đổi nguồn của nó. Trong trường hợp này, để chuyển đổi nó thành một phạm vi từ 0 đến 1, bạn có thể nhân trước ma trận chiếu của mình với một ma trận khác để điều chỉnh phạm vi độ sâu.
Bây giờ, hãy thảo luận về một số mẹo và thủ thuật để làm việc với WebGPU.
Bạn càng sử dụng nhiều đường dẫn, bạn càng có nhiều chuyển đổi trạng thái và hiệu suất càng thấp; điều này có thể không tầm thường, tùy thuộc vào nguồn tài sản của bạn đến từ đâu.
Tạo một quy trình và sử dụng nó ngay lập tức có thể hiệu quả, nhưng điều này không được khuyến khích. Thay vào đó, hãy tạo các hàm trả về ngay lập tức và bắt đầu làm việc trên một luồng khác. Khi bạn sử dụng quy trình, hàng thực thi cần đợi quá trình tạo quy trình đang chờ xử lý hoàn tất. Điều này có thể dẫn đến các vấn đề hiệu suất đáng kể. Để tránh điều này, hãy đảm bảo dành chút thời gian từ lúc tạo quy trình đến lần sử dụng đầu tiên.
Hoặc tốt hơn nữa, hãy sử dụng các biến thể create*PipelineAsync
! Lời hứa sẽ được giải quyết khi đường dẫn đã sẵn sàng để sử dụng mà không bị đình trệ.
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()]); });
Gói kết xuất là các lượt kết xuất được ghi trước, một phần, có thể tái sử dụng. Chúng có thể chứa hầu hết các lệnh kết xuất (ngoại trừ những thứ như thiết lập khung nhìn) và có thể được "phát lại" như một phần của quá trình kết xuất thực tế sau này.
const renderPass = encoder.beginRenderPass(descriptor); renderPass.setPipeline(renderPipeline); renderPass.draw(3); renderPass.executeBundles([renderBundle]); renderPass.setPipeline(renderPipeline); renderPass.draw(3); renderPass.end();
Các gói kết xuất có thể được thực thi cùng với các lệnh kết xuất thông thường. Trạng thái kết xuất được đặt lại về mặc định trước và sau mỗi lần thực thi gói. Điều này chủ yếu được thực hiện để giảm chi phí vẽ bằng JavaScript. Hiệu suất GPU vẫn giữ nguyên bất kể cách tiếp cận nào.
Việc chuyển đổi sang WebGPU không chỉ có nghĩa là chuyển đổi các API đồ họa. Đây cũng là một bước hướng tới tương lai của đồ họa web, kết hợp các tính năng và thực tiễn thành công từ nhiều API đồ họa khác nhau. Việc di chuyển này đòi hỏi sự hiểu biết thấu đáo về những thay đổi về mặt kỹ thuật và triết học, nhưng lợi ích thì rất đáng kể.