COMPLETE RENDERING EXAMPLE数据 · 纹理 · 管线 · 绘制
WebGPU 完整 3D 渲染
一个不依赖外部资源的纹理立方体:24 个顶点记录、36 个索引、1 张程序生成纹理和 1 个 MVP Uniform,完整走通从 CPU 数据到屏幕像素的流程。
实时结果
下面的立方体正由本页完整示例实时渲染;纹理、矩阵和顶点数据均在本地创建。
正在初始化 WebGPU…
整个渲染流程
先创建长期资源,再在每一帧更新矩阵、编码 RenderPass,最后提交到 GPU。
1模型数据position + uv + index
2GPU 资源Buffer + Texture
3Bind GroupMVP + view + sampler
4PipelineWGSL + 顶点布局
5RenderPass颜色 + 深度附件
6drawIndexed36 个索引 → 像素
六个面各用 4 个顶点记录和独立 UV,IndexBuffer 将它们组织成 12 个三角形。
CPU 计算 MVP 矩阵,通过 queue.writeBuffer 写入 64 字节 UniformBuffer。
像素上传到 GPUTexture,TextureView 与 Sampler 分别占用一个 binding。
DepthTexture 保存最近表面深度,确保立方体背后的三角形不会覆盖前面。
完整代码
三个代码块按顺序拼接即可运行;没有省略资源创建、绑定或绘制步骤。
创建数据与 GPU 资源
配置 Canvas,上传顶点、索引与 4×4 棋盘纹理,并创建 UniformBuffer。
// HTML: <canvas id="webgpu-canvas"></canvas>
const canvas = document.querySelector('#webgpu-canvas');
const adapter = await navigator.gpu?.requestAdapter();
if (!canvas || !adapter) throw new Error('WebGPU 不可用');
const device = await adapter.requestDevice();
const context = canvas.getContext('webgpu');
const format = navigator.gpu.getPreferredCanvasFormat();
context.configure({ device, format, alphaMode: 'opaque' });
// 每个顶点:position.xyz + uv.xy,共 20 字节
// 六个面各用 4 个顶点,让每个面都拥有独立的 0~1 UV
const vertices = new Float32Array([
-1,-1, 1, 0,1, 1,-1, 1, 1,1, 1, 1, 1, 1,0, -1, 1, 1, 0,0, // front
1,-1,-1, 0,1, -1,-1,-1, 1,1, -1, 1,-1, 1,0, 1, 1,-1, 0,0, // back
1,-1, 1, 0,1, 1,-1,-1, 1,1, 1, 1,-1, 1,0, 1, 1, 1, 0,0, // right
-1,-1,-1, 0,1, -1,-1, 1, 1,1, -1, 1, 1, 1,0, -1, 1,-1, 0,0, // left
-1, 1, 1, 0,1, 1, 1, 1, 1,1, 1, 1,-1, 1,0, -1, 1,-1, 0,0, // top
-1,-1,-1, 0,1, 1,-1,-1, 1,1, 1,-1, 1, 1,0, -1,-1, 1, 0,0, // bottom
]);
const indices = new Uint16Array([
0, 1, 2, 0, 2, 3, 4, 5, 6, 4, 6, 7,
8, 9,10, 8,10,11, 12,13,14, 12,14,15,
16,17,18, 16,18,19, 20,21,22, 20,22,23,
]);
function createBuffer(data, usage) {
const buffer = device.createBuffer({
size: data.byteLength, usage, mappedAtCreation: true,
});
new data.constructor(buffer.getMappedRange()).set(data);
buffer.unmap();
return buffer;
}
const vertexBuffer = createBuffer(
vertices, GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
);
const indexBuffer = createBuffer(indices, GPUBufferUsage.INDEX);
const uniformBuffer = device.createBuffer({
size: 64,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});
// 生成 4×4 棋盘纹理并上传到 GPU
const pixels = new Uint8Array(4 * 4 * 4);
for (let y = 0; y < 4; y++) for (let x = 0; x < 4; x++) {
const color = (x + y) % 2 ? [40, 200, 180, 255] : [70, 110, 255, 255];
pixels.set(color, (y * 4 + x) * 4);
}
const texture = device.createTexture({
size: [4, 4], format: 'rgba8unorm',
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
});
device.queue.writeTexture(
{ texture }, pixels, { bytesPerRow: 16 }, { width: 4, height: 4 },
);
const sampler = device.createSampler({
magFilter: 'nearest', minFilter: 'nearest',
});
WGSL:矩阵变换与纹理采样
顶点阶段读取 Uniform 和 VertexBuffer,片元阶段通过 Texture + Sampler 取色。
struct Uniforms {
mvp: mat4x4<f32>,
};
@group(0) @binding(0) var<uniform> uniforms: Uniforms;
@group(0) @binding(1) var colorTexture: texture_2d<f32>;
@group(0) @binding(2) var colorSampler: sampler;
struct VertexInput {
@location(0) position: vec3<f32>,
@location(1) uv: vec2<f32>,
};
struct VertexOutput {
@builtin(position) position: vec4<f32>,
@location(0) uv: vec2<f32>,
@location(1) objectPosition: vec3<f32>,
};
@vertex
fn vsMain(input: VertexInput) -> VertexOutput {
var output: VertexOutput;
output.position = uniforms.mvp * vec4<f32>(input.position, 1.0);
output.uv = input.uv;
output.objectPosition = input.position;
return output;
}
@fragment
fn fsMain(input: VertexOutput) -> @location(0) vec4<f32> {
let normal = normalize(cross(dpdx(input.objectPosition), dpdy(input.objectPosition)));
let light = 0.35 + 0.65 * abs(dot(normal, normalize(vec3<f32>(0.4, 0.7, 1.0))));
let texel = textureSample(colorTexture, colorSampler, input.uv);
return vec4<f32>(texel.rgb * light, texel.a);
}
@binding(0)UniformBufferMVP 矩阵 · Vertex
@binding(1)TextureView颜色纹理 · Fragment
@binding(2)Sampler采样规则 · Fragment
创建管线并逐帧绘制
Pipeline 固定顶点布局和深度状态;每帧只更新矩阵、编码命令并提交。
const module = device.createShaderModule({ code: shader });
const pipeline = device.createRenderPipeline({
layout: 'auto',
vertex: {
module, entryPoint: 'vsMain',
buffers: [{
arrayStride: 20,
attributes: [
{ shaderLocation: 0, offset: 0, format: 'float32x3' },
{ shaderLocation: 1, offset: 12, format: 'float32x2' },
],
}],
},
fragment: { module, entryPoint: 'fsMain', targets: [{ format }] },
primitive: { cullMode: 'back' },
depthStencil: {
format: 'depth24plus', depthWriteEnabled: true, depthCompare: 'less',
},
});
const bindGroup = device.createBindGroup({
layout: pipeline.getBindGroupLayout(0),
entries: [
{ binding: 0, resource: { buffer: uniformBuffer } },
{ binding: 1, resource: texture.createView() },
{ binding: 2, resource: sampler },
],
});
// 下面四个小函数只负责生成 MVP 矩阵
const multiply = (a, b) => {
const out = new Float32Array(16);
for (let c = 0; c < 4; c++) for (let r = 0; r < 4; r++)
for (let k = 0; k < 4; k++) out[c*4+r] += a[k*4+r] * b[c*4+k];
return out;
};
const perspective = (fov, aspect, near, far) => {
const f = 1 / Math.tan(fov / 2), nf = 1 / (near - far);
return new Float32Array([
f/aspect,0,0,0, 0,f,0,0, 0,0,far*nf,-1, 0,0,near*far*nf,0,
]);
};
const translation = z => new Float32Array([
1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,z,1,
]);
const rotation = t => {
const y = 0.65 + t * 0.55, x = 0.45 + t * 0.32;
return multiply(
new Float32Array([Math.cos(y),0,-Math.sin(y),0, 0,1,0,0, Math.sin(y),0,Math.cos(y),0, 0,0,0,1]),
new Float32Array([1,0,0,0, 0,Math.cos(x),Math.sin(x),0, 0,-Math.sin(x),Math.cos(x),0, 0,0,0,1]),
);
};
const depthTexture = device.createTexture({
size: [canvas.width, canvas.height], format: 'depth24plus',
usage: GPUTextureUsage.RENDER_ATTACHMENT,
});
function frame(ms) {
const projection = perspective(Math.PI / 3, canvas.width / canvas.height, .1, 100);
const mvp = multiply(projection, multiply(translation(-5), rotation(ms / 1000)));
device.queue.writeBuffer(uniformBuffer, 0, mvp);
const encoder = device.createCommandEncoder();
const pass = encoder.beginRenderPass({
colorAttachments: [{
view: context.getCurrentTexture().createView(),
clearValue: { r: .025, g: .04, b: .08, a: 1 },
loadOp: 'clear', storeOp: 'store',
}],
depthStencilAttachment: {
view: depthTexture.createView(),
depthClearValue: 1, depthLoadOp: 'clear', depthStoreOp: 'store',
},
});
pass.setPipeline(pipeline);
pass.setBindGroup(0, bindGroup);
pass.setVertexBuffer(0, vertexBuffer);
pass.setIndexBuffer(indexBuffer, 'uint16');
pass.drawIndexed(indices.length);
pass.end();
device.queue.submit([encoder.finish()]);
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
COMPLETE FLOW模型数据 → GPUBuffer / GPUTexture → BindGroup → Pipeline → RenderPass → drawIndexed
继续学习纹理上传