ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

wgpu 离屏渲染实战:render_to_texture 示例源码逐行拆解与图片回读方案

wgpu 离屏渲染实战:render_to_texture 示例源码逐行拆解与图片回读方案 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.tomlbin 入口为src/main.rs并以render_to_texture作为第一个命令行参数启动对应示例。main.rs中的get_example_name()会读取std::env::args().nth(1)原生环境来定位示例然后从EXAMPLES常量表中找到名字匹配的入口函数并调用。在原生环境输出图片默认写入当前目录下的please_dont_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 × 4RGBA 每像素 4 字节容量的Vecu8稍后用来存放从 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()——它选择了向下兼容的保守默认限制让示例在尽可能多的硬件上都能运行。另外由于没有 Surfacerequest_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], });逐字段理解size512×512×1一张 2D 纹理mip_level_count: 1、sample_count: 1单 mip、无 MSAA保持最简单dimension: wgpu::TextureDimension::D2二维纹理format: wgpu::TextureFormat::Rgba8UnormSrgbsRGB 格式与hello_triangle保持一致确保最终输出的颜色观感与窗口渲染一致usage是离屏渲染的关键RENDER_ATTACHMENT允许该纹理作为渲染通道Render Pass的颜色附件COPY_SRC允许它作为copy_texture_to_buffer的拷贝源。这两个 usage 组合正是渲染进去 读出来的完整语义view_formats声明了可用的视图格式这里与纹理格式相同。紧接着创建用于回读像素数据的暂存缓冲staging bufferlet 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的颜色格式必须与渲染目标纹理格式一致这里同样是Rgba8UnormSrgbbuffers: []没有顶点缓冲三角形的三个顶点完全由着色器内联给出。着色器位于 examples/features/src/render_to_texture/shader.wgsl与hello_triangle的着色器同构vertex fn vs_main(builtin(vertex_index) in_vertex_index: u32) - builtin(position) vec4f32 { var vertices arrayvec4f32, 3( vec4f32(0.0, 1.0, 0.0, 1.0), vec4f32(-1.0, -1.0, 0.0, 1.0), vec4f32(1.0, -1.0, 0.0, 1.0) ); return vertices[in_vertex_index]; } fragment fn fs_main() - location(0) vec4f32 { return vec4f32(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*42048 满足要求 // 因此无需手动补齐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 字节编码为 PNGencoder.set_color(png::ColorType::Rgba)然后写入命令行指定的路径默认是please_dont_git_push_me.pngWASM 环境output_image_wasm不生成文件而是把像素写入一个隐藏的canvasstaging canvas再通过canvas.to_data_url()生成 data URL赋值给页面上的img idoutput-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_texturestorage_texture着色器类型渲染管线vertex fragment计算管线compute写入纹理的机制Render Pass 颜色附件StorageTexture绑定逐像素写入纹理 usageRENDER_ATTACHMENT \| COPY_SRCSTORAGE_BINDING \| COPY_SRC格式Rgba8UnormSrgbRgba8Unorm图像内容绿底红色三角形Mandelbrot 集合灰度图从 storage_texture 源码 可以看到它通过dispatch_workgroups(512, 512, 1)让每个像素对应一个计算工作项直接写入绑定为存储纹理的纹理。而render_to_texture走的则是标准的图形渲染管线。二者的回读部分copy_texture_to_buffermap_async poll几乎完全一致——这正是渲染/计算到纹理 → 拷贝 → 回读 → 输出图片这一通用模板的两条具体实例。关键注意事项总结usage 必须声明齐全既要RENDER_ATTACHMENT让纹理可被渲染又要COPY_SRC让纹理可被拷贝缺一不可同理回读缓冲需要COPY_DST | MAP_READ。bytes_per_row的 256 字节对齐copy_texture_to_buffer对行字节数有硬性对齐要求示例因 512×42048 恰好满足而未做 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),仅供参考
返回列表