欢迎光临
我们一直在努力

Part 7: WebGPU中如何使用纹理和深度/模板测试

纹理和深度/模板测试

摘要:本文详细介绍了WebGPU中纹理和深度/模板测试的核心概念与实现方法。主要内容包括:1)纹理创建与采样的完整流程(从ImageBitmap到着色器访问);2)纹理压缩技术(BCn、ETC2、ASTC)及其设备兼容性检查;3)深度/模板测试的实现步骤,涵盖纹理创建、比较采样器配置、渲染通道设置和管线配置。文章通过代码示例和参数说明,帮助开发者掌握WebGPU中图像处理和深度测试的关键技术。

在大多数应用中,片段着色器需要从图片中取色,也就是从纹理(texture)中读取(采样)像素数据,作为当前片段的颜色。图片数据必须先存储到纹理对象中才能被采样。纹理数据使用自己的一套二维坐标(通常称为 UV 坐标):左上角为 (0, 0),右下角为 (1, 1);这与我们之前接触过的设备坐标(NDC)不同——NDC 左下角为 (-1, -1),右上角为 (1, 1)。两套坐标系的原点位置和取值范围都不一样,使用时不要混淆。

创建纹理和采样器

纹理资源在概念上和缓冲区很像,都是用来向 GPU 传递数据的资源;区别在于纹理专门存放图像数据,GPU 会把它存储在为图像访问优化过的专用纹理内存中,而不是普通缓冲区内存。如果纹理数据来自一张图片文件,使用纹理资源大致需要 6 步:

  • 用图片文件创建 ImageBitmap;
  • 调用 device.createTexture 创建空的纹理对象;
  • 把 ImageBitmap 的数据写入纹理内存;
  • 调用 device.createSampler 创建采样器;
  • 把采样器和纹理一起加入绑定组;
  • 在片段着色器中访问纹理和采样器。
  • 第一步:创建 ImageBitmap

    获取一张图片对应的 ImageBitmap,一般需要三步:

    // 1. 发起请求,得到 Response
    let resp = await fetch("image.png");

    // 2. 调用 Response 的 blob 方法,得到文件内容对应的 Blob 对象
    let blob = await resp.blob();

    // 3. 将 Blob 转换为 ImageBitmap
    let bitmap = await createImageBitmap(blob);

    第二步:创建纹理对象

    调用 device.createTexture 创建纹理对象,它接收一个最多包含 8 个属性的描述符对象:

    属性是否必须描述
    label 纹理对象的调试标识
    format 纹理像素的格式
    size 纹理的尺寸
    usage 指定纹理的使用方式
    dimension 纹理的维度("1d"、"2d" 或 "3d",默认为 "2d")
    sampleCount 多重采样的样本数(1 或 4,默认为 1)
    mipLevelCount mipmap 的层级数(默认为 1)
    viewFormats 额外允许从该纹理创建视图时使用的格式列表

    如:

    const texture = device.createTexture({
    size: [imageBitmap.width, imageBitmap.height, 1],
    format: "rgba8unorm",
    usage:
    GPUTextureUsage.TEXTURE_BINDING |
    GPUTextureUsage.COPY_DST |
    GPUTextureUsage.RENDER_ATTACHMENT
    });

    WebGPU 支持多种像素格式,涵盖压缩和未压缩两大类。如果纹理既不使用压缩,也不用来存储深度/模板值,format 可以设为约 36 种未压缩颜色格式中的任意一种,例如最常用的 "rgba8unorm"(每个像素 4 个通道、每通道 8 位无符号归一化整数)。

    size 应设置为一个包含纹理宽、高、深度(或数组层数)的数组或对象,例如 [width, height] 或 { width, height, depthOrArrayLayers: 1 }。usage 的设置方式和缓冲区类似,是若干标志位的按位或组合,WebGPU 为纹理定义了 5 种标志:

    usage标识描述
    TEXTURE_BINDING 允许在着色器中作为可采样纹理绑定
    COPY_SRC 允许作为拷贝操作的数据源
    COPY_DST 允许作为拷贝或写入操作的目标
    RENDER_ATTACHMENT 允许在渲染通道中用作颜色或深度/模板附件(attachment)
    STORAGE_BINDING 允许在着色器中绑定为存储纹理

    大多数应用是把图片数据写入纹理、再供着色器采样,这种最常见的场景下,usage 通常设为 TEXTURE_BINDING | COPY_DST | RENDER_ATTACHMENT:COPY_DST 是因为要把数据拷入纹理,RENDER_ATTACHMENT 则是因为拷贝操作在内部要把该纹理当作颜色附件处理。

    sampleCount 默认为 1;如果需要抗锯齿,可以创建多重采样纹理,把它设为 4(WebGPU 目前只允许 1 或 4)。

    当观察者远离物体时,物体上纹理占据的屏幕面积会变小,理想情况下也应该少读取一些细节,这就是所谓的细节级别(LOD,level of detail)。常见的实现方法是给纹理准备一组 mipmap:每一级图像的分辨率都是上一级的一半,第 0 级分辨率最高。mipLevelCount 用来指定纹理一共有多少级 mipmap,默认为 1(即不使用 mipmap)。

    第三步:将ImageBitmap写入纹理

    GPUQueue 提供了 writeTexture 和 copyExternalImageToTexture 两个方法,用来把数据拷贝进纹理资源,二者的主要区别在于支持的数据来源类型不同:writeTexture 接受普通的 ArrayBuffer/TypedArray/DataView,而 copyExternalImageToTexture 专门用来接受 ImageBitmap、HTMLVideoElement、VideoFrame、HTMLCanvasElement 或 OffscreenCanvas 这类"外部图像源"。这里我们的数据来源是 ImageBitmap,因此使用 copyExternalImageToTexture,它接受三个必填的对象参数:

    参数描述
    source 标识纹理数据来源的对象
    destination 标识数据写入到哪个纹理的对象
    copySize 标识本次拷贝的数据尺寸的对象或数组

    例如:

    device.queue.copyExternalImageToTexture(
    { source: imageBitmap },
    { texture: tex },
    [imageBitmap.width, imageBitmap.height]
    );

    source 参数对象有三个属性:

    属性是否必须描述
    source 包含数据的 ImageBitmap、HTMLVideoElement、VideoFrame、HTMLCanvasElement 或 OffscreenCanvas
    origin 从源图像的哪个偏移量(左上角为原点)开始读取,默认为 (0, 0)
    flipY 是否在拷贝前把源图像沿垂直方向翻转,默认为 false

    destination有六个属性:

    属性是否必须描述
    texture 目标 GPUTexture 对象
    origin 写入到纹理内的哪个偏移量(左上角为原点),默认为 (0, 0)
    aspect 拷贝操作影响纹理的哪个方面:"all"、"depth-only" 或 "stencil-only",默认为 "all"
    mipLevel 写入到纹理的第几级 mipmap,默认为 0
    premultipliedAlpha 标识RGB通道是否应该在渲染前乘以alpha通道,默认为false
    colorSpace 写入数据所使用的颜色空间和编码,默认为 "srgb"

    最后一个参数 copySize 标识本次要写入纹理的数据尺寸:宽、高、深度(或数组层数),既可以是形如 [width, height] 的数组,也可以是形如 { width, height } 的对象。

    第四步:创建采样器

    着色器要读取纹理中某个坐标处的数据,需要借助一种专门的资源——采样器(sampler)。调用 GPUDevice.createSampler() 即可创建,方法本身没有必传参数,接收的描述符对象所有字段都是可选的:

    属性是否必须描述
    label 采样器标识
    magFilter 采样区域大于一个纹素时如何取值("nearest" 或 "linear"),默认为 "nearest"
    minFilter 采样区域小于一个纹素时如何取值("nearest" 或 "linear"),默认为 "nearest"
    mipmapFilter 采样位置介于两级 mipmap 之间时如何取值("nearest" 或 "linear"),默认为 "nearest"
    maxAnisotropy 各向异性过滤的最大采样比例,取值 1–16,默认为 1(即不做各向异性过滤)
    lodMinClamp 采样时允许使用的最小细节级别(mip 层级),默认为 0
    lodMaxClamp 采样时允许使用的最大细节级别,默认为 32
    addressModeU 沿宽度方向访问超出纹理边界的区域时如何取值:"clamp-to-edge"、"repeat"、"mirror-repeat",默认为 "clamp-to-edge"
    addressModeV 沿高度方向访问超出纹理边界的区域时如何取值,取值同上
    addressModeW 沿深度方向(用于 3D 纹理)访问超出纹理边界的区域时如何取值,取值同上
    compare 设置后该采样器变为比较采样器,用于深度/模板测试(见后文),取值见下表

    如创建一个简单的线性采样器:

    const sampler = device.createSampler({
    magFilter: "linear",
    minFilter: "linear",
    });

    片段着色器用一对浮点数坐标去访问纹理中的某个位置。如果这个位置恰好落在多个纹素之间,就需要过滤(filter)策略来决定取哪个值:"nearest" 直接取离得最近的那个纹素,"linear" 则对周围若干纹素做加权平均(即双线性插值,bilinear filtering)。mipmap 可以看作在平面之外多出的第三个维度,如果 magFilter、minFilter、mipmapFilter 都设为 "linear",就构成了三线性过滤(trilinear filtering)。

    多数应用在水平和垂直方向读取相同数量的样本,这称为各向同性过滤(isotropic filtering),当观察方向与纹理表面垂直时效果最好。但如果观察者以一定夹角看向带纹理的表面,纹理在屏幕上会呈现出一个方向被拉伸、另一个方向被压缩的效果;为了不损失被拉伸方向上的清晰度,渲染器需要在该方向上多读取一些样本,这种按方向差异化采样的方式称为各向异性过滤(anisotropic filtering)。maxAnisotropy 设置为 N 时,表示在两个方向上读取的样本数量最大可以相差 N 倍。

    compare 属性与深度测试、模板测试相关:当采样器从深度/模板纹理中读出一个值(采样值)后,会将其与另一个值(参考值)做比较,比较结果可用于过滤或丢弃片段。取值如下:

    取值描述
    never 比较测试永不通过
    less 参考值小于采样值时通过
    equal 两个值相等时通过
    less-equal 参考值小于等于采样值时通过
    greater 参考值大于采样值时通过
    not-equal 两个值不相等时通过
    greater-equal 参考值大于等于采样值时通过
    always 比较测试总是通过

    第五步:更新绑定组

    和统一缓冲区一样,纹理和采样器也必须先加入绑定组,才能被着色器访问:

    const tex = device.createTexture(/* … */);
    const sampler = device.createSampler();

    const bindGroup = device.createBindGroup({
    layout: bindGroupLayout,
    entries: [
    {
    binding: 0,
    resource: sampler
    },
    {
    binding: 1,
    resource: tex.createView({
    dimension: "2d",
    })
    }
    ]
    });

    注意绑定组条目引用的不是纹理对象本身,而是调用纹理的 createView 方法得到的纹理视图(GPUTextureView)。createView 接受一个可选的描述符对象:

    属性是否必须描述
    label 视图标识
    dimension 视图的维度:"1d"、"2d"、"2d-array"、"3d"、"cube"、"cube-array",默认与纹理自身维度一致
    aspect 视图可以访问纹理的哪个方面:"all"、"depth-only"、"stencil-only",默认为 "all"
    format 视图使用的格式,默认为纹理自身的格式(如需使用不同格式,该格式必须出现在创建纹理时的 viewFormats 列表中)
    baseArrayLayer 视图访问的第一个数组层的索引,默认为 0
    arrayLayerCount 视图可访问的数组层数量,默认从 baseArrayLayer 到纹理末尾的所有层
    baseMipLevel 视图访问的第一个 mipmap 层级的索引,默认为 0
    mipLevelCount 视图可访问的 mipmap 层级数,默认从 baseMipLevel 到纹理末尾的所有层级

    绑定组创建之后,就可以像统一缓冲区那样和渲染通道编码器关联起来:

    renderPass.setBindGroup(0, bindGroup);

    第六步:在片段着色器中访问纹理和采样器

    在 WGSL 中声明采样器和纹理变量的方式,和统一缓冲区类似,同样使用 @group、@binding:

    @group(0) @binding(0) var sam : sampler;
    @group(0) @binding(1) var tex : texture_2d<f32>;

    WGSL 只提供两种采样器数据类型:一般用途的 sampler,以及配合比较采样器使用的 sampler_comparison(对应创建时设置了 compare 属性的采样器)。

    纹理的数据类型则依据维度、是否多重采样、是否是深度纹理而有所不同:

    数据类型描述
    texture_1d<T> 一维纹理
    texture_2d<T> 二维纹理,最常用
    texture_2d_array<T> 二维纹理数组
    texture_3d<T> 三维纹理
    texture_cube<T> 立方体贴图(cubemap)
    texture_cube_array<T> 立方体贴图数组
    texture_multisampled_2d<T> 多重采样的二维纹理
    texture_depth_2d 二维深度纹理(用于深度测试,见后文)
    texture_depth_2d_array 二维深度纹理数组
    texture_depth_cube 立方体深度纹理
    texture_depth_cube_array 立方体深度纹理数组
    texture_depth_multisampled_2d 多重采样的二维深度纹理

    其中 T 通常是 f32(对应 unorm/snorm/float 等格式),也可以是 i32 或 u32(对应有符号/无符号整数格式)——但要注意,整数格式的纹理一般不支持线性过滤,只能通过 textureLoad 按精确坐标读取,不能用 textureSample 采样。深度纹理类型(texture_depth_*)本身不带类型参数,元素恒为 f32。

    WGSL 还提供了若干函数用于访问纹理:

    函数描述
    textureSample(texture, sampler, coords) 在给定坐标处采样纹理,返回颜色值(vec4f)或深度值(f32)
    textureSampleBias(texture, sampler, coords, bias) 与 textureSample 类似,但额外对选取的 mip 层级施加一个偏移
    textureSampleCompare(texture, sampler, coords, refValue) 使用比较采样器采样深度纹理并与参考值比较,见"深度和模板测试"一节
    textureLoad(texture, coords, level) 按整数坐标直接读取纹素,不使用采样器、不做过滤
    textureDimensions(texture) 返回纹理的尺寸
    textureNumLayers(texture) 返回纹理数组的层数
    textureNumLevels(texture) 返回纹理的 mipmap 层级数
    textureNumSamples(texture) 返回多重采样纹理每个纹素的样本数

    假如绘制一个矩形并覆盖纹理,在片段着色器中可以调用textureSample方法设置片段颜色:

    struct DataStruct {
    @builtin(position) pos: vec4f,
    @location(0) uvPos: vec2f,
    }

    @group(0) @binding(0) var sam : sampler;
    @group(0) @binding(1) var tex : texture_2d<f32>;

    @vertex
    fn vs_main(@location(0) coords: vec2f, @location(1) uvCoords: vec2f) -> DataStruct {
    var outData: DataStruct;
    outData.pos = vec4f(coords, 0.0, 1.0);
    outData.uvPos = uvCoords;
    return outData;
    }

    @fragment
    fn fs_main(fragData: DataStruct) -> @location(0) vec4f {
    return textureSample(tex, sam, fragData.uvPos);
    }

    顶点缓冲区里除了顶点坐标 coords,还额外存了一份 UV 坐标 uvCoords(本文开头提到的、以左上角为原点的纹理坐标),顶点着色器把它原样透传给片段着色器,片段着色器再用它去 textureSample 里查表取色。

    纹理压缩

    多数简单应用里,每个纹素含红、绿、蓝三个通道,每通道占 8 位,一共 24 位(如果再算上 alpha 通道就是 32 位)。纹理体积越大,从内存传输到 GPU 所需的时间和带宽也越多,为此 WebGPU 支持读取经过压缩的纹理格式,例如:

    const texture = device.createTexture({

    format: "astc-5×4-unorm",

    });

    这里把 format 设为某种 ASTC 格式,即声明该纹理使用压缩数据。WebGPU 支持三大类压缩算法:

    • 块压缩(Block Compression,BCn);
    • 爱立信纹理压缩(Ericsson Texture Compression,ETC/ETC2/EAC);
    • 自适应可伸缩纹理压缩(Adaptive Scalable Texture Compression,ASTC)。

    这三类压缩格式都不是 WebGPU 的必选能力,使用前都需要通过 GPUAdapter.features 检查对应的可选特性是否被设备支持;如果没有相应特性,创建这类格式的纹理会失败。

    块压缩(BCn) 是历史最悠久的纹理压缩方式,也称为 S3 纹理压缩(S3TC)。WebGPU 提供了从 BC1 到 BC7 的一系列格式标识,它们都是把 4×4 的像素块压缩成 64 位或 128 位整数。块压缩在桌面 GPU 上普遍支持,但在移动端并不常见,使用前应检查:

    const adapter = await navigator.gpu.requestAdapter();
    if (adapter && adapter.features.has("texture-compression-bc")) {
    // 支持块压缩
    }

    爱立信纹理压缩(ETC) 同样把 4×4 像素块压缩成 64 位数据。第二代 ETC2 压缩效果更好,除了 RGB 图像外还能压缩 RGBA 图像;任何支持 OpenGL ES 3.0 及以上版本的设备都保证支持 ETC2。对于只有一个或两个颜色通道的纹理,可以使用配套的 EAC 压缩,同样在 OpenGL ES 3.x 设备上可用。检测方式:

    const adapter = await navigator.gpu.requestAdapter();
    if (adapter && adapter.features.has("texture-compression-etc2")) {
    // 支持 ETC2 / EAC
    }

    自适应可伸缩纹理压缩(ASTC) 同样是把一块纹素压缩为一个值,虽不如 ETC2 普及,但压缩质量通常更好,且可以自由选择压缩块的尺寸——块越大压缩率越高,但画质损失也越大;最小的块为 4×4,最大为 12×12。检测方式:

    const adapter = await navigator.gpu.requestAdapter();
    if (adapter && adapter.features.has("texture-compression-astc")) {
    // 支持 ASTC
    }

    深度和模板测试

    比较方式下)的那个片段,其余的应当被丢弃。每个片段的深度取值范围是 0.0(最近)到 1.0(最远)。渲染过程中,GPU 会在每个像素位置维护一个"当前最小深度值",新片段的深度只有通过比较测试(默认是"小于")才会替换掉原来保存的深度值和颜色。保存这份逐像素深度数据的结构叫深度缓冲区,也称 z 缓冲区。

    除了深度缓冲区,应用还可以往模板缓冲区(stencil buffer)里写值。片段着色器执行时可以读取模板值、和参考值做比较,并据此决定保留还是丢弃某个片段,因此模板测试常被用来"遮罩"掉渲染区域的某一部分(比如镜面反射、贴花、UI 挖孔等效果)。

    模板值通常不需要太多比特位,实践中常常把深度缓冲区和模板缓冲区合并存储在同一份纹理里:一个 32 位的纹素中,用 24 位存储深度值,8 位存储模板值(对应格式 "depth24plus-stencil8")。

    在 WebGPU 中实现深度/模板测试大致需要 5 步:

  • 创建一张纹理充当深度/模板缓冲区;
  • 创建并配置一个比较采样器,用于在着色器中读取该纹理;
  • 配置渲染通道编码器,使其在渲染时访问这张深度/模板纹理;
  • 配置渲染管线,声明要执行的深度/模板测试规则;
  • (如果还需要在片段着色器里手动做比较)声明比较采样器变量并调用 textureSampleCompare。
  • 需要说明的是,第 4 步配置好之后,深度测试本身(按像素比较、写入深度缓冲区)是渲染管线自动完成的,不需要在片段着色器里手写代码;第 5 步的 textureSampleCompare 是另一种更主动的用法——让着色器自己去读取(可能是另一张、例如阴影贴图这样的)深度纹理并做比较,常见于阴影映射(shadow mapping)等技术。下面依次展开这 5 步。

    创建深度/模板纹理

    深度/模板纹理通常不会被写入图像数据(它的内容由 GPU 在渲染时自动生成),所以一般不需要设置 GPUTextureUsage.COPY_DST;如果确实需要从 CPU 端预先写入或从别处拷贝深度/模板数据,才需要加上这个标志。纹理尺寸通常设置为和渲染目标(canvas 或颜色 attachment)一致,以便覆盖每一个像素。纹理的 format 必须是以下几种深度/模板专用格式之一:

    格式描述
    stencil8 只存储 8 位模板值,不含深度
    depth16unorm 用 16 位无符号归一化整数存储深度值
    depth24plus 至少用 24 位存储深度值,不含模板
    depth24plus-stencil8 至少用 24 位存储深度值,另用 8 位存储模板值
    depth32float 用 32 位浮点数存储深度值,不含模板
    depth32float-stencil8 用 32 位浮点数存储深度值,另用 8 位整数存储模板值

    注意:depth32float-stencil8 是一个可选格式,需要设备支持同名的可选特性 "depth32float-stencil8" 才能使用,创建纹理前应先用 adapter.features.has("depth32float-stencil8") 检查,避免在不支持的设备上创建失败。

    创建深度/模板纹理示例:

    const depthStencilTexture = device.createTexture({
    size: [400, 200, 1],
    usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
    format: "depth24plus-stencil8"
    });

    应用需要创建视图才能在渲染通道访问它:

    const depthStencilView = depthStencilTexture.createView();

    创建比较采样器

    如果要在片段着色器中主动读取深度/模板纹理(例如自行实现阴影映射),同样需要通过采样器访问,只不过用的是比较采样器(comparison sampler)。创建方式是在 createSampler 时指定 compare 属性;对应的绑定组布局条目也要把采样器类型声明为 "comparison":

    const sampler = device.createSampler({
    compare: "less",
    });

    const bindGroupLayout = device.createBindGroupLayout({
    entries: [
    {
    binding: 0,
    visibility: GPUShaderStage.FRAGMENT,
    sampler: {
    type: "comparison",
    },
    },
    {
    binding: 1,
    visibility: GPUShaderStage.FRAGMENT,
    texture: {
    sampleType: "depth",
    },
    },
    ],
    });

    其中第二个条目对应深度纹理本身:绑定深度纹理时,texture.sampleType 应设为 "depth"(而不是采样普通颜色纹理时默认的 "float"),这样绑定组布局才能和着色器里声明的 texture_depth_2d 等深度纹理类型正确匹配。

    配置渲染通道编码器

    为了提升渲染效率,GPU 会用帧缓冲(framebuffer)统一管理一次渲染涉及的各种缓冲区。此前我们在创建渲染通道时传入的 colorAttachments,就是把一个或多个颜色缓冲区关联到帧缓冲上;如果还需要做深度/模板测试,则要额外设置 depthStencilAttachment,把深度/模板缓冲区也关联进来:

    const renderPassDescriptor = {
    colorAttachments: [
    { /* … */ },
    ],
    depthStencilAttachment: {
    view: depthStencilView,
    depthClearValue: 1.0,
    depthLoadOp: "clear",
    depthStoreOp: "store",
    stencilClearValue: 0,
    stencilLoadOp: "clear",
    stencilStoreOp: "store"
    }
    };

    depthStencilAttachment 描述符的字段如下:

    属性是否可选描述
    view 要读写的深度/模板纹理视图
    depthReadOnly 深度部分是否只读,默认为 false
    depthClearValue depthLoadOp 为 "clear" 时,深度缓冲区每个元素的初始值
    depthLoadOp 是(含深度方面且非只读时必须提供) 渲染前如何初始化深度值:"load"(沿用已有内容)或 "clear"(清空为 depthClearValue)
    depthStoreOp 是(同上,必须提供) 渲染后是否保存深度结果:"store" 或 "discard"
    stencilReadOnly 模板部分是否只读,默认为 false
    stencilClearValue stencilLoadOp 为 "clear" 时,模板缓冲区每个元素的初始值
    stencilLoadOp 是(含模板方面且非只读时必须提供) 渲染前如何初始化模板值,取值同 depthLoadOp
    stencilStoreOp 是(同上,必须提供) 渲染后是否保存模板结果,取值同 depthStoreOp

    提示:depthLoadOp/depthStoreOp(以及模板对应的两个字段)在表中虽标记为"可选",但只要 view 对应的纹理格式含有深度(或模板)方面、且没有把 depthReadOnly(或 stencilReadOnly)设为 true,就必须同时显式提供这一对 load/store 操作,否则会报错——它们并没有隐式默认值。另外,depthClearValue 常见的做法是设为 1.0(代表最远处),这样场景中任何片段的深度默认都能通过后面提到的 "less" 比较测试,不会一开始就被"最远"的初始值挡住。

    配置渲染管线

    创建渲染管线时,device.createRenderPipeline 接受一个 depthStencil 对象来配置具体的测试规则,它最多有 10 个字段:

    属性是否可选描述
    format 深度/模板缓冲区的格式,必须和创建对应纹理时的格式一致
    depthCompare 指定用于将片段深度与 depthStencilAttachment 的深度值进行比较的比较运算:never、less、equal、less-equal、greater、not-equal、greater-equal、always
    depthWriteEnabled 测试通过时,是否允许把新的深度值写回深度缓冲区
    depthBias 深度测试前,给每个片段的深度额外加上的一个偏移量,默认为 0
    depthBiasSlopeScale 按片段深度在屏幕空间中的斜率进一步缩放偏移量的系数,默认为 0
    depthBiasClamp 限制上面两项计算出的偏移量的最大值,默认为 0
    stencilFront 正面朝向图元的模板测试配置
    stencilBack 背面朝向图元的模板测试配置
    stencilReadMask 指定读操作应该访问模板值的哪些比特,默认为0xffffffff
    stencilWriteMask 指定写操作应该更改模板值的哪些比特,默认为0xffffffff

    一般希望"离相机更近"的片段覆盖"更远"的片段,所以 depthCompare 最常用的取值就是 "less":只有新片段深度小于(更靠近相机)缓冲区里已有的值,测试才通过。对于复杂的深度测试,depthWriteEnabled 通常设为 true,这样通过测试的片段才会更新深度缓冲区,供后续片段继续比较。

    如果两个物体的深度非常接近甚至相同,光栅化时可能出现两者的三角形交错、闪烁的现象(俗称 z-fighting),常见的缓解办法是给其中一个物体的深度加一点微小偏移,让二者不再"打架":depthBias 提供一个固定偏移量,depthBiasSlopeScale 则会乘以该像素处深度在水平、垂直方向上的最大斜率,斜率越陡(比如与视线夹角很小的地面)偏移越大,depthBiasClamp 用来限制最终偏移量不至于过大。

    stencilFront 和 stencilBack 分别针对正面、背面图元配置模板测试,二者的结构相同,都可以设置以下四个字段:

  • compare:模板测试使用的比较函数,取值与 depthCompare 相同;
  • passOp:模板测试通过时,如何更新模板值;
  • failOp:模板测试未通过时,如何更新模板值;
  • depthFailOp:模板测试通过但深度测试未通过时,如何更新模板值。
  • passOp、failOp、depthFailOp 可以取以下 8 个值之一:

    • keep:保留当前模板值;
    • zero:将模板值置为 0;
    • replace:将模板值替换为当前渲染状态设置的参考值;
    • invert:按位翻转模板值;
    • increment-clamp:模板值加 1,达到最大值后保持不变(钳位);
    • increment-wrap:模板值加 1,超出最大值后回绕为 0;
    • decrement-clamp:模板值减 1,到 0 后保持不变(钳位);
    • decrement-wrap:模板值减 1,小于 0 后回绕为最大值。

    一个配置了深度、模板测试的渲染管线大致如下:

    const pipeline = device.createRenderPipeline({
    layout: device.createPipelineLayout({ /* … */ }),
    vertex: { /* … */ },
    fragment: { /* … */ },
    depthStencil: {
    format: "depth24plus-stencil8",
    depthCompare: "less",
    depthWriteEnabled: true,
    stencilFront: {
    passOp: "replace",
    }
    }
    });

    执行比较测试

    前面几步配置好之后,深度/模板测试本身就已经由渲染管线自动执行了。如果还需要在片段着色器中主动读取深度纹理并比较(典型场景是实现阴影映射:先把光源视角的深度渲染进一张深度纹理,再在正常渲染时用当前片段相对光源的深度去和这张纹理比较,判断是否处于阴影中),需要先在着色器中声明比较采样器和对应的深度纹理变量:

    @group(0) @binding(0) var comparisonSampler: sampler_comparison;
    @group(0) @binding(1) var shadowMap: texture_depth_2d;

    然后调用 textureSampleCompare(texture, sampler, coords, refValue) 执行比较:

    @fragment
    fn fs_main(@location(0) shadowCoord: vec3f) -> @location(0) vec4f {
    // shadowCoord.xy 是采样坐标,shadowCoord.z 是待比较的参考深度值
    let visibility = textureSampleCompare(
    shadowMap, comparisonSampler, shadowCoord.xy, shadowCoord.z
    );
    // visibility 为 1.0 表示比较通过(不在阴影中),0.0 表示比较失败(在阴影中)
    return vec4f(vec3f(visibility), 1.0);
    }

    textureSampleCompare 返回一个介于 0.0 和 1.0 之间的 f32:0.0 表示比较失败,1.0 表示比较成功。这个结果通常直接被用作光照强度的乘数,或用于其他自定义的片段处理逻辑。

    赞(0)
    未经允许不得转载:171主机测评 » Part 7: WebGPU中如何使用纹理和深度/模板测试
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址