wgpu 离屏渲染实战:render_to_texture 示例源码逐行拆解与图片回读方案
【免费下载链接】wgpuA cross-platform, safe, pure-Rust graphics API.项目地址: https://gitcode.com/GitHub_Trending/wg/wgpu
离屏渲染(Off-Screen Rendering)是图形引擎中极为常用的能力:不把画面直接呈现到窗口或画布,而是先渲染到一张纹理上,再进行后处理、截图或后续合成。本文以 wgpu 官方示例仓库中的render_to_texture为蓝本,完整讲解"渲染到纹理 → 纹理拷贝到缓冲 → 回读为 PNG 图片"的完整链路,并逐段分析其源码实现与关键参数,帮助读者掌握可复用的 off-screen 渲染与截图代码模板。
示例定位:与 hello_triangle 的异同
render_to_texture是 wgpu 官方 examples(位于 examples/features/src/render_to_texture/)中的一个基础示例。根据其 README 的描述,它与hello_triangle非常相似——都是绘制"绿色背景上的红色三角形",但关键区别在于:
不再渲染到窗口或画布,而是渲染到一张纹理,然后把这张纹理作为图片输出(与 storage_texture 示例的输出方式相同)。
也就是说,这是一个"去掉 Surface(交换链)"的渲染管线:整个渲染流程只涉及纹理(Texture)、缓冲(Buffer)与命令编码器(CommandEncoder),完全没有窗口、画布或 present 环节。这在 examples/README.md 中被进一步阐述为:
Renders to an image texture offscreen, demonstrating both off-screen rendering as well as how to add a sort of resolution-agnostic screenshot feature to an engine.
翻译过来即:该示例同时演示了离屏渲染,以及如何为引擎添加一种"与分辨率无关的截图功能"——因为渲染目标是固定尺寸的纹理,而不是随窗口变化的交换链。
从仓库的示例注册表看,examples/features/src/main.rs 中render_to_texture被标记为webgl: false, webgpu: true,表明它在 WebGPU 后端与原生环境均可运行,但不支持 WebGL 后端。
运行方式
在仓库根目录下执行:
cargo run --bin wgpu-examples render_to_texture该命令会编译wgpu-examples这个 bin target(定义见 examples/features/Cargo.toml,bin 入口为src/main.rs),并以render_to_texture作为第一个命令行参数启动对应示例。main.rs中的get_example_name()会读取std::env::args().nth(1)(原生环境)来定位示例,然后从EXAMPLES常量表中找到名字匹配的入口函数并调用。
在原生环境,输出图片默认写入当前目录下的please_don't_git_push_me.png;也可以通过--显式指定输出路径(来自 examples/README.md 的说明):
cargo run --bin wgpu-examples -- render_to_texture "test.png"程序执行完毕后,会生成一张 512×512 的 PNG 图片,内容为绿底红色三角形。若一切正常,最终结果应该与hello_triangle中那个经典的"绿色背景上的红色三角形"如出一辙。
核心流程:从纹理到图片的完整链路
示例主体代码位于 examples/features/src/render_to_texture/mod.rs,整个run()函数(约 145 行)可以划分为五个阶段,下面逐一拆解。
阶段一:确定尺寸与初始化 GPU 上下文
const TEXTURE_DIMS: (usize, usize) = (512, 512); let mut texture_data = Vec::<u8>::with_capacity(TEXTURE_DIMS.0 * TEXTURE_DIMS.1 * 4);TEXTURE_DIMS固定为 512×512,这是整个示例的"虚拟分辨率"——渲染目标纹理、回读缓冲的容量都以此为准。texture_data是一个预留了512 × 512 × 4(RGBA 每像素 4 字节)容量的Vec<u8>,稍后用来存放从 GPU 回读的原始像素数据。
随后是标准的 wgpu 初始化三连:
let instance = wgpu::Instance::default(); let adapter = instance .request_adapter(&wgpu::RequestAdapterOptions::default()) .await .unwrap(); let (device, queue) = adapter .request_device(&wgpu::DeviceDescriptor { label: None, required_features: wgpu::Features::empty(), required_limits: wgpu::Limits::downlevel_defaults(), default_queue: wgpu::QueueDescriptor { label: None }, experimental_features: wgpu::ExperimentalFeatures::disabled(), memory_hints: wgpu::MemoryHints::MemoryUsage, trace: wgpu::Trace::Off, }) .await .unwrap();这里值得注意的是required_limits: wgpu::Limits::downlevel_defaults()——它选择了向下兼容的保守默认限制,让示例在尽可能多的硬件上都能运行。另外,由于没有 Surface,request_adapter使用RequestAdapterOptions::default()(不要求 surface 支持),这也正是离屏渲染相对窗口渲染的一个便利之处:不需要依赖任何窗口系统。
阶段二:创建渲染目标纹理与回读缓冲
渲染目标纹理是本示例的核心资源:
let render_target = device.create_texture(&wgpu::TextureDescriptor { label: None, size: wgpu::Extent3d { width: TEXTURE_DIMS.0 as u32, height: TEXTURE_DIMS.1 as u32, depth_or_array_layers: 1, }, mip_level_count: 1, sample_count: 1, dimension: wgpu::TextureDimension::D2, format: wgpu::TextureFormat::Rgba8UnormSrgb, usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC, view_formats: &[wgpu::TextureFormat::Rgba8UnormSrgb], });逐字段理解:
size:512×512×1,一张 2D 纹理;mip_level_count: 1、sample_count: 1:单 mip、无 MSAA,保持最简单;dimension: wgpu::TextureDimension::D2:二维纹理;format: wgpu::TextureFormat::Rgba8UnormSrgb:sRGB 格式,与hello_triangle保持一致,确保最终输出的颜色观感与窗口渲染一致;usage是离屏渲染的关键:RENDER_ATTACHMENT允许该纹理作为渲染通道(Render Pass)的颜色附件;COPY_SRC允许它作为copy_texture_to_buffer的拷贝源。这两个 usage 组合正是"渲染进去 + 读出来"的完整语义;view_formats声明了可用的视图格式(这里与纹理格式相同)。
紧接着创建用于回读像素数据的"暂存缓冲"(staging buffer):
let output_staging_buffer = device.create_buffer(&wgpu::BufferDescriptor { label: None, size: texture_data.capacity() as u64, // 512 * 512 * 4 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ, mapped_at_creation: false, });该缓冲的size正好是 512×512×4 字节,usage为COPY_DST | MAP_READ:先由纹理拷贝写入(COPY_DST),再由 CPU 映射读取(MAP_READ)。这正是 GPU→CPU 回读的标准缓冲形态。
阶段三:渲染管线与 WGSL 着色器
管线创建如下(顶点阶段不使用任何顶点缓冲,片元阶段只有一个颜色目标):
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { label: None, layout: None, // 无绑定组,使用默认(空)管线布局 vertex: wgpu::VertexState { module: &shader, entry_point: Some("vs_main"), compilation_options: Default::default(), buffers: &[], }, fragment: Some(wgpu::FragmentState { module: &shader, entry_point: Some("fs_main"), compilation_options: Default::default(), targets: &[Some(wgpu::TextureFormat::Rgba8UnormSrgb.into())], }), primitive: wgpu::PrimitiveState::default(), depth_stencil: None, multisample: wgpu::MultisampleState::default(), multiview_mask: None, cache: None, });要点:
layout: None:示例没有任何资源绑定(无 uniform、无纹理采样),wgpu 会使用空的默认布局;targets的颜色格式必须与渲染目标纹理格式一致,这里同样是Rgba8UnormSrgb;buffers: &[]:没有顶点缓冲,三角形的三个顶点完全由着色器内联给出。
着色器位于 examples/features/src/render_to_texture/shader.wgsl,与hello_triangle的着色器同构:
@vertex fn vs_main(@builtin(vertex_index) in_vertex_index: u32) -> @builtin(position) vec4<f32> { var vertices = array<vec4<f32>, 3>( vec4<f32>(0.0, 1.0, 0.0, 1.0), vec4<f32>(-1.0, -1.0, 0.0, 1.0), vec4<f32>(1.0, -1.0, 0.0, 1.0) ); return vertices[in_vertex_index]; } @fragment fn fs_main() -> @location(0) vec4<f32> { return vec4<f32>(1.0, 0.0, 0.0, 1.0); }顶点着色器通过内置的vertex_index在三个硬编码顶点中取下标,形成覆盖屏幕上半部的三角形;片元着色器恒定输出红色(1.0, 0.0, 0.0, 1.0)。背景的绿色则由渲染通道的LoadOp::Clear填充(见下一阶段)。
阶段四:渲染通道——把三角形画进纹理
首先为渲染目标纹理创建视图,然后编码渲染命令:
let texture_view = render_target.create_view(&wgpu::TextureViewDescriptor::default()); let mut command_encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor::default()); { let mut render_pass = command_encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: None, color_attachments: &[Some(wgpu::RenderPassColorAttachment { view: &texture_view, depth_slice: None, resolve_target: None, ops: wgpu::Operations { load: wgpu::LoadOp::Clear(wgpu::Color::GREEN), store: wgpu::StoreOp::Store, }, })], depth_stencil_attachment: None, occlusion_query_set: None, timestamp_writes: None, multiview_mask: None, }); render_pass.set_pipeline(&pipeline); render_pass.draw(0..3, 0..1); }这里与窗口渲染的差别一目了然:颜色附件(color attachment)的view不是交换链的SurfaceTexture视图,而是我们自己创建的纹理视图。这就是"渲染到纹理"的本质——把纹理当作画布。
LoadOp::Clear(wgpu::Color::GREEN)负责在绘制前把整张纹理清成绿色;StoreOp::Store保证绘制结果被写回纹理。render_pass.draw(0..3, 0..1)使用 3 个顶点、1 个实例完成三角形绘制。
阶段五:纹理 → 缓冲拷贝与 CPU 回读
渲染通道结束后,注释点明"纹理现在包含了我们渲染好的图像",接下来的核心操作是把纹理像素搬进暂存缓冲:
command_encoder.copy_texture_to_buffer( wgpu::TexelCopyTextureInfo { texture: &render_target, mip_level: 0, origin: wgpu::Origin3d::ZERO, aspect: wgpu::TextureAspect::All, }, wgpu::TexelCopyBufferInfo { buffer: &output_staging_buffer, layout: wgpu::TexelCopyBufferLayout { offset: 0, // 该值必须是 256 的倍数。这里我们恰好知道 512*4=2048 满足要求, // 因此无需手动补齐(padding),但一般场景下都需要计算对齐。 bytes_per_row: Some((TEXTURE_DIMS.0 * 4) as u32), rows_per_image: Some(TEXTURE_DIMS.1 as u32), }, }, wgpu::Extent3d { width: TEXTURE_DIMS.0 as u32, height: TEXTURE_DIMS.1 as u32, depth_or_array_layers: 1, }, ); queue.submit(Some(command_encoder.finish()));这段代码有两点重要的工程细节:
bytes_per_row必须是 256 的倍数。WGSL/WebGPU 规范要求copy_texture_to_buffer的bytes_per_row按 256 字节对齐。注释明确指出:这里因为 512 像素 × 4 字节 = 2048,恰好是 256 的倍数,所以无需填充;但在真实项目中(例如非 4 字节对齐的格式、非整数倍宽度的纹理),必须手动计算 padding。- 拷贝完成后通过
queue.submit提交整个命令缓冲,GPU 才真正开始执行渲染与拷贝。
回读阶段使用异步映射 + 显式 poll 的组合:
let buffer_slice = output_staging_buffer.slice(..); let (sender, receiver) = flume::bounded(1); buffer_slice.map_async(wgpu::MapMode::Read, move |r| sender.send(r).unwrap()); device.poll(wgpu::PollType::wait_indefinitely()).unwrap(); receiver.recv_async().await.unwrap().unwrap(); { let view = buffer_slice.get_mapped_range().unwrap(); texture_data.extend_from_slice(&view[..]); } output_staging_buffer.unmap();map_async(MapMode::Read, ...)请求将缓冲映射为可读;回调通过一个容量为 1 的flume通道把结果传回,这是典型的"回调 → Future"桥接写法;device.poll(wgpu::PollType::wait_indefinitely())是原生环境下的关键一步:阻塞等待 GPU 完成所有已提交的工作,确保映射就绪;get_mapped_range()拿到映射区间的字节切片,extend_from_slice把 512×512×4 的像素数据复制进texture_data;- 最后
unmap()释放映射,缓冲归还给 GPU。
阶段六:输出为图片
像素数据回到 CPU 后,根据目标平台调用不同的输出函数(见 examples/features/src/utils.rs):
#[cfg(not(target_arch = "wasm32"))] output_image_native(texture_data.to_vec(), TEXTURE_DIMS, _path.unwrap()); #[cfg(target_arch = "wasm32")] output_image_wasm(texture_data.to_vec(), TEXTURE_DIMS);- 原生环境:
output_image_native使用pngcrate 将 RGBA 字节编码为 PNG(encoder.set_color(png::ColorType::Rgba)),然后写入命令行指定的路径,默认是please_don't_git_push_me.png; - WASM 环境:
output_image_wasm不生成文件,而是把像素写入一个隐藏的canvas(staging canvas),再通过canvas.to_data_url()生成 data URL,赋值给页面上的<img id="output-image-target">元素,让浏览器直接展示图片。
main()入口函数(mod.rs 末尾)同样做了平台分叉:原生端用env_logger初始化日志、用pollster::block_on阻塞运行异步的run();wasm 端则使用console_log与wasm_bindgen_futures::spawn_local。
与 storage_texture 的对比:两条殊途同归的"输出图片"路线
README 提到该示例与storage_texture输出方式一致,二者也确实共享同一套output_image_native/output_image_wasm工具函数,但内部生成图片的方式完全不同:
| 维度 | render_to_texture | storage_texture |
|---|---|---|
| 着色器类型 | 渲染管线(vertex + fragment) | 计算管线(compute) |
| 写入纹理的机制 | Render Pass 颜色附件 | StorageTexture绑定,逐像素写入 |
| 纹理 usage | RENDER_ATTACHMENT \| COPY_SRC | STORAGE_BINDING \| COPY_SRC |
| 格式 | Rgba8UnormSrgb | Rgba8Unorm |
| 图像内容 | 绿底红色三角形 | Mandelbrot 集合灰度图 |
从 storage_texture 源码 可以看到,它通过dispatch_workgroups(512, 512, 1)让每个像素对应一个计算工作项,直接写入绑定为存储纹理的纹理。而render_to_texture走的则是标准的图形渲染管线。二者的回读部分(copy_texture_to_buffer+map_async+ poll)几乎完全一致——这正是"渲染/计算到纹理 → 拷贝 → 回读 → 输出图片"这一通用模板的两条具体实例。
关键注意事项总结
- usage 必须声明齐全:既要
RENDER_ATTACHMENT(让纹理可被渲染)又要COPY_SRC(让纹理可被拷贝),缺一不可;同理,回读缓冲需要COPY_DST | MAP_READ。 bytes_per_row的 256 字节对齐:copy_texture_to_buffer对行字节数有硬性对齐要求,示例因 512×4=2048 恰好满足而未做 padding;通用代码中需要按256对齐计算补齐字节数。- 颜色格式一致性:管线
targets中的格式必须与渲染目标纹理的格式一致(此处均为Rgba8UnormSrgb),否则会在管线创建或验证阶段报错。 - 回读同步:原生端依赖
device.poll(wgpu::PollType::wait_indefinitely())保证 GPU 工作完成后再读取映射数据;wasm 端则依赖异步 poll。忽略这一步骤会导致读取到未完成(甚至未初始化)的数据。 - 平台差异:示例在原生端输出 PNG 文件、在 wasm 端输出
<img>元素,且不支持 WebGL 后端,只支持原生 + WebGPU。
结语
render_to_texture虽然代码量不大,却把 wgpu 离屏渲染的完整链路浓缩在一个示例中:从纹理的 usage 声明,到渲染通道把颜色附件指向自定义纹理,再到copy_texture_to_buffer的 256 字节对齐细节,最后到map_async+ poll 的 CPU 回读模式。这套"渲染到纹理 → 回读 → 输出"的代码骨架,可以不加改动地移植到截图系统、后处理管线、贴图烘焙等真实需求中,也可以作为理解 wgpu 纹理与缓冲数据流的最佳入门教材。
【免费下载链接】wgpuA cross-platform, safe, pure-Rust graphics API.项目地址: https://gitcode.com/GitHub_Trending/wg/wgpu
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考