Iced 3D 渲染教程:用 1 个自定义着色器渲染 500 个立方体
【免费下载链接】icedA cross-platform GUI library for Rust, inspired by Elm项目地址: https://gitcode.com/GitHub_Trending/ic/iced
Iced 是一个受 Elm 启发的跨平台 Rust GUI 库。想在 Rust GUI 里做 3D 渲染,Iced 给出了完整通路:wgpu 后端(覆盖 Vulkan、Metal、DX12、OpenGL、WebGPU)、Shader widget,以及一个开箱即用的完整示例——custom_shader 里的 IcedCubes,用一份 WGSL 着色器渲染 500 个旋转的冰块。下面按数据流动的顺序拆解这条 3D 渲染链路。
先跑通 IcedCubes 示例:GUI 里的 3D 最短路径
结论先行:不用写任何代码,clone 下来直接跑。
git clone https://gitcode.com/GitHub_Trending/ic/iced cd iced cargo run --example custom_shader窗口里是雾效场景中的 500 个随机旋转冰块,带天穹盒的反射与折射;下方控制面板有立方体数量(1–500)、尺寸、灯光 RGB 的滑杆,以及一个 "Show Depth Buffer" 复选框。
两点注意:iced 的默认 features 包含wgpu(见 Cargo.toml 第 25–27 行),所以示例在普通开发机上开箱即跑;示例是 workspace 成员(examples/*),cargo 能直接定位到它。
两套矩阵:2D 的 Transformation 与 3D 的视口投影矩阵
Iced 的矩阵体系分工明确:2D 走自己的抽象层,3D 直接用 glam。
2D 部分在 core/src/transformation.rs 中。Transformation是glam::Mat4的薄封装,提供orthographic、translate、scale、inverse,并支持*组合:
// 2D 变换的组合方式(core/src/transformation.rs) let t = Transformation::translate(100.0, 0.0) * Transformation::scale(0.8); let p = Point::new(0.0, 0.0) * t; // Point/Vector/Size/Rectangle 均可直接 *布局、缩放、悬停动效都建立在这一层上,它还提供了scale_factor()和translation()反解,供命中测试与布局回写使用。
3D 矩阵不走这个类型。示例里的相机自己构建透视投影:
// examples/custom_shader/src/scene/camera.rs 的逻辑 let view = look_at_mat4(eye, target, up); let proj = directx::perspective(fov_y, aspect, near, far); let mvp = OPENGL_TO_WGPU_MATRIX * proj * view;视场角 45°,near/far 取 0.1/100,aspect 由 widget 的实际 bounds 计算——窗口尺寸一变投影就跟着变,这是很多手写 3D 场景会漏掉的细节。
原生平台的分层架构中,窗口与事件循环由 winit 层负责,渲染交给 wgpu 后端,2D 控件与 3D 内容在同一帧合成。
Shader widget:把 wgpu 管线嵌进 widget 树
关键机制在 widget/src/shader.rs:Shader<Message, P>本身就是一个普通Widget,你只需为你的场景实现shader::Programtrait:
impl shader::Program<Message> for Scene { type State = (); type Primitive = Primitive; fn draw(&self, _state, _cursor, bounds) -> Self::Primitive { Primitive::new(&self.cubes, &self.camera, bounds, self.show_depth_buffer, self.light_color) } }Scene持有立方体数组、相机和灯光颜色,draw把它们打包成Primitive(顶点缓冲 + 实例化缓冲 + uniform 缓冲 + 深度纹理)。在 view 里它就一行:shader(&self.scene).width(Fill).height(Fill),参与布局、事件与焦点的方式和 slider 没有区别。
性能上,500 个立方体共享一份 cube 几何,每个实例通过顶点属性传 Mat4 变换矩阵和 Mat3 法线矩阵(step_mode: Instance),整个场景一次 draw call 画完。
读懂 cubes.wgsl:从法线贴图到冰块质感的 3 步
着色器在 examples/custom_shader/src/shaders/ 下,主文件是 cubes.wgsl,要点有三:
- 切线空间:顶点阶段用 tangent/normal/binormal 构建 TBN 矩阵,把世界坐标、相机位置、光源位置全部转到切线空间,这样片元阶段才能直接用同一坐标系采样法线贴图
- 光照:漫反射是
max(dot(normal, dir_to_light), 0.0) * light_color;镜面高光取反射向量与视线夹角;所有法线都来自贴图采样而非几何法线 - 冰块感:反射方向与折射方向(折射率 1.31,接近冰的数值)各采样天穹盒 cubemap 后五五混合,再与基础色按 0.8 权重混合;最后叠加指数衰减雾效,远处物体渐白
法线贴图是 1024×1024 的 RGBA 纹理,运行时解码后上传为 GPU 纹理。天穹盒则是 128×128 的六面 cubemap。"Show Depth Buffer" 复选框对应第二个着色器 depth.wgsl,把深度纹理可视化出来,方便确认遮挡关系是否正确。
交互接线:滑杆、订阅与逐帧更新
交互完全走 Elm 风格:控件发出Message,update修改场景,3D 数据与 GUI 代码完全分离。动画由一个订阅驱动:
fn subscription(&self) -> Subscription<Message> { window::frames().map(Message::Tick) }每帧Tick带来时间戳,Scene::update按时间推进每个立方体的四元数角度(旋转轴和方向在创建时随机分配)。灯光颜色经 RGB 三个滑杆修改后写入Uniforms缓冲(camera_proj、camera_pos、light_color三个字段,见 examples/custom_shader/src/scene/pipeline/uniforms.rs)。GUI 与 3D 共享同一事件循环,不需要自己处理双线程同步。
要注意的代价:wgpu 后端的三个限制
- 只支持 wgpu 后端。
Shader的约束是Renderer: primitive::Renderer,tiny-skia 软件渲染后端不支持自定义着色器;headless 或 CI 环境里 3D 内容跑不起来。 - 需要 GPU 与 WGSL 编译能力,且着色器特性要自己核对,跨 GPU 行为可能有差异。
- 坐标约定容易踩坑。WGPU 的 clip space 是 z ∈ [0,1],与 OpenGL 不同,示例用
OPENGL_TO_WGPU_MATRIX做转换;照抄代码时如果把投影改成 OpenGL 风格却忘了这个矩阵,会直接得到黑屏。
排查问题可以启用debugfeature 后按 F12 查看帧指标,或用testerfeature 录制回放操作序列。
下一步
- 通读 examples/custom_shader/:main.rs → scene.rs → scene/pipeline/,就是一条完整的 wgpu 管线
- 把 scene.rs 顶部的
MAX常量(500)调大或调小,观察实例缓冲与帧率的变化 - 需要纯 2D 图形效果时,翻 examples/ 里的 canvas、the_matrix 等示例
- 想做基准测试,仓库自带 benches/ 下的 wgpu 与 cpu 两组 criterion 测试
【免费下载链接】icedA cross-platform GUI library for Rust, inspired by Elm项目地址: https://gitcode.com/GitHub_Trending/ic/iced
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考