“渲染出来的立方体有透视,却没有深度。”这是不少人在 wgpu 里搭 3D 场景时撞上的第一堵墙。问题不在模型,也不在着色器写错了,而是缺了一套相机系统——没有视图矩阵和投影矩阵,顶点坐标会被直接送进 WebGPU 的归一化设备坐标(NDC)裁剪空间。
![]()
在 wgpu 里,NDC 是一个有边界的裁剪空间:X 和 Y 的范围是 [-1.0, 1.0],Z 的范围是 [0.0, 1.0]。没有透视矩阵做透视除法,场景就会渲染成一个扁平的 orthographic 盒子。超出这个范围的顶点会被立刻裁掉,物体也不会随着距离变远而变小。
要拿到真实的 3D 深度,得实现一个经典的针孔相机模型,核心是两次矩阵变换。
投影矩阵用 cgmath::perspective 构造一个视锥体,按距离缩放坐标,同时做宽高比校正,这样窗口拉伸时物体不会变形。视图矩阵用 cgmath::Matrix4::look_at_rh,以相机眼位置、目标朝向和“上”向量为参数,在右手坐标系下变换世界空间。
这里有个容易踩的坑:cgmath 是为 OpenGL 设计的,Z 范围是 -1.0 到 1.0。所以需要乘上 OPENGL_TO_WGPU_MATRIX,把 Z 映射到 0.0 到 1.0。最终相机矩阵的写法是:
let camera_matrix = OPENGL_TO_WGPU_MATRIX * proj * view;
矩阵算好了,还得送进 WGSL 着色器。这一步靠 wgpu 的 bind group 建立显式的布局契约。camera_controller.bind_group_layout 在数据进入着色器之前,就告诉 WebGPU 这份 uniform 数据的确切形状和内存可见性,对应着色器里的 @group(0) @binding(0) var camera: CameraUniform;。render_pipeline_layout 则把相机布局注册到 0 号槽位,让管线在绘制调用时知道要去 @group(0) 找相机数据。
绑定策略值得单独说一句。把 bind group 显式分层,引擎才有扩展空间。后面加材质、纹理、灯光时,只要往更高的 group 槽位挂,不用动相机这套设置:
- Group 0:相机 uniform,每帧或相机移动时更新
- Group 1:材质与纹理,按网格或材质更新
- Group 2:灯光数据,场景灯光变化时更新
相机矩阵每帧更新 uniform buffer 之后,就可以围绕任意已加载的 3D 网格自由环绕、平移和缩放了。
相机搞定,另一个拖慢迭代的问题浮出水面:每次改 WGSL 里的颜色或光照公式,都得重启应用。与其预编译着色器,不如用 notify 监听目标文件夹,只要 .wgsl 文件一保存,就在运行时重建 RenderPipeline。
底层机制分三步。第一步是操作系统级的异步追踪,用 notify::RecommendedWatcher 接入系统原生的文件通知接口,在后台监控文件变化,不阻塞渲染线程。第二步是主线程上的通道接收端,后台 watcher 通过线程安全的 mpsc 通道发送文件系统更新,接收端留在主线程待命。第三步是在重绘时触发重载:主事件循环进入 RedrawRequested 阶段时,用 watcher.rx.try_recv() 非阻塞地检查通道,一旦检测到文件修改事件,state.reload_shaders() 就从磁盘读取更新后的 .wgsl 文件,当场替换掉正在使用的 RenderPipeline。
改着色器不用关应用,迭代速度立刻不一样了。
特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.