返回知识库

渲染

片元与像素处理

片元与像素处理 封面
WebGPU/WebGLShaderGLSL/WGSL渲染管线图形学

直觉问题:GPU 如何”画”出每个像素?

想象你画水彩画:先用铅笔勾勒轮廓,然后在轮廓内填充颜色。GPU 渲染也类似:先确定”轮廓”(三角形占据哪些像素),然后”填充颜色”(计算每个像素的颜色值)。

但 GPU 面临更复杂的问题:

  • 三角形可能只覆盖像素的一部分,边缘该画什么颜色?
  • 多个三角形重叠,哪个应该显示在最前面?
  • 如何防止”锯齿”,让边缘看起来平滑?

这就涉及 片元与像素处理——GPU 渲染管线的”光栅化与测试阶段”,负责将抽象的图元转换为屏幕上的最终像素。

NOTE

本篇聚焦于 光栅化到帧缓冲 的过程,包括 Fragment Shader、深度测试、模板测试、多重采样等关键环节。

核心概念:从图元到像素的转换

光栅化(Rasterization)

光栅化是 GPU 将连续的几何图元(三角形、线段、点)转换为离散的像素(片元)的过程。每个片元包含:

  • 屏幕坐标(Screen Space,xyxy
  • 深度值(Depth,zz
  • 插值属性(Interpolated Attributes,如纹理坐标、颜色、法线)

光栅化流程:

  1. 三角形设置(Triangle Setup):计算三角形边方程、平面方程
  2. 三角形遍历(Triangle Traversal):扫描三角形覆盖的像素
  3. 片元生成(Fragment Generation):为每个覆盖的像素生成片元
  4. 属性插值(Attribute Interpolation):插值计算逐片元属性
flowchart LR
    A[顶点变换<br>Vertex Shader] --> B[图元装配<br>Primitive Assembly]
    B --> C[光栅化<br>Rasterization]
    C --> D[片元生成<br>Fragment Generation]
    D --> E[属性插值<br>Interpolation]
    E --> F[片元着色<br>Fragment Shader]
    F --> G[深度测试<br>Depth Test]
    G --> H[模板测试<br>Stencil Test]
    H --> I[混合<br>Blending]
    I --> J[写入帧缓冲<br>Frame Buffer]
    
    style C fill:#e3f2fd,stroke:#2196f3,stroke-width:3px
    style F fill:#bbdefb,stroke:#2196f3
    style G fill:#90caf9,stroke:#2196f3
    style H fill:#64b5f6,stroke:#2196f3
    style I fill:#42a5f5,stroke:#2196f3

TIP

光栅化是 不可编程 的阶段,GPU 硬件自动完成。开发者只能控制其输入(顶点数据)和输出(片元属性)。

片元着色器(Fragment Shader)

Fragment Shader 是管线中第二个可编程阶段,它对每个片元执行一次,主要职责是:

  1. 逐片元计算:计算片元的颜色、深度、透明度等
  2. 纹理采样:读取纹理数据,计算最终颜色
  3. 光照计算:计算逐片元光照(Phong/Blinn-Phong/PBR)
  4. 深度修改:可选地修改片元深度值(gl_FragDepth / @builtin(frag_depth)

WARNING

Fragment Shader 的执行次数 = 三角形覆盖的片元数量。一个 100×100 像素的三角形可能产生 10,000 个片元,每个片元都会调用 Fragment Shader 一次。

深度测试(Depth Test)

深度测试用于解决 遮挡关系问题,确保离相机近的物体遮挡远处的物体。GPU 使用 深度缓冲区(Depth Buffer,也叫 Z-Buffer)存储每个像素的深度值(zz 值)。

深度测试流程

  1. 片元生成后,GPU 查询深度缓冲区中对应位置的当前深度值 zcurrentz_{current}
  2. 比较 zfragmentz_{fragment}zcurrentz_{current}(通常是”小于”或”小于等于”)
  3. 如果通过测试,更新深度缓冲区;否则,丢弃片元
graph TD
    A[片元生成] --> B{深度测试}
    B -->|通过| C[更新深度缓冲区]
    B -->|失败| D[丢弃片元]
    C --> E[继续后续流程]
    D --> E
    
    style B fill:#e3f2fd,stroke:#2196f3,stroke-width:3px
    style C fill:#bbdefb,stroke:#2196f3
    style D fill:#ffcdd2,stroke:#f44336

NOTE

  • WebGL/GLSL 的深度测试通过 gl.enable(gl.DEPTH_TEST) 启用,通过 gl.depthFunc(gl.LEQUAL) 设置比较函数。
  • WebGPU/WGSL 的深度测试通过 depthStencil 状态配置。

模板测试(Stencil Test)

模板测试用于实现 复杂的渲染效果,如镜子反射、阴影体积、UI 遮罩等。GPU 使用 模板缓冲区(Stencil Buffer)存储每个像素的模板值(通常为 8 位整数)。

模板测试流程

  1. 片元生成后,GPU 查询模板缓冲区中对应位置的当前模板值 scurrents_{current}
  2. 比较 sreferences_{reference}scurrents_{current}(由开发者指定比较函数)
  3. 如果通过测试,根据模板操作更新模板缓冲区
stateDiagram-v2
    [*] --> 模板测试
    模板测试 --> 通过: 条件满足
    模板测试 --> 失败: 条件不满足
    通过 --> 深度测试
    失败 --> 深度测试: 若配置继续
    失败 --> [*]: 丢弃
    深度测试 --> 通过: 深度合格
    深度测试 --> 失败: 深度不合格
    通过 --> 更新模板
    失败 --> 更新模板: 若配置继续
    失败 --> [*]: 丢弃
    更新模板 --> 混合
    混合 --> 写入帧缓冲
    写入帧缓冲 --> [*]
graph TD
    A[片元生成] --> B{模板测试}
    B -->|通过| C{深度测试}
    B -->|失败| D[根据失败操作更新模板]
    C -->|通过| E[根据通过操作更新模板]
    C -->|失败| F[根据深度失败操作更新模板]
    D --> G[后续流程]
    E --> G
    F --> G
    
    style B fill:#e3f2fd,stroke:#2196f3,stroke-width:3px
    style D fill:#ffcdd2,stroke:#f44336
    style E fill:#c8e6c9,stroke:#4caf50
    style F fill:#ffe0b2,stroke:#ff9800

TIP

  • 模板测试在深度测试 之前 执行,可以提前丢弃不符合条件的片元。
  • 常用场景:渲染平面镜的反射(只在镜面区域渲染反射物体)、渲染阴影体积、实现圆形/圆角 UI 控件。

混合(Blending)

混合用于实现 透明效果,将片元的颜色与帧缓冲区中已有的颜色进行混合。混合公式: Cfinal=Csource×Fsource+Cdestination×FdestinationC_{final} = C_{source} \times F_{source} + C_{destination} \times F_{destination}

其中:

  • CsourceC_{source}:片元的源颜色(来自 Fragment Shader)
  • CdestinationC_{destination}:帧缓冲区中的目标颜色(已渲染的像素)
  • FsourceF_{source}:源混合因子(如 SRC_ALPHA
  • FdestinationF_{destination}:目标混合因子(如 ONE_MINUS_SRC_ALPHA

常用混合模式

  • 标准透明混合SRC_ALPHA + ONE_MINUS_SRC_ALPHAglBlendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
  • 加法混合SRC_ALPHA + ONEglBlendFunc(gl.SRC_ALPHA, gl.ONE),用于发光效果)
  • 乘法混合ZERO + SRC_COLORglBlendFunc(gl.ZERO, gl.SRC_COLOR),用于遮罩)

WARNING

  • 混合会 禁用深度写入gl.depthMask(gl.FALSE)),否则透明物体的深度值会遮挡后续的不透明物体。
  • 渲染透明物体时,通常需要 从后往前渲染(先渲染离相机远的透明物体,再渲染近的)。

多重采样(Multisample Anti-Aliasing,MSAA)

多重采样是一种 抗锯齿技术,通过在每个像素内采样多个子像素来消除锯齿。MSAA 的工作原理:

  1. 子像素采样:每个像素被划分为多个子像素(如 4× MSAA = 16 个子像素)
  2. 覆盖测试:每个子像素独立进行覆盖测试
  3. 颜色计算:片元颜色只计算一次,然后根据子像素覆盖比例混合
  4. Resolve:最终渲染时,将子像素合并为一个像素颜色
graph TD
    A[普通渲染<br>单像素] --> B[4× MSAA<br>16 子像素]
    B --> C[每个子像素独立覆盖测试]
    C --> D[片元颜色计算一次]
    D --> E[根据覆盖比例混合]
    E --> F[Resolve 为单像素]
    
    style B fill:#e3f2fd,stroke:#2196f3,stroke-width:3px
    style F fill:#bbdefb,stroke:#2196f3

TIP

  • MSAA 只解决 几何边缘锯齿,不解决纹理锯齿(需要 Mipmapping)。
  • MSAA 的性能开销与采样数成正比(4× MSAA ≈ 4× 性能开销)。
  • WebGPU 支持 MSAA,但需要额外的 Render Pass 配置。

Early-Z 和 Hi-Z

Early-Z 是一种优化技术,在 Fragment Shader 执行 之前 进行深度测试,提前丢弃被遮挡的片元。

graph TD
    A[片元生成] --> B[Early-Z 测试]
    B -->|通过| C[Fragment Shader]
    B -->|失败| D[丢弃片元]
    C --> E[深度测试]
    E -->|通过| F[更新深度缓冲区]
    E -->|失败| G[丢弃片元]
    
    style B fill:#e3f2fd,stroke:#2196f3,stroke-width:3px
    style D fill:#ffcdd2,stroke:#f44336

Hi-Z(Hierarchical Z)是 Early-Z 的改进版本,使用 层次化的深度缓冲区来加速深度测试:

  • 将屏幕划分为多个层级,每个层级存储对应区域的最小/最大深度值
  • 深度测试时先检查粗层级,如果整个区域都遮挡,则直接丢弃
  • 递归检查细层级,直到确定最终的深度结果

WARNING

  • Early-Z 假设 Fragment Shader 不会修改深度值(gl_FragDepth / @builtin(frag_depth))。
  • 如果 Fragment Shader 修改了深度值,Early-Z 可能会失效,导致性能下降。
  • 透明物体(禁用深度写入)会禁用 Early-Z。

数学原理:光栅化与插值

三角形光栅化

GPU 使用 边方程(Edge Function)进行三角形光栅化。给定三角形的三个顶点 A,B,CA, B, C(屏幕坐标),边方程定义为: E(P,A,B)=(xBxA)(yPyA)(yByA)(xPxA)E(P, A, B) = (x_B - x_A)(y_P - y_A) - (y_B - y_A)(x_P - x_A)

PP 在三角形内部的充要条件是: E(P,A,B)0,E(P,B,C)0,E(P,C,A)0E(P, A, B) \geq 0, \quad E(P, B, C) \geq 0, \quad E(P, C, A) \geq 0

GPU 在硬件上高效计算边方程,遍历三角形覆盖的像素。

属性插值

片元的属性(如纹理坐标、颜色、法线)是通过对顶点属性进行 插值 得到的。GPU 支持 三种插值模式

  1. 透视插值(Perspective Interpolation,默认):考虑透视除法的影响 ufragment=uA×wA+uB×wB+uC×wCwA+wB+wCu_{fragment} = \frac{u_A \times w_A + u_B \times w_B + u_C \times w_C}{w_A + w_B + w_C} 其中 w=1/zw = 1/z 是顶点深度的倒数。

  2. 线性插值(Linear Interpolation):不考虑透视,适用于 2D 渲染 ufragment=uA×α+uB×β+uC×γu_{fragment} = u_A \times \alpha + u_B \times \beta + u_C \times \gamma 其中 α,β,γ\alpha, \beta, \gamma 是重心坐标。

  3. 重心插值(Centroid Interpolation):只在片元覆盖的子像素内插值,用于 MSAA

NOTE

  • GLSL 使用 smooth(透视插值,默认)、flat(无插值,直接使用顶点属性)、noperspective(线性插值)修饰符。
  • WGSL 使用 @interpolate(perspective)(默认)、@interpolate(linear)@interpolate(flat) 修饰符。

重心坐标(Barycentric Coordinates)

重心坐标是描述点在三角形内位置的参数。给定三角形 ABCABC 和点 PP,重心坐标 (α,β,γ)(\alpha, \beta, \gamma) 满足: P=αA+βB+γC,α+β+γ=1P = \alpha A + \beta B + \gamma C, \quad \alpha + \beta + \gamma = 1

重心坐标可用于:

  • 属性插值uP=αuA+βuB+γuCu_P = \alpha u_A + \beta u_B + \gamma u_C
  • 点在三角形内判断α,β,γ0\alpha, \beta, \gamma \geq 0
  • 重心着色:根据重心坐标计算颜色

TIP

  • GPU 硬件自动计算重心坐标,开发者无需手动计算。
  • 如果需要手动计算,可以使用边方程的倒数作为重心坐标。

GLSL vs WGSL 代码对照

基础 Fragment Shader 对比

GLSL (WebGL 2.0)

#version 300 es
precision mediump float;

in vec3 v_normal;
in vec2 v_texcoord;

uniform sampler2D u_texture;
uniform vec3 u_lightDirection;
uniform vec3 u_lightColor;

out vec4 fragColor;

void main() {
    // 纹理采样
    vec4 texColor = texture(u_texture, v_texcoord);
    
    // 漫反射光照(Lambert)
    float diffuse = max(dot(normalize(v_normal), normalize(u_lightDirection)), 0.0);
    vec3 lighting = texColor.rgb * u_lightColor * diffuse;
    
    // 输出最终颜色
    fragColor = vec4(lighting, texColor.a);
}

WGSL (WebGPU)

struct Uniforms {
    lightDirection: vec3<f32>,
    lightColor: vec3<f32>,
}

@group(0) @binding(0) var<uniform> uniforms: Uniforms;
@group(0) @binding(1) var texSampler: sampler;
@group(0) @binding(2) var texture: texture_2d<f32>;

struct FragmentInput {
    @location(0) normal: vec3<f32>,
    @location(1) texcoord: vec2<f32>,
}

@fragment
fn fragment_shader(input: FragmentInput) -> @location(0) vec4<f32> {
    // 纹理采样
    let texColor = textureSample(texture, texSampler, input.texcoord);
    
    // 漫反射光照(Lambert)
    let normal = normalize(input.normal);
    let lightDir = normalize(uniforms.lightDirection);
    let diffuse = max(dot(normal, lightDir), 0.0);
    let lighting = texColor.rgb * uniforms.lightColor * diffuse;
    
    // 输出最终颜色
    return vec4<f32>(lighting, texColor.a);
}

语法差异对比表

概念GLSLWGSL差异说明
输出变量out vec4 fragColor@location(0) vec4<f32>WGSL 使用返回值 + Location
纹理采样texture(sampler2D, uv)textureSample(texture, sampler, uv)WGSL 分离纹理和采样器
Uniform 绑定uniform sampler2D u_texture@group(0) @binding(N)WGSL 使用 Binding Group
精度修饰符precision mediump floatWGSL 默认 32 位浮点
插值修饰符smooth in vec3 v_normal@location(0) normal: vec3<f32>WGSL 默认透视插值
深度修改gl_FragDepth = 0.5@builtin(frag_depth)WGSL 需要显式声明
丢弃片元discarddiscard语法相同

实例:深度测试 + Early-Z

GLSL 启用深度测试(JavaScript):

// 启用深度测试
gl.enable(gl.DEPTH_TEST);
gl.depthFunc(gl.LEQUAL);  // 深度小于等于时通过测试

// 清除深度缓冲区
gl.clearDepth(1.0);
gl.clear(gl.DEPTH_BUFFER_BIT);

// 渲染场景
// ...

WGSL 配置深度测试(Rust/WebGPU):

// 创建深度纹理
let depth_texture = device.create_texture(&wgpu::TextureDescriptor {
    size: wgpu::Extent3d {
        width: 1024,
        height: 768,
        depth_or_array_layers: 1,
    },
    mip_level_count: 1,
    sample_count: 1,
    dimension: wgpu::TextureDimension::D2,
    format: wgpu::TextureFormat::Depth24PlusStencil8,
    usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
    label: Some("Depth Texture"),
});

// 配置深度测试
let depth_stencil = wgpu::DepthStencilState {
    format: wgpu::TextureFormat::Depth24PlusStencil8,
    depth_write_enabled: true,
    depth_compare: wgpu::CompareFunction::LessEqual,  // 深度小于等于时通过测试
    stencil: wgpu::StencilState::default(),
    bias: wgpu::DepthBiasState::default(),
};

// 渲染通道
let render_pass_descriptor = wgpu::RenderPassDescriptor {
    label: Some("Render Pass"),
    color_attachments: &[Some(wgpu::RenderPassColorAttachment {
        view: &color_view,
        resolve_target: None,
        ops: wgpu::Operations {
            load: wgpu::LoadOp::Clear(wgpu::Color {
                r: 0.0,
                g: 0.0,
                b: 0.0,
                a: 1.0,
            }),
            store: wgpu::StoreOp::Store,
        },
    })],
    depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
        view: &depth_texture.create_view(&wgpu::TextureViewDescriptor::default()),
        depth_ops: Some(wgpu::Operations {
            load: wgpu::LoadOp::Clear(1.0),  // 清除深度为 1.0
            store: wgpu::StoreOp::Store,
        }),
        stencil_ops: None,
    }),
};

