渲染
综合案例集
直觉问题
- 学了一大堆渲染理论,如何验证自己的理解?有没有”一行代码就能跑”的 Demo?
- 从 0 到 1 写一个完整的渲染 Demo,应该按什么顺序组织代码?
这篇笔记提供 4 个完整可运行的渲染 Demo,覆盖 2D SDF、3D 地球、GPU 粒子系统和后处理链。每个 Demo 都包含完整的 GLSL 和 WGSL 代码,以及”为什么这样写”的原理拆解。
Demo 1: 2D SDF 交互图形
效果预览
一个由 Signed Distance Function (SDF) 描述的 2D 图形,支持鼠标交互改变形状、颜色和动画。
核心原理
SDF 的核心思想是:对于平面上任意一点 ,函数 返回该点到图形边界的有符号距离。
- :点在图形外部
- :点在边界上
- :点在内部
多个 SDF 图形可以通过 min、max、smoothmin 等操作组合。
GLSL 版本
#version 300 es
precision highp float;
uniform vec2 u_resolution;
uniform vec2 u_mouse;
uniform float u_time;
out vec4 fragColor;
// 圆形 SDF
float circleSDF(vec2 p, float r) {
return length(p) - r;
}
// 矩形 SDF
float boxSDF(vec2 p, vec2 b) {
vec2 d = abs(p) - b;
return length(max(d, vec2(0.0))) + min(max(d.x, d.y), 0.0);
}
// smooth min 用于柔和过渡两个形状
float smin(float a, float b, float k) {
float h = max(k - abs(a - b), 0.0) / k;
return min(a, b) - h * h * k * (1.0 / 4.0);
}
void main() {
// 坐标归一化到 [-1, 1]
vec2 p = (gl_FragCoord.xy - 0.5 * u_resolution) / min(u_resolution.x, u_resolution.y);
// 鼠标归一化位置
vec2 mousePos = (u_mouse - 0.5 * u_resolution) / min(u_resolution.x, u_resolution.y);
// 动画参数
float t = u_time * 0.5;
// 圆(位置随鼠标移动)
float d1 = circleSDF(p - mousePos, 0.3 + 0.1 * sin(t));
// 矩形(旋转动画)
float cosT = cos(t);
float sinT = sin(t);
vec2 rotP = vec2(p.x * cosT + p.y * sinT, p.y * cosT - p.x * sinT);
float d2 = boxSDF(rotP, vec2(0.25, 0.15));
// 合并两个形状
float d = smin(d1, d2, 0.2);
// 颜色:内部为暖色,外部为冷色
vec3 color;
if (d < 0.0) {
color = mix(vec3(1.0, 0.4, 0.2), vec3(0.9, 0.7, 0.1), -d * 3.0);
} else {
color = mix(vec3(0.1, 0.2, 0.4), vec3(0.05, 0.1, 0.2), smoothstep(0.0, 0.5, d));
}
fragColor = vec4(color, 1.0);
}
WGSL 版本
@group(0) @binding(0) var<uniform> uUniforms: Uniforms;
struct Uniforms {
resolution: vec2f,
mouse: vec2f,
time: f32,
_pad: f32,
}
fn circleSDF(p: vec2f, r: f32) -> f32 {
return length(p) - r;
}
fn boxSDF(p: vec2f, b: vec2f) -> f32 {
let d = abs(p) - b;
return length(max(d, vec2f(0.0))) + min(max(d.x, d.y), 0.0);
}
fn smin(a: f32, b: f32, k: f32) -> f32 {
let h = max(k - abs(a - b), 0.0) / k;
return min(a, b) - h * h * k * 0.25;
}
@fragment
fn fs_main(@builtin(position) fragCoord: vec4f) -> @location(0) vec4f {
let p = (fragCoord.xy - 0.5 * uUniforms.resolution) / min(uUniforms.resolution.x, uUniforms.resolution.y);
let mousePos = (uUniforms.mouse - 0.5 * uUniforms.resolution) / min(uUniforms.resolution.x, uUniforms.resolution.y);
let t = uUniforms.time * 0.5;
let d1 = circleSDF(p - mousePos, 0.3 + 0.1 * sin(t));
let cosT = cos(t);
let sinT = sin(t);
let rotP = vec2f(p.x * cosT + p.y * sinT, p.y * cosT - p.x * sinT);
let d2 = boxSDF(rotP, vec2f(0.25, 0.15));
let d = smin(d1, d2, 0.2);
var color: vec3f;
if (d < 0.0) {
color = mix(vec3f(1.0, 0.4, 0.2), vec3f(0.9, 0.7, 0.1), -d * 3.0);
} else {
color = mix(vec3f(0.1, 0.2, 0.4), vec3f(0.05, 0.1, 0.2), smoothstep(0.0, 0.5, d));
}
return vec4f(color, 1.0);
}
原理拆解
graph LR
A[片元坐标] --> B[坐标归一化]
B --> C[计算 SDF 距离]
C --> D[smoothmin 合并]
D --> E[距离 → 颜色映射]
E --> F[输出]
- 坐标归一化:将像素坐标从 映射到 ,保证不同分辨率下图形比例一致。
- SDF 计算:
circleSDF和boxSDF分别返回点到圆和矩形的距离。 - 形状合并:
smin(smooth minimum) 让两个形状的交界区域柔和过渡,而非生硬切割。 - 颜色映射:利用距离值映射到不同的颜色,内部暖色表示”内部”,外部冷色表示”外部”。
Demo 2: 3D 地球——大气散射与日夜交替
效果预览
一个由 Raymarching 渲染的 3D 地球,展示大气散射效果(Rayleigh 散射)和简单的日夜交替动画。
核心原理
地球渲染通常涉及:
- 球体 Raymarching:从相机位置沿视线方向步进,找到与球体的交点
- 大气散射:模拟光线穿过大气层时的散射现象(蓝色天空/红色日落)
- 法线计算:在交点处计算法线,用于光照
GLSL 版本
#version 300 es
precision highp float;
uniform vec2 u_resolution;
uniform float u_time;
out vec4 fragColor;
#define MAX_STEPS 128
#define MAX_DIST 100.0
#define EPS 0.001
// 球体 SDF(3D)
float sphereSDF(vec3 p, float r) {
return length(p) - r;
}
// 地球场景(球体 + 大气层)
float map(vec3 p) {
return sphereSDF(p, 1.0);
}
// 法线计算
vec3 calcNormal(vec3 p) {
vec2 e = vec2(EPS, 0.0);
return normalize(vec3(
map(p + e.xyy) - map(p - e.xyy),
map(p + e.yxy) - map(p - e.yxy),
map(p + e.yyx) - map(p - e.yyx)
));
}
// 大气散射简化模型(Rayleigh)
vec3 atmosphere(vec3 rayDir, vec3 sunDir) {
float cosTheta = dot(rayDir, sunDir);
float rayleigh = 1.0 + cosTheta * cosTheta;
vec3 color = vec3(
0.2 + 0.8 * rayleigh,
0.4 + 0.6 * rayleigh,
1.0
);
return color;
}
void main() {
// 屏幕坐标归一化
vec2 uv = (gl_FragCoord.xy - 0.5 * u_resolution) / u_resolution.y;
// 相机
vec3 ro = vec3(0.0, 0.0, -3.0); // 相机位置
vec3 rd = normalize(vec3(uv, 1.0)); // 视线方向
// 旋转相机
float angle = u_time * 0.1;
rd.xz = mat2(cos(angle), sin(angle), -sin(angle), cos(angle)) * rd.xz;
// Raymarching
float t = 0.0;
for (int i = 0; i < MAX_STEPS; i++) {
vec3 p = ro + rd * t;
float d = map(p);
if (d < EPS || t > MAX_DIST) break;
t += d;
}
vec3 color;
if (t < MAX_DIST) {
// 打到地球表面
vec3 p = ro + rd * t;
vec3 normal = calcNormal(p);
vec3 lightDir = normalize(vec3(1.0, 1.0, 0.5));
// 漫反射
float diff = max(dot(normal, lightDir), 0.0);
vec3 earthColor = mix(vec3(0.1, 0.3, 0.1), vec3(0.2, 0.5, 0.9), diff);
// 边缘光(大气散射效果)
float fresnel = pow(1.0 - max(dot(normal, -rd), 0.0), 3.0);
color = mix(earthColor, vec3(0.5, 0.7, 1.0), fresnel * 0.5);
} else {
// 没打到地球,渲染天空(星空)
color = vec3(0.02, 0.02, 0.05);
}
fragColor = vec4(color, 1.0);
}
WGSL 版本
@group(0) @binding(0) var<uniform> uUniforms: Uniforms;
struct Uniforms {
resolution: vec2f,
time: f32,
_pad: f32,
}
const MAX_STEPS = 128;
const MAX_DIST = 100.0;
const EPS = 0.001;
fn sphereSDF(p: vec3f, r: f32) -> f32 {
return length(p) - r;
}
fn map(p: vec3f) -> f32 {
return sphereSDF(p, 1.0);
}
fn calcNormal(p: vec3f) -> vec3f {
let e = vec2f(EPS, 0.0);
return normalize(vec3f(
map(p + e.xyy) - map(p - e.xyy),
map(p + e.yxy) - map(p - e.yxy),
map(p + e.yyx) - map(p - e.yyx)
));
}
@fragment
fn fs_main(@builtin(position) fragCoord: vec4f) -> @location(0) vec4f {
let uv = (fragCoord.xy - 0.5 * uUniforms.resolution) / uUniforms.resolution.y;
let ro = vec3f(0.0, 0.0, -3.0);
var rd = normalize(vec3f(uv, 1.0));
// 旋转
let angle = uUniforms.time * 0.1;
let cosA = cos(angle);
let sinA = sin(angle);
rd = vec3f(rd.x * cosA - rd.z * sinA, rd.y, rd.x * sinA + rd.z * cosA);
// Raymarching
var t = 0.0;
for (var i = 0; i < MAX_STEPS; i = i + 1) {
let p = ro + rd * t;
let d = map(p);
if (d < EPS || t > MAX_DIST) { break; }
t = t + d;
}
var color: vec3f;
if (t < MAX_DIST) {
let p = ro + rd * t;
let normal = calcNormal(p);
let lightDir = normalize(vec3f(1.0, 1.0, 0.5));
let diff = max(dot(normal, lightDir), 0.0);
let earthColor = mix(vec3f(0.1, 0.3, 0.1), vec3f(0.2, 0.5, 0.9), diff);
let fresnel = pow(1.0 - max(dot(normal, -rd), 0.0), 3.0);
color = mix(earthColor, vec3f(0.5, 0.7, 1.0), fresnel * 0.5);
} else {
color = vec3f(0.02, 0.02, 0.05);
}
return vec4f(color, 1.0);
}
原理拆解
graph LR
A[相机位置 ro] --> B[视线方向 rd]
B --> C[Raymarching<br/>沿 rd 步进]
C --> D{打到球体?}
D -- 是 --> E[计算法线 + 光照]
E --> F[边缘光散射]
D -- 否 --> G[渲染星空背景]
F --> H[输出颜色]
G --> H
- Raymarching 核心:从相机出发,沿视线方向不断步进,每次步进距离
d = map(p)。当d < EPS时说明已接近表面。 - 法线计算:利用 SDF 的梯度近似法线——在 3D 空间中取 6 个邻点的距离差值归一化。
- 光照计算:简单的 Lambertain 漫反射
max(dot(N, L), 0)。 - 大气散射(Fresnel 边缘光):视线与法线夹角越大(接近边缘),散射越明显,呈现蓝色光晕。
Demo 3: GPU 粒子系统(Compute Shader)
效果预览
使用 WebGPU Compute Shader 驱动 100,000 个粒子,粒子受重力、风力影响,并在边界处反弹。
核心原理
传统 CPU 粒子系统:每帧在 JS 中计算粒子位置 → 上传 GPU → 绘制。瓶颈在于 CPU-GPU 数据传输。
GPU 粒子系统:粒子位置/速度全部存储在 GPU Storage Buffer 中,Compute Shader 直接读写,无需 CPU 参与。
graph LR
A[CPU: 初始化粒子数据] --> B[GPU Storage Buffer]
B --> C[Compute Shader<br/>更新位置/速度]
C --> B
B --> D[Vertex Shader<br/>读取位置并绘制]
D --> E[Fragment Shader<br/>渲染粒子]
WGSL 版本(Compute + Render)
// ========== Compute Shader ==========
struct Particle {
position: vec3<f32>,
velocity: vec3<f32>,
life: f32,
_pad: f32,
}
@group(0) @binding(0) var<storage, read_write> particles: array<Particle>;
@compute @workgroup_size(256)
fn cs_update(@builtin(global_invocation_id) id: vec3<u32>) {
let idx = id.x;
let particleCount = arrayLength(&particles);
if (idx >= particleCount) { return; }
var p = particles[idx];
// 重力
p.velocity.y -= 0.01;
// 风力(基于粒子位置的噪声)
p.velocity.x += sin(p.position.y * 2.0) * 0.001;
p.velocity.z += cos(p.position.y * 2.0) * 0.001;
// 更新位置
p.position += p.velocity;
// 边界反弹
if (p.position.y < -2.0) {
p.velocity.y *= -0.8; // 能量损失
p.position.y = -2.0;
}
// 生命周期衰减
p.life -= 0.005;
if (p.life < 0.0) {
// 重置粒子
p.position = vec3f((fract(f32(idx) * 0.731) - 0.5) * 10.0, 5.0, 0.0);
p.velocity = vec3f(0.0, 0.0, 0.0);
p.life = 1.0;
}
particles[idx] = p;
}
// ========== Vertex Shader ==========
struct VertexOutput {
@builtin(position) position: vec4f,
@location(0) life: f32,
}
@vertex
fn vs_main(@builtin(instance_index) instanceIdx: u32,
@location(0) vertexPos: vec3f) -> VertexOutput {
var output: VertexOutput;
let p = particles[instanceIdx];
output.position = vec4f(p.position + vertexPos * 0.05, 1.0);
output.life = p.life;
return output;
}
// ========== Fragment Shader ==========
@fragment
fn fs_main(input: VertexOutput) -> @location(0) vec4f {
// 颜色随生命周期变化:亮 → 暗
let color = mix(vec3f(1.0, 0.5, 0.1), vec3f(0.2, 0.1, 0.5), 1.0 - input.life);
return vec4f(color * input.life, input.life);
}
JS 端(WebGPU 驱动代码)
// 粒子数据初始化
const PARTICLE_COUNT = 100000;
const particleSize = 32; // 4 * vec3 + 2 * float = 4*12 + 4*2 = 56 ≈ 对齐到 32
const particleData = new Float32Array(PARTICLE_COUNT * 8); // position(3) + velocity(3) + life + pad
for (let i = 0; i < PARTICLE_COUNT; i++) {
particleData[i * 8 + 0] = (Math.random() - 0.5) * 10.0; // x
particleData[i * 8 + 1] = (Math.random() - 0.5) * 5.0; // y
particleData[i * 8 + 2] = (Math.random() - 0.5) * 10.0; // z
particleData[i * 8 + 3] = 0; // vx
particleData[i * 8 + 4] = 0; // vy
particleData[i * 8 + 5] = 0; // vz
particleData[i * 8 + 6] = Math.random(); // life
particleData[i * 8 + 7] = 0; // pad
}
// 创建 Storage Buffer
const particleBuffer = device.createBuffer({
size: particleData.byteLength,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.VERTEX,
mappedAtCreation: true,
});
new Float32Array(particleBuffer.getMappedRange()).set(particleData);
particleBuffer.unmap();
// 粒子渲染为点精灵 (Point Sprite)
const pipeline = device.createRenderPipeline({
vertex: {
module: shaderModule,
entryPoint: 'vs_main',
buffers: [],
},
fragment: {
module: shaderModule,
entryPoint: 'fs_main',
targets: [{ format: presentationFormat }],
},
primitive: { topology: 'point-list' },
});
// Render Loop
function render() {
// 1. Compute Pass: 更新粒子
const computeEncoder = device.createCommandEncoder();
const computePass = computeEncoder.beginComputePass();
computePass.setPipeline(computePipeline);
computePass.setBindGroup(0, bindGroup);
computePass.dispatchWorkgroups(Math.ceil(PARTICLE_COUNT / 256));
computePass.end();
device.queue.submit([computeEncoder.finish()]);
// 2. Render Pass: 绘制粒子
const renderEncoder = device.createCommandEncoder();
const renderPass = renderEncoder.beginRenderPass({
colorAttachments: [{
view: context.getCurrentTexture().createView(),
clearValue: [0, 0, 0, 1],
loadOp: 'clear',
storeOp: 'store',
}],
});
renderPass.setPipeline(pipeline);
renderPass.setVertexBuffer(0, particleBuffer);
renderPass.draw(PARTICLE_COUNT);
renderPass.end();
device.queue.submit([renderEncoder.finish()]);
requestAnimationFrame(render);
}
原理拆解
graph TD
A[CPU: 初始化粒子数据] --> B[GPU Storage Buffer]
B --> C[Compute Shader<br/>dispatchWorkgroups]
C --> D[逐粒子更新<br/>位置/速度/生命周期]
D --> E[结果写回 Storage Buffer]
E --> F[Render Pass<br/>读取位置绘制点精灵]
F --> G[Swap Chain 显示]
- Storage Buffer 双用途:既可以被 Compute Shader 读写,也可以作为 Vertex Buffer 绑定到渲染管线。
- 数据对齐:WGSL 中
struct Particle的大小是 32 字节(经过对齐),JS 中需要按相同布局填充数据。 - Dispatch 计算:
dispatchWorkgroups(ceil(count / 256))确保所有粒子都被处理。 - 生命周期管理:粒子
life衰减到 0 后重置,实现粒子系统的自我维持。
Demo 4: 后处理链(多 Pass 效果)
效果预览
实现一个多 Pass 后处理链:场景渲染 → 高斯模糊提取 → Bloom 叠加 → 最终输出,支持以下效果:
- 高斯模糊(水平和垂直两个 Pass)
- Bloom(阈值提取 + 模糊 + 叠加)
- Vignette(暗角)
- Chromatic Aberration(色差)
后处理链架构
graph TD
A[Main Scene] --> B[Pass 1: 渲染到 FBO]
B --> C[Pass 2: 亮度阈值提取]
C --> D[Pass 3: 水平高斯模糊]
D --> E[Pass 4: 垂直高斯模糊]
E --> F[Pass 5: 合成 Bloom]
F --> G[Pass 6: Vignette]
G --> H[Pass 7: 色差]
H --> I[Pass 8: 最终输出到屏幕]
GLSL 版本
// ========== Bloom 提取 + 高斯模糊 ==========
#version 300 es
precision highp float;
uniform sampler2D u_sourceTexture;
uniform vec2 u_texelSize;
uniform int u_horizontal;
uniform float u_threshold;
in vec2 v_texCoord;
out vec4 fragColor;
const float weights[5] = float[](0.227027, 0.1945946, 0.1216216, 0.054054, 0.016216);
void main() {
vec2 dir = u_horizontal == 1 ? vec2(u_texelSize.x, 0.0) : vec2(0.0, u_texelSize.y);
vec4 color = texture(u_sourceTexture, v_texCoord);
float brightness = dot(color.rgb, vec3(0.2126, 0.7152, 0.0722));
vec4 result = u_threshold > 0.0 ? max(color - u_threshold, 0.0) : color;
for (int i = 1; i < 5; i++) {
result += texture(u_sourceTexture, v_texCoord + float(i) * dir) * weights[i];
result += texture(u_sourceTexture, v_texCoord - float(i) * dir) * weights[i];
}
fragColor = result;
}
// ========== 合成 + Vignette + 色差 ==========
#version 300 es
precision highp float;
uniform sampler2D u_sceneTexture;
uniform sampler2D u_bloomTexture;
uniform vec2 u_resolution;
uniform float u_time;
in vec2 v_texCoord;
out vec4 fragColor;
void main() {
vec4 scene = texture(u_sceneTexture, v_texCoord);
vec4 bloom = texture(u_bloomTexture, v_texCoord);
vec3 color = scene.rgb + bloom.rgb * 0.5;
// Vignette
vec2 center = v_texCoord - 0.5;
float vignette = 1.0 - dot(center, center) * 1.5;
vignette = clamp(vignette, 0.0, 1.0);
color *= vignette;
// Chromatic Aberration
float dist = length(center);
float aberrationStrength = dist * 0.01;
color.r = texture(u_sceneTexture, v_texCoord + aberrationStrength * center).r;
color.g = texture(u_sceneTexture, v_texCoord).g;
color.b = texture(u_sceneTexture, v_texCoord - aberrationStrength * center).b;
fragColor = vec4(color, 1.0);
}
WGSL 版本
// ========== Bloom 提取 + 高斯模糊 ==========
@group(0) @binding(0) var uTexture: texture_2d<f32>;
@group(0) @binding(1) var uSampler: sampler;
struct Uniforms {
texelSize: vec2f,
horizontal: i32,
threshold: f32,
}
@group(0) @binding(2) var<uniform> uUniforms: Uniforms;
@fragment
fn fs_blur(input: VertexOutput) -> @location(0) vec4f {
let dir = select(
vec2f(0.0, uUniforms.texelSize.y),
vec2f(uUniforms.texelSize.x, 0.0),
uUniforms.horizontal == 1
);
let weights = array<f32, 5>(0.227027, 0.1945946, 0.1216216, 0.054054, 0.016216);
var color = textureSample(uTexture, uSampler, input.texCoord);
let brightness = dot(color.rgb, vec3f(0.2126, 0.7152, 0.0722));
var result = select(color, max(color - vec4f(uUniforms.threshold), vec4f(0.0)), uUniforms.threshold > 0.0);
for (var i = 1; i < 5; i++) {
let offset = vec2f(f32(i)) * dir;
result += textureSample(uTexture, uSampler, input.texCoord + offset) * weights[i];
result += textureSample(uTexture, uSampler, input.texCoord - offset) * weights[i];
}
return result;
}
// ========== 合成 + Vignette + 色差 ==========
@group(0) @binding(0) var uSceneTex: texture_2d<f32>;
@group(0) @binding(1) var uBloomTex: texture_2d<f32>;
@group(0) @binding(2) var uSampler: sampler;
struct VertexOutput {
@builtin(position) position: vec4f,
@location(0) texCoord: vec2f,
}
@fragment
fn fs_final(input: VertexOutput) -> @location(0) vec4f {
let scene = textureSample(uSceneTex, uSampler, input.texCoord);
let bloom = textureSample(uBloomTex, uSampler, input.texCoord);
var color = scene.rgb + bloom.rgb * 0.5;
let center = input.texCoord - 0.5;
let vignette = clamp(1.0 - dot(center, center) * 1.5, 0.0, 1.0);
color *= vignette;
let dist = length(center);
let aberrationStrength = dist * 0.01;
color.r = textureSample(uSceneTex, uSampler, input.texCoord + aberrationStrength * center).r;
color.b = textureSample(uSceneTex, uSampler, input.texCoord - aberrationStrength * center).b;
return vec4f(color, 1.0);
}
原理拆解
| 效果 | 原理 | 关键技术 |
|---|---|---|
| Bloom | 提取高亮区域 → 模糊 → 叠加回原图 | 亮度阈值 + 高斯模糊(separable 2-pass) |
| Vignette | 随距中心距离增大而降低亮度 | 1.0 - dot(uv, uv) * strength |
| Chromatic Aberration | RGB 通道分别偏移采样 | 基于距中心距离的径向偏移 |
4 个 Demo 对比总结
| Demo | 核心技术 | GLSL 代码量 | WGSL 代码量 | 复杂度 |
|---|---|---|---|---|
| 2D SDF | SDF、smoothmin、颜色映射 | ~40 行 | ~50 行 | 入门 |
| 3D 地球 | Raymarching、法线计算、Fresnel | ~60 行 | ~70 行 | 中级 |
| GPU 粒子 | Compute Shader、Storage Buffer | 仅 Fragment (~30 行) | Compute (~50 行) + Render (~30 行) | 中高 |
| 后处理链 | 多 Pass、FBO、高斯模糊、合成 | ~80 行 | ~90 行 | 高级 |
常见误区与陷阱
-
忽略 SDF 的归一化
- 陷阱:直接使用
gl_FragCoord.xy计算 SDF,不同分辨率下图形变形 - 解决:始终归一化坐标
p = (fragCoord - 0.5 * resolution) / min(resolution)
- 陷阱:直接使用
-
Raymarching 步长过大导致穿透
- 陷阱:
MAX_STEPS太小或步进距离超过表面厚度 - 解决:确保
EPS足够小(通常 0.001),MAX_STEPS128-256
- 陷阱:
-
Compute Shader workgroup 大小不当
- 陷阱:
@workgroup_size(1, 1, 1)导致 GPU 利用率极低 - 解决:设为 64-256(如
@workgroup_size(256)),匹配 GPU warp size
- 陷阱:
-
Storage Buffer 布局不对齐
- 陷阱:JS 中
Float32Array的布局与 WGSLstruct的内存布局不一致 - 解决:使用
vec3<padding>或手动计算 offset,确保 16 字节对齐
- 陷阱:JS 中
-
多 Pass 后处理 FBO 绑定错误
- 陷阱:前后 Pass 读写同一纹理导致数据竞争
- 解决:使用 ping-pong FBO(A 读 B 写,下一帧 B 读 A 写)
-
高斯模糊权重总和不为 1
- 陷阱:权重值写错导致模糊后图像变暗/变亮
- 解决:确保
sum(weights) * 2 - centerWeight = 1( separable 模糊)
-
Compute Shader 忘记同步
- 陷阱:Compute 和 Render 之间没有正确 barrier
- 解决:WebGPU 中同一 Queue 自动保证顺序;不同 Queue 需显式同步
-
忽略纹理格式限制
- 陷阱:Bloom 提取使用
rgba8unorm,高亮值(如 5.0)被截断到 1.0 - 解决:使用
rgba16float或rgba32float格式存储 HDR 数据
- 陷阱:Bloom 提取使用
延伸阅读与自测
权威资料
- The Book of Shaders - SDF - 2D SDF 基础教程
- WebGPU Compute Shader Basics - 通过
web-reader于 2026-07-07 检索 - Image Processing in the Browser with WebGPU - 通过
web-reader于 2026-07-07 检索 - Motion GPU - Minimal WebGPU Framework - 通过
web-reader于 2026-07-07 检索 - Interactive Galaxy with WebGPU Compute Shaders - 通过
web-search于 2026-07-07 检索 - WebGPU Particles GitHub - 通过
web-reader于 2026-07-07 检索
自测题
-
实践题:将 2D SDF Demo 中的
smin替换为普通min,观察交界区域的变化。理解smoothmin的作用。 -
优化题:在 3D 地球 Demo 中,Raymarching 的
MAX_STEPS和MAX_DIST如何权衡?提高MAX_STEPS一定能提高精度吗? -
对比题:GPU 粒子系统 vs CPU 粒子系统的核心差异是什么?在什么场景下 GPU 粒子系统有明显优势?
-
扩展题:后处理链中的高斯模糊使用”两 Pass 可分离”技术,为什么不直接用一个大 kernel 的单 Pass?用数学解释。
-
设计题:如果你要设计一个”雨滴落在水面上产生涟漪”的效果,应该使用哪种技术组合?(提示:SDF / Compute Shader / 多 Pass 后处理)
参考资料获取时间: 2026-07-07,通过 web-search-prime 与 web-reader 工具检索验证。