跳到正文
DEV Community· J·· 3 小時前AI 評分34

建構可重用的 WebGPU 與 Rust 引擎:第 3 部分——實作 3D 相機系統及著色器熱重載

Building a Reusable WebGPU & Rust Engine: Part 3 — Implementing a 3D Camera System & Hot-Reloading Shaders

AI 導讀

WebGPU 與 Rust 可重用引擎系列第 3 部分實作針孔 3D 相機系統,以投影矩陣和視圖矩陣為網格加入透視效果。相機矩陣透過 wgpu bind groups 傳入 WGSL。文章亦示範以 notify 監察 .wgsl 檔案,檔案變更時在執行期間重建 RenderPipeline,毋須重啟應用程式即可重新載入著色器。

正文

It’s been a while since my last update! I received some great feedback and architectural suggestions, especially around asset loading and pipeline optimization. I plan to integrate those ideas step by step.

Before jumping into advanced features like shader hot-reloading or multi-threaded asset streaming, we need to solve a fundamental problem: moving from flat 2D rendering to a full 3D viewport. Today, we are focusing on setting up a Pinhole 3D Camera System.

1. Why Do We Need a Camera? (Normalized Device Coordinates)

If you render a mesh in wgpu without a Camera View or Projection matrix uniform in your WGSL shader, your vertex positions are passed directly into WebGPU's Normalized Device Coordinates (NDC) clip space.

In wgpu, NDC is a bounded clip space where:
X and Y range from [-1.0, 1.0]
Z ranges from [0.0, 1.0]

Without a perspective matrix to perform perspective division (scaling geometry based on its distance along the Z-axis), your scene renders as a flat, orthographic box. Vertices outside this [-1.0, 1.0] range get clipped instantly, and objects don't get smaller as they move further away.


2. Building a Standard Pinhole Camera

To achieve realistic 3D depth, we implement a classic pinhole camera model using two core matrix transformations:

  1. Projection Matrix (cgmath::perspective): Constructs a Field-of-View (FOV) frustum that scales coordinates based on distance and applies aspect ratio correction so objects don't stretch when resizing the window.

  2. View Matrix (cgmath::Matrix4::look_at_rh): Transforms world space relative to the camera's eye position, target orientation, and "up" vector using right-handed coordinate space (_rh).

note: cgmath was designed for OpenGL (-1.0 to 1.0 Z-range). I multiply by OPENGL_TO_WGPU_MATRIX to map Z to (0.0 to 1.0).
let camera_matrix = OPENGL_TO_WGPU_MATRIX * proj * view;


3. Passing Camera Uniforms to WGSL

To get our camera matrix into the shader, we set up an explicit layout contract using wgpu bind groups:

  • camera_controller.bind_group_layout : Tells WebGPU the exact shape and memory visibility of the uniform data before it enters the shader (@group(0) @binding(0) var<uniform> camera: CameraUniform;).

  • render_pipeline_layout: Registers the camera layout into slot 0 so the pipeline expects camera data in @group(0) during draw calls.

Modular Binding Strategy
Structuring your bind groups explicitly keeps your engine scalable. When you add materials, textures, or lights later, you simply bind them to higher group slots without breaking your camera setup:

// Group 0: Camera Uniforms (Updated once per frame or camera move)
@group(0) @binding(0) var<uniform> camera: CameraUniform;

// Group 1: Material & Textures (Updated per mesh/material)
@group(1) @binding(0) var t_diffuse: texture_2d<f32>;
@group(1) @binding(1) var s_diffuse: sampler;

// Group 2: Lighting Data (Updated when scene lights change)
@group(2) @binding(0) var<uniform> light: LightUniform;

What’s Next?
With the camera matrix updating uniform buffers every frame, we can now freely orbit, pan, and zoom around any loaded 3D mesh. I will leave the details of those features for later.


Here we get a slightly interesting image, cube with perspective change but no depth.


Bonus: Shader Hot-Reloading

Restarting the application every time you tweak a color or lighting equation in WGSL slows down iteration fast.

Instead of pre-compiling shaders, we can use notify to watch the -/any_name/ folder and rebuild the RenderPipeline at runtime whenever a .wgsl file is saved.

How It Works Under the Hood:

  1. OS-Level Asynchronous Tracking: I use notify::RecommendedWatcher to hook into the operating system's native file-notification API. This monitors file changes in the background without blocking the rendering thread.

  2. Main-Thread Channel Receiver (Receiver<Result<Event>>): The background watcher sends file-system updates through a thread-safe mpsc channel. The receiver side stays available on the main thread.

  3. Triggering the Reload on Redraw: During the RedrawRequested step inside our main event loop, we non-blockingly check the channel using watcher.rx.try_recv(). If a file-modification event is detected, state.reload_shaders() reads the updated .wgsl file from disk and replaces the active RenderPipeline on the fly.


and we can change the shader without close the application!


Thanks for reading! If you're working on similar systems, check out my personal projects:
Building industrial 3D vision or AI pipelines? |
Check out

— designed to help benchmark, test, and validate your detection networks against challenging optical geometry.

來源:DEV Community · dev.to