实例:透明渲染 + 混合

GLSL 透明渲染(JavaScript + Shader):

// 启用混合
gl.enable(gl.BLEND);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);

// 禁用深度写入(透明物体)
gl.depthMask(gl.FALSE);

// 渲染透明物体
// ...

// 恢复深度写入
gl.depthMask(gl.TRUE);
// Fragment Shader(计算透明度)
precision mediump float;

in vec2 v_texcoord;
uniform sampler2D u_texture;

out vec4 fragColor;

void main() {
    vec4 texColor = texture(u_texture, v_texcoord);
    
    // 如果完全透明,丢弃片元(优化混合)
    if (texColor.a < 0.01) {
        discard;
    }
    
    fragColor = texColor;
}

WGSL 透明渲染(Rust/WebGPU):

// 配置混合
let blend = Some(wgpu::BlendState {
    color: wgpu::BlendComponent {
        src_factor: wgpu::BlendFactor::SrcAlpha,
        dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
        operation: wgpu::BlendOperation::Add,
    },
    alpha: wgpu::BlendComponent {
        src_factor: wgpu::BlendFactor::One,
        dst_factor: wgpu::BlendFactor::One,
        operation: wgpu::BlendOperation::Add,
    },
});

// 禁用深度写入
let depth_write_enabled = false;
// Fragment Shader(计算透明度)
@fragment
fn fragment_shader(input: FragmentInput) -> @location(0) vec4<f32> {
    let texColor = textureSample(texture, texSampler, input.texcoord);
    
    // 如果完全透明,丢弃片元(优化混合)
    if (texColor.a < 0.01) {
        discard;
    }
    
    return texColor;
}

