wgpu mipmap 示例深度解析:纹理 Mip 链的生成、采样与性能测量
wgpu mipmap 示例深度解析纹理 Mip 链的生成、采样与性能测量【免费下载链接】wgpuA cross-platform, safe, pure-Rust graphics API.项目地址: https://gitcode.com/GitHub_Trending/wg/wgpu本指南以 wgpu 官方示例库中的 mipmap 示例 为主体完整拆解如何在 wgpu 中运行时生成 mipmap 链、用不同 mip 级别采样纹理并借助 Timestamp Query 与 Pipeline Statistics Query 量化每一级 mip 生成的开销。读完本文你将掌握TextureView按 mip 级别裁剪、SamplerDescriptor::mipmap_filter语义、copy_buffer_to_texture上传数据以及查询对象QuerySet从创建、写入、resolve 到 map 回读的完整闭环。示例概览与运行方式该示例的核心目标正如其 README 所述shows how to generate and make use of mipmaps演示如何生成并使用 mipmap。它首先在 CPU 端程序化生成一张分形Mandelbrot纹理上传到 GPU 后通过逐级缩小渲染的方式构造出 10 个 mip 级别最后把一个全屏三角形贴在该纹理上绘制到窗口让纹理在屏幕空间的不同缩放比例下自动切换采样级别。运行方式在仓库根目录执行cargo run --bin wgpu-examples mipmap该二进制由 examples/features/Cargo.toml 定义name wgpu-examplespath src/main.rs。入口逻辑位于 examples/features/src/main.rs程序读取第一个命令行参数作为示例名在EXAMPLES表中查找mipmap条目并调用其main函数若未传参数或名称无效会打印全部可用示例列表。在 examples/features/src/main.rs 中可以看到mipmap同时标记了webgl: true与webgpu: true即它既可在原生环境运行也可在浏览器端通过 WebGL 2 或 WebGPU 后端运行Web 端通过 URL 查询参数?examplemipmap选择示例。mipmap 基础为什么要生成 Mip 链mipmap 是同一纹理的一组逐级缩小一半的副本序列。当纹理在屏幕上被缩小显示例如远处的墙面、地面时一个像素会覆盖纹理的多个 texel若只使用原始分辨率采样就会产生严重的闪烁与锯齿混叠。预先生成的低分辨率级别让硬件采样器能自动选取合适的级别从而获得平滑的远景与更好的缓存/带宽表现。在 wgpu 中一条完整的 mip 链由三个要素组成纹理本身具备多个 mip 级别创建Texture时通过mip_level_count声明。每个级别对应一个TextureView通过base_mip_levelmip_level_count裁剪出单级视图。采样器决定级别选择策略SamplerDescriptor::mipmap_filter控制相邻 mip 级别之间如何混合。本示例完整覆盖了以上三点并且第 2、3 点分别被用于两个阶段mip 生成阶段Blit 管线与最终绘制阶段Draw 管线。常量与资源设计从源码看整体骨架examples/features/src/mipmap/mod.rs 开头的常量直接决定了整个示例的规模const TEXTURE_FORMAT: wgpu::TextureFormat wgpu::TextureFormat::Rgba8UnormSrgb; const MIP_LEVEL_COUNT: u32 10; const MIP_PASS_COUNT: u32 MIP_LEVEL_COUNT - 1; const QUERY_FEATURES: wgpu::Features { wgpu::Features::TIMESTAMP_QUERY .union(wgpu::Features::TIMESTAMP_QUERY_INSIDE_PASSES) .union(wgpu::Features::PIPELINE_STATISTICS_QUERY) };TEXTURE_FORMAT使用Rgba8UnormSrgb注意sRGB 格式意味着着色器采样时硬件会自动做 sRGB → 线性转换而渲染输出时又会反向编码这对分形这样高动态的彩色图案很重要。MIP_LEVEL_COUNT 10纹理边长取1 MIP_PASS_COUNT 512即从 512×512 一直降到 1×1恰好 10 级。生成 10 级 mip 需要 9 次缩小操作因此MIP_PASS_COUNT 9每个 pass 由 level i-1 生成 level i。QUERY_FEATURES声明了三个可选特性用于后面的性能测量在optional_features()中返回mod.rs表示有就用没有也不影响运行。第一步在 CPU 端生成纹理数据为了不依赖外部图片资源示例在 CPU 端用迭代法生成一张 Mandelbrot 分形图mod.rsfn create_texels(size: usize, cx: f32, cy: f32) - Vecu8 { use std::iter; (0..size * size) .flat_map(|id| { let mut x 4.0 * (id % size) as f32 / (size - 1) as f32 - 2.0; let mut y 2.0 * (id / size) as f32 / (size - 1) as f32 - 1.0; let mut count 0; while count 0xFF x * x y * y 4.0 { let old_x x; x x * x - y * y cx; y 2.0 * old_x * y cy; count 1; } iter::once(0xFF - (count * 2) as u8) .chain(iter::once(0xFF - (count * 5) as u8)) .chain(iter::once(0xFF - (count * 13) as u8)) .chain(iter::once(u8::MAX)) }) .collect() }对每个像素把屏幕坐标映射到复平面后反复迭代z z² c用逃逸迭代次数编码 RGB 三个通道乘以不同系数得到彩色效果Alpha 固定为u8::MAX。分形图案的显著特点是从中心向外细节密度不断变化正是检验 mipmap 缩小采样是否产生锯齿的理想素材——从截图可以看到近处大纹理坐标跨度保留高细节远处自动切到低细节级别后依然平滑。第二步创建纹理并上传初始数据在Example::init中mod.rs示例创建了一个 512×512、带 10 个 mip 级别、用途为TEXTURE_BINDING | RENDER_ATTACHMENT | COPY_DST的 2D 纹理let texture device.create_texture(wgpu::TextureDescriptor { size: texture_extent, mip_level_count: MIP_LEVEL_COUNT, sample_count: 1, dimension: wgpu::TextureDimension::D2, format: TEXTURE_FORMAT, usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_DST, label: None, view_formats: [], });三种 usage 各司其职TEXTURE_BINDING让最终 Draw 管线以及 Blit 管线的输入侧能采样它RENDER_ATTACHMENT让 mip 生成 pass 能把它当作颜色附着点写入COPY_DST允许从 buffer 拷贝数据进来本示例用COPY_DST方式做初始上传。数据上传这里特意选择copy_buffer_to_texture而非queue.write_texture代码注释明确指出其他示例多用write_texture此处是为了展示另一种可行路径mod.rs。流程为先用create_buffer_init来自wgpu::util::DeviceExt建一个临时的COPY_SRCbuffer再在命令编码器里执行一次 buffer→texture 拷贝注意bytes_per_row: Some(4 * size)RGBA8 每行 4 字节/像素无行填充rows_per_image传Noneinit_encoder.copy_buffer_to_texture( wgpu::TexelCopyBufferInfo { buffer: temp_buf, layout: wgpu::TexelCopyBufferLayout { offset: 0, bytes_per_row: Some(4 * size), rows_per_image: None, }, }, texture.as_image_copy(), texture_extent, );as_image_copy()生成的拷贝描述默认指向base_mip_level 0因此只有第 0 级被填充其余 9 级等待后续的逐级生成。第三步逐级生成 mipBlit 管线3.1 单个三角形覆盖全屏的 Blit 着色器生成 mip 的核心思想是以第 i-1 级为输入渲染到第 i 级输入输出大小减半配合双线性过滤即完成一次缩小。负责这一操作的着色器是 blit.wgsl它只画一个大三角形覆盖整个裁剪空间顶点着色器用vertex_index0/1/2直接推出三个顶点把 UV 设为 0~2 以便覆盖全屏vertex fn vs_main(builtin(vertex_index) vertex_index: u32) - VertexOutput { var result: VertexOutput; let x i32(vertex_index) / 2; let y i32(vertex_index) 1; let tc vec2f32(f32(x) * 2.0, f32(y) * 2.0); result.position vec4f32(tc.x * 2.0 - 1.0, 1.0 - tc.y * 2.0, 0.0, 1.0); result.tex_coords tc; return result; } fragment fn fs_main(vertex: VertexOutput) - location(0) vec4f32 { return textureSample(r_color, r_sampler, vertex.tex_coords); }片段着色器仅做一次textureSample管线不绑定任何顶点缓冲buffers: []仅需rpass.draw(0..3, 0..1)三个顶点即可完成一次全屏拷贝。这就是图形学中常见的fullscreen triangle blit技巧。3.2 为每个 mip 级别创建单级 TextureView生成管线创建于generate_mipmapsmod.rs内。关键的第一步是为 10 个级别各建一个只覆盖单一 mip 的视图let views (0..mip_count) .map(|mip| { texture.create_view(wgpu::TextureViewDescriptor { label: Some(mip), format: None, dimension: None, usage: None, aspect: wgpu::TextureAspect::All, base_mip_level: mip, mip_level_count: Some(1), base_array_layer: 0, array_layer_count: None, swizzle: wgpu::TextureComponentSwizzle::default(), }) }) .collect::Vec_();base_mip_level指定起始级别、mip_level_count: Some(1)限定只看这一级。这样views[i]就是只包含第 i 级的独立视图可以同时充当 BindGroup 里的采样资源与 RenderPass 里的颜色附着点。3.3 逐级渲染循环生成过程是一个从target_mip 1到mip_count - 1的循环mod.rsfor target_mip in 1..mip_count as usize { let bind_group device.create_bind_group(wgpu::BindGroupDescriptor { layout: bind_group_layout, entries: [ wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(views[target_mip - 1]), }, wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::Sampler(sampler), }, ], label: None, }); // ... let mut rpass encoder.begin_render_pass(wgpu::RenderPassDescriptor { color_attachments: [Some(wgpu::RenderPassColorAttachment { view: views[target_mip], // ... ops: wgpu::Operations { load: wgpu::LoadOp::Clear(wgpu::Color::WHITE), store: wgpu::StoreOp::Store, }, })], // ... }); rpass.set_pipeline(pipeline); rpass.set_bind_group(0, bind_group, []); rpass.draw(0..3, 0..1); }每个 pass 的关键对应关系是角色资源说明采样输入binding 0views[target_mip - 1]上一级大一倍的图渲染目标color attachmentviews[target_mip]当前级小一半的图三角形绘制draw(0..3, 0..1)全屏 blit无需顶点缓冲由于输出分辨率恰好是输入的一半而渲染时 UV 覆盖范围不变双线性过滤会天然完成 4 个 texel → 1 个 texel 的降采样。3.4 生成阶段专用的采样器生成管线使用的采样器mod.rs有几个值得注意的设置let sampler device.create_sampler(wgpu::SamplerDescriptor { label: Some(mip), address_mode_u: wgpu::AddressMode::ClampToEdge, address_mode_v: wgpu::AddressMode::ClampToEdge, address_mode_w: wgpu::AddressMode::ClampToEdge, mag_filter: wgpu::FilterMode::Linear, min_filter: wgpu::FilterMode::Linear, mipmap_filter: wgpu::MipmapFilterMode::Nearest, ..Default::default() });mag_filter/min_filter均为Linear这是降采样正确性的关键只有线性过滤才能把 2×2 邻域平均成一个 texel若用Nearest则会产生严重混叠mipmap_filter: Nearest在本场景中其实不起作用因为输入视图views[i-1]只包含一个 mip 级别mip_level_count: Some(1)没有跨级混合的可能这里显式指定 Nearest 是一种防御性写法地址模式全部ClampToEdge避免 UV 越过边界时采样到对侧内容。第四步用 mipmap 进行渲染Draw 管线mip 链生成完毕后示例创建第二条渲染管线用于最终展示着色器为 draw.wgsl。它把两个顶点坐标100 和 1000 的量级差异映射成 UV并乘以0.05系数result.tex_coords 0.05 * pos vec2f32(0.5, 0.5);配合一个 4 顶点TriangleStripdraw(0..4, 0..1)画出一大一小两个正方形纹理坐标跨度差异很大——屏幕右侧大面积放大的部分对应低细节 mip从而让不同距离看到不同细节级别的效果非常直观。最终展示用的采样器与生成阶段相反mod.rslet sampler device.create_sampler(wgpu::SamplerDescriptor { label: None, address_mode_u: wgpu::AddressMode::Repeat, address_mode_v: wgpu::AddressMode::Repeat, address_mode_w: wgpu::AddressMode::Repeat, mag_filter: wgpu::FilterMode::Linear, min_filter: wgpu::FilterMode::Linear, mipmap_filter: wgpu::MipmapFilterMode::Linear, ..Default::default() });三个要点mipmap_filter: Linear允许采样器在相邻两级 mip 之间做三线性插值先各自采样再混合是质量最高的 mip 采样模式也是本示例make use of mipmaps的直接体现min_filter: Linear当纹素被缩小minification时用双线性过滤配合 mipmap 消除闪烁AddressMode::Repeat让纹理可无缝平铺配合展示用的大正方形铺满更多区域。BindGroup 绑定顺序为binding 0 变换矩阵 uniformglam::Mat4通过bytemuck转为[f32; 16]上传、binding 1 完整纹理视图texture.create_view(TextureViewDescriptor::default())包含全部 10 级、binding 2 采样器mod.rs。resize回调中用queue.write_buffer更新 uniform 矩阵以保持正确的透视投影mod.rs。第五步可选用 GPU Query 测量各级 mip 的生成开销这是该示例在生成 mipmap之外的第二个亮点利用查询对象量化每个 pass 的耗时与着色器调用次数。只有在设备支持QUERY_FEATURES时才启用mod.rs否则跳过整个测量逻辑。5.1 创建两组 QuerySetTimestamp 查询count mip_passes * 2因为每个 pass 需要开始 结束两个时间戳QueryType::TimestampPipeline Statistics 查询count mip_passes统计类型为FRAGMENT_SHADER_INVOCATIONS用于计算每个 pass 实际执行的片段着色器次数。时间戳是设备相关的计数器需用queue.get_timestamp_period()拿到每个计数对应的纳秒数才能在回读后换算成真实耗时。5.2 在 RenderPass 内写入查询在generate_mipmaps的循环内部围绕一次draw前后分别写入时间戳、并在 pass 生命周期内包围统计查询mod.rsrpass.write_timestamp(query_sets.timestamp, timestamp_query_index_base); rpass.begin_pipeline_statistics_query(query_sets.pipeline_statistics, pipeline_query_index_base); rpass.set_pipeline(pipeline); rpass.set_bind_group(0, bind_group, []); rpass.draw(0..3, 0..1); rpass.write_timestamp(query_sets.timestamp, timestamp_query_index_base 1); rpass.end_pipeline_statistics_query();索引安排上时间戳每个 pass 占 2 个槽timestamp_query_index_base (target_mip - 1) * 2统计查询每个 pass 占 1 个槽pipeline_query_index_base target_mip - 1。注意TIMESTAMP_QUERY_INSIDE_PASSES特性正是为了让write_timestamp能在 RenderPass 内部调用而引入的。5.3 resolve 到 Buffer 并回读所有 pass 结束后把两类查询结果解析到同一个 GPU buffer 的不同偏移处mod.rs统计结果需按QUERY_RESOLVE_BUFFER_ALIGNMENT对齐pipeline_statistics_offset取TimestampQueries大小与对齐值的最大值encoder.resolve_query_set(query_sets.timestamp, 0..timestamp_query_count, query_sets.data_buffer, 0); encoder.resolve_query_set(query_sets.pipeline_statistics, 0..MIP_PASS_COUNT, query_sets.data_buffer, pipeline_statistics_offset());随后通过copy_buffer_to_buffer把结果拷到MAP_READbuffermap_async(MapMode::Read)后在device.poll(PollType::wait_indefinitely())后同步回读用bytemuck::from_bytes把裸字节解成TimestampData { start, end }数组计算耗时并打印let nanoseconds (timestamp.end - timestamp.start) as f32 * query_sets.timestamp_period; println!( Generating mip level {} took {:.3} μs and called the fragment shader {} times, idx 1, microseconds, pipeline );运行时可看到类似输出Generating mip level 1 took 12.345 μs and called the fragment shader 65536 times——level 1 的输出为 256×256 65536 像素恰好与统计结果吻合印证了每级面积减半、片段调用次数按 4 倍递减的 mip 链特性。5.4 测试中的查询分支模块底部定义了TEST与TEST_QUERY两个 GPU 测试mod.rs前者不启用查询特性后者启用QUERY_FEATURES分别对照截图screenshot.png与screenshot_query.png图片位于 examples/features/src/mipmap 目录。值得注意的工程细节带查询的测试因 CI 的 lavapipe软件 Vulkan 实现上均值误差偶发超过 0.005特意放宽了比较阈值Mean(0.03)加 99 分位0.1的容忍度代码注释解释了这一妥协mod.rs。测试注册在 examples/features/src/lib.rs 的all_tests()中随wgpu_test框架在原生与 Web 端运行。运行环境与依赖说明示例依赖wgpu、winit、glam矩阵、bytemuck数据转换等 crate具体版本均通过 workspace 统一管理见 examples/features/Cargo.toml 与仓库根目录 Cargo.toml原生端需要支持 Vulkan / Metal / DX12 的显卡或软件实现如 lavapipe并可用RUST_LOG环境变量控制日志级别见 examples/features/src/framework.rs 的日志初始化Web 端wasm32通过webgl/webgpufeature 编译框架代码对 surface 的创建时机做了平台差异处理framework.rs若设备不支持TIMESTAMP_QUERY等特性示例会自动降级为只生成 mip、不做测量模式不会崩溃。总结通过 mipmap 示例 及其完整实现 mod.rs可以提炼出在 wgpu 中处理 mipmap 的四条核心经验生成用全屏三角形 blit 单级 TextureView 线性过滤逐级渲染即可在 GPU 上构造 mip 链无需依赖generateMipmap类的扩展使用展示管线对完整纹理视图采样并把mipmap_filter设为Linear以获得三线性过滤质量验证Timestamp / Pipeline Statistics Query 让每一级 mip 的耗时与着色器调用次数可量化且统计结果与面积四分之一递减的理论完全吻合健壮性将查询特性声明为可选optional_features配合device.features().contains运行时判断保证功能在低端设备上优雅降级。这套运行时生成 mip 查询量化的组合拳既适合作为学习 wgpu 纹理管线的入门范例也可直接移植到实际项目中处理程序化纹理、动态贴图或渲染到纹理render-to-texture的场景。【免费下载链接】wgpuA cross-platform, safe, pure-Rust graphics API.项目地址: https://gitcode.com/GitHub_Trending/wg/wgpu创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考