Build a complete 3D ambient aquarium simulation using the WebGPU compute and graphics pipeline. Learn how to simulate flocking behavior (Boids algorithm) and optimize draw counts using instanced rendering.
This lesson is currently only available in Vietnamese. Please switch the language toggle in the menu to Vietnamese to read the full guide and take the interactive quiz.
Chào mừng các bạn đến với bài học cuối cùng trong chuỗi học trình WebGPU cơ bản! Lần này, chúng ta sẽ ứng dụng tất cả kiến thức đã tích lũy từ các bài học trước – bao gồm thiết lập chiều sâu depth stencil, bộ đệm lưu trữ Compute Storage Buffer, truyền tham số Uniform và nạp chất liệu – để cùng xây dựng một dự án 3D hoàn chỉnh: ColorQuarium 3D (phiên bản WebGPU bể cá Ambient nổi tiếng).
Bài học này sẽ hướng dẫn cách hiện thực hóa thuật toán di chuyển mô phỏng hành vi sinh học của đàn cá song song trên GPU và cách tối ưu vẽ hàng loạt thực thể đa giác bằng Instanced Rendering.
1. Thuật toán đàn cá Boids trên Compute Shader
Để mô phỏng đàn cá bơi lội tự nhiên không đâm vào nhau, chúng ta sử dụng thuật toán Boids (Craig Reynolds, 1987). Thuật toán này định nghĩa hành vi di chuyển của mỗi cá thể dựa trên ba quy tắc vector cục bộ:
\[ \vec{v}_{\text{steering}} = w_s \cdot \vec{f}_{\text{separation}} + w_c \cdot \vec{f}_{\text{cohesion}} + w_a \cdot \vec{f}_{\text{alignment}} \]1.1 Separation (Tách biệt)
Mỗi con cá sẽ tính toán vector đẩy ngược hướng với các con cá quá gần nó để tránh va chạm vật lý.
if (dist < rSeparation) {
forceSeparation += (pos - other.pos) / dist * (rSeparation - dist);
}
1.2 Cohesion (Hội tụ)
Mỗi con cá sẽ bị hút về phía trung tâm vị trí trung bình (trọng tâm) của các con cá xung quanh nó.
if (dist < rCohesion) {
centerPosition += other.pos;
}
1.3 Alignment (Đồng hướng)
Mỗi con cá sẽ cố gắng đồng bộ vận tốc và bơi theo hướng trung bình của những con cá lân cận.
if (dist < rAlignment) {
averageVelocity += other.vel;
}
2. Instanced Rendering & Căn hướng đối tượng trên GPU
Để vẽ hàng trăm chú cá, ta chỉ truyền đúng 1 mô hình lưới đa giác (mesh) của cá vào đệm đỉnh. Sau đó
dùng lệnh gọi drawIndexed(indicesCount, fishCount) để yêu cầu GPU nhân bản mesh này lên
$N$ lần.
Trong Vertex Shader, ta đọc thuộc tính vị trí và hướng bay của thực thể cá hiện tại thông qua chỉ số
@builtin(instance_index) từ Storage Buffer nhị phân, tự động xoay lưới cá trùng khớp với
hướng bơi hiện tại:
let p = fishes[instanceIdx];
let zAxis = normalize(p.vel); // Trục tiến hướng bơi
let xAxis = normalize(cross(vec3<f32>(0.0, 1.0, 0.0), zAxis));
let yAxis = cross(zAxis, xAxis);
let rotMatrix = mat3x3<f32>(xAxis, yAxis, zAxis);
let rotatedPos = rotMatrix * localVertexPos;
let worldPos = rotatedPos + p.pos.xyz; // .xyz: p.pos là vec4 (padding ở w)
3. Hiệu ứng động quẫy đuôi (Tail Waving) trên Vertex Shader
Để mô phỏng đàn cá sống động như bơi thật thay vì di chuyển cứng ngắc như tàu vũ trụ, ta uốn cong đuôi cá (tọa độ $z$ âm) qua hàm sóng lượng giác hình sin ngay trong Vertex Shader:
// Uốn cong tọa độ x của các đỉnh nằm ở đuôi cá theo thời gian
let wave = sin(u.time * 14.0 - localVertexPos.z * 18.0) * 0.07;
if (localVertexPos.z < -0.05) {
let factor = (-0.05 - localVertexPos.z) / 0.13;
localVertexPos.x += wave * factor;
}
Bể cá Ambient: ColorQuarium 3D Sandbox
Dưới đây là bể cá 3D hoàn chỉnh được tăng tốc phần cứng WebGPU. 200 thực thể cá neon được mô phỏng hành vi bầy đàn động học thời gian thực, với đuôi quẫy uốn lượn uốn khúc và ánh sáng khúc xạ mờ dưới đại dương.
*Hãy điều chỉnh thanh trượt để thay đổi hành vi và tốc độ bơi của đàn cá.*
Điều khiển đàn cá
struct Params {
cohesion : f32,
separation : f32,
alignment : f32,
maxSpeed : f32,
deltaTime : f32,
fishCount : u32,
time : f32,
};
struct Fish {
pos : vec3<f32>,
vel : vec3<f32>,
color : vec3<f32>,
};
@group(0) @binding(0) var<uniform> u : Params;
@group(0) @binding(1) var<storage, read> srcFish : array<Fish>;
@group(0) @binding(2) var<storage, read_write> dstFish : array<Fish>;
@compute @workgroup_size(64)
fn main(@builtin(global_invocation_id) id : vec3<u32>) {
let idx = id.x;
if (idx >= u.fishCount) { return; }
var f = srcFish[idx];
var pos = f.pos;
var vel = f.vel;
var forceCohesion = vec3<f32>(0.0);
var forceSeparation = vec3<f32>(0.0);
var forceAlignment = vec3<f32>(0.0);
var countCohesion = 0.0;
var countSeparation = 0.0;
var countAlignment = 0.0;
// Lặp tìm các lân cận để lái hành vi bầy đàn
for (var i = 0u; i < u.fishCount; i = i + 1u) {
if (i == idx) { continue; }
let other = srcFish[i];
let diff = pos - other.pos;
let dist = length(diff);
if (dist < 0.18 && dist > 0.001) {
forceSeparation += (diff / dist) * (0.18 - dist);
countSeparation += 1.0;
}
}
// Tích hợp di chuyển Euler và giới hạn tốc độ tối đa...
dstFish[idx].pos = pos + vel * u.deltaTime;
dstFish[idx].vel = vel;
dstFish[idx].color = f.color;
}
// Quy trình Double-Buffering chuyển đổi GPU Buffers qua các khung hình
let srcIndex = 0;
function frame() {
const commandEncoder = device.createCommandEncoder();
// 1. Compute Pass mô phỏng Boids đàn cá
const compPass = commandEncoder.beginComputePass();
compPass.setPipeline(computePipeline);
compPass.setBindGroup(0, computeBindGroups[srcIndex]);
compPass.dispatchWorkgroups(Math.ceil(fishCount / 64));
compPass.end();
// 2. Render Pass vẽ Instanced Rendering 200 con cá chỉ bằng 1 lệnh gọi
const renderPass = commandEncoder.beginRenderPass(renderPassDesc);
renderPass.setPipeline(renderPipeline);
renderPass.setVertexBuffer(0, vertexBuffer);
renderPass.setIndexBuffer(indexBuffer, 'uint16');
renderPass.setBindGroup(0, renderBindGroups[1 - srcIndex]); // Đọc đầu ra của compute
renderPass.drawIndexed(indicesLength, fishCount); // Instanced
renderPass.end();
device.queue.submit([commandEncoder.finish()]);
// Hoán đổi buffer nguồn/đích
srcIndex = 1 - srcIndex;
requestAnimationFrame(frame);
}
Tài liệu tham khảo chuyên sâu & Trích dẫn Học thuật:
- Nghiên cứu gốc đề xuất mô phỏng bầy đàn Boids (1987) của Craig Reynolds: Craig Reynolds - Boids Flocking Behavior Model.
- Hướng dẫn vẽ Instanced Rendering tăng tốc đồ họa WebGPU của W3C: W3C WebGPU specification - Instanced drawing.
Trắc nghiệm ôn tập
Câu 1: Kỹ thuật Instanced Rendering mang lại lợi ích lớn nhất nào cho hiệu năng đồ họa 3D?
Trắc nghiệm ôn tập
Câu 2: Tại sao trong dự án bể cá, chúng ta cần cơ chế Double-Buffering (2 đệm lưu trữ luân phiên) cho Compute Shader?