常见误区与陷阱

1. 混合与深度测试冲突

误区:透明物体可以和不透明物体一样启用深度写入和深度测试。

正确理解

  • 透明物体必须 禁用深度写入gl.depthMask(gl.FALSE) / depth_write_enabled = false),否则会遮挡后续的透明物体。
  • 透明物体通常需要 从后往前渲染(先渲染离相机远的透明物体,再渲染近的),否则混合顺序错误。

正确流程

  1. 渲染所有不透明物体(启用深度写入和测试)
  2. 按深度排序透明物体(从后往前)
  3. 渲染透明物体(禁用深度写入,启用测试和混合)

错误示例

// 错误:透明物体启用深度写入
gl.depthMask(gl.TRUE);  // 错误!应该是 gl.FALSE
gl.enable(gl.BLEND);
// 渲染透明物体...

2. Early-Z 失效

误区:Early-Z 总是有效,可以优化所有场景。

正确理解

  • Early-Z 假设 Fragment Shader 不会修改深度值(gl_FragDepth / @builtin(frag_depth))。
  • 如果 Fragment Shader 修改了深度值,Early-Z 会 失效,导致性能下降。
  • 透明物体(禁用深度写入)会 禁用 Early-Z
  • Alpha Test(discard)可能会影响 Early-Z 的准确性。

优化建议

  • 尽量避免在 Fragment Shader 中写入 gl_FragDepth / @builtin(frag_depth)
  • 如果必须修改深度值,考虑使用 Z-Prepass(先渲染场景到深度缓冲区,再渲染到颜色缓冲区)。

3. 模板测试与深度测试顺序错误

误区:深度测试在模板测试之前执行。

正确理解

  • 模板测试在深度测试 之前 执行,可以提前丢弃不符合条件的片元。
  • 深度测试的通过/失败会影响模板测试的操作(模板操作分为”通过”、“深度测试失败”、“全部失败”三种)。

正确顺序

  1. 模板测试
  2. 深度测试
  3. 混合

错误示例

// 错误:假设深度测试在模板测试之前
gl.enable(gl.STENCIL_TEST);
gl.enable(gl.DEPTH_TEST);
// 模板测试会在深度测试之前执行

4. MSAA 性能陷阱

误区:MSAA 是”免费”的抗锯齿技术,可以随意开启。

正确理解

  • MSAA 的性能开销与采样数成正比(4× MSAA ≈ 4× 性能开销)。
  • MSAA 只解决 几何边缘锯齿,不解决纹理锯齿(需要 Mipmapping)。
  • MSAA 会增加内存占用(深度缓冲区、模板缓冲区都需要 MSAA)。

优化建议

  • 对于移动设备,考虑使用 FXAA(后处理抗锯齿)代替 MSAA。
  • 如果必须使用 MSAA,选择 2× 或 4× MSAA,避免 8× 或更高。
  • 对于 UI 渲染,考虑使用 SDF(Signed Distance Field) 字体代替 MSAA。

5. 丢弃片元与深度测试冲突

误区discard 可以替代深度测试,手动实现遮挡剔除。

正确理解

  • discard 会在 Fragment Shader 中 终止 片元处理,不会更新深度缓冲区。
  • discard禁用 Early-Z,导致性能下降(GPU 无法提前进行深度测试)。
  • discard 应该用于 Alpha Test(透明度阈值裁剪),而不是替代深度测试。

正确用法

// 正确:Alpha Test(透明度小于阈值时丢弃)
vec4 texColor = texture(u_texture, v_texcoord);
if (texColor.a < 0.5) {
    discard;  // 正确:Alpha Test
}

错误用法

// 错误:用 discard 替代深度测试
if (depth_value > current_depth) {
    discard;  // 错误!应该使用深度测试
}

延伸阅读与自测

权威资料索引

NOTE

以上资料通过 WebSearch 于 2026-01-18 检索。

自测题

  1. 基础概念:光栅化和 Fragment Shader 的区别是什么?GPU 在光栅化阶段和 Fragment Shader 阶段分别做什么?

  2. 数学推导:给定三角形的三个顶点 A(100,100),B(200,100),C(150,200)A(100, 100), B(200, 100), C(150, 200),请计算点 P(150,150)P(150, 150) 的重心坐标 (α,β,γ)(\alpha, \beta, \gamma)

  3. 实践应用:在 Fragment Shader 中实现一个”距离雾”效果:根据片元与相机的距离,计算雾的颜色和密度,并与片元颜色混合。请写出 GLSL 和 WGSL 的实现代码。

  4. 性能优化:假设你需要渲染 10,000 个半透明的粒子,如何优化混合和深度测试?提示:考虑粒子排序、Early-Z、Alpha Test 等。

  5. 调试技巧:你的透明物体渲染结果不正确(顺序错误或混合错误),请列出至少 3 个可能的调试步骤。提示:检查深度写入、混合函数、渲染顺序。

参考答案(简要)

  1. 光栅化 vs Fragment Shader

    • 光栅化:不可编程,GPU 硬件自动将三角形转换为片元,计算片元的屏幕坐标和深度值。
    • Fragment Shader:可编程,对每个片元计算颜色、纹理采样、光照等。
  2. 重心坐标计算

    • 边方程:E(P,A,B)=(200100)(150100)(100100)(150100)=5000E(P, A, B) = (200-100)(150-100) - (100-100)(150-100) = 5000
    • 重心坐标:(α,β,γ)=(0.5,0.5,0.0)(\alpha, \beta, \gamma) = (0.5, 0.5, 0.0)(点 PPABAB 边上)
  3. 距离雾效果

    • GLSL:float fogFactor = 1.0 - exp(-distance * fogDensity); vec3 finalColor = mix(fogColor, fragColor.rgb, fogFactor);
    • WGSL:let fogFactor = 1.0 - exp(-distance * fogDensity); let finalColor = mix(fogColor, fragColor.rgb, fogFactor);
  4. 粒子优化

    • 从后往前排序粒子(按深度降序)
    • 使用 discard 丢弃完全透明的粒子(Alpha Test)
    • 考虑使用 加法混合ONE + ONE)代替标准透明混合
    • 如果粒子很小,考虑使用 点精灵(Point Sprite)代替三角形
  5. 调试步骤

    • 检查是否禁用了深度写入(gl.depthMask(gl.FALSE) / depth_write_enabled = false
    • 检查混合函数是否正确(SRC_ALPHA + ONE_MINUS_SRC_ALPHA
    • 使用调试工具(RenderDoc/Spector.js)截取渲染过程,检查深度缓冲区和颜色缓冲区
    • 尝试禁用混合,查看深度排序是否正确

下一篇05 · 2D 渲染基础 —— 2D 变换矩阵、Sprite 渲染、字体渲染(SDF/MSDF)、瓦片地图渲染、Canvas2D vs WebGL 2D。