git.lucas.co / cce-ui
GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git

src/draw/scene.rs (15.6K)

  1 //! The 3D scene a renderer draws beneath the UI, with no renderer in it: the
  2 //! mesh vertex ([`Vertex3D`]), a staged draw ([`SceneDraw`]) and image
  3 //! ([`SceneImage`]), the uniform block and image quads both renderers lay out
  4 //! the same way, and [`Stage3D`] — what an app stages a scene through, on the
  5 //! Vulkan renderer natively and the WebGPU one in a browser.
  6 //!
  7 //! The pass: meshes drawn with a depth buffer into a full-size BACKDROP
  8 //! image, scissored to a pane, which the renderer copies beneath the UI pass
  9 //! and the 2D shader's blur plates sample. A staged scene is drawn once; the
 10 //! backdrop it leaves is shown under every later frame until the next one.
 11 //! `scene3d.wgsl` / `scene3d_image.wgsl` (in `draw/`) are the shaders. A
 12 //! traced pane ([`super::rt`]) takes the raster scene's place in the same
 13 //! backdrop, staged through the same trait.
 14 
 15 /// Layout-identical to the app's `geometry::Vertex3D` (bytemuck-castable at cutover).
 16 /// Also the layout of an INSTANCE (`SceneDraw::instances`): an offset and a
 17 /// colour.
 18 #[repr(C)]
 19 #[derive(Debug, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
 20 pub struct Vertex3D {
 21     pub position: [f32; 3],
 22     pub color: [f32; 3],
 23 }
 24 
 25 /// A mesh a renderer holds, as [`Stage3D::create_mesh`] named it. Meaningful
 26 /// only to the renderer that made it.
 27 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
 28 pub struct MeshId(pub(crate) usize);
 29 
 30 /// One draw in the staged scene: a mesh under an mvp. The window-size/radius
 31 /// tail of shader_3d's uniform block is filled in by the renderer.
 32 pub struct SceneDraw {
 33     pub mesh: MeshId,
 34     pub mvp: [[f32; 4]; 4],
 35     /// Rasterize through the line pipeline (LINE_LIST topology): the mesh
 36     /// must be an EDGE mesh (vertex pairs), not the triangle fill mesh.
 37     pub wireframe: bool,
 38     /// rgb + mix: the fragment color is mixed toward `wire_tint.rgb` by
 39     /// `wire_tint[3]`. Zero = vertex colors untouched (the default draw).
 40     /// A wireframe pass overlaid on its own filled mesh needs this — the
 41     /// lines inherit the mesh's colors and would otherwise vanish into the
 42     /// identical fill beneath.
 43     pub wire_tint: [f32; 4],
 44     /// Whole-draw alpha multiplier (1.0 = opaque). The pass blends with
 45     /// straight alpha, so translucent draws show whatever rendered beneath.
 46     pub opacity: f32,
 47     /// Rasterized line width in framebuffer pixels for wireframe draws
 48     /// (ignored on fills). Clamped to the device's wideLines cap — 1.0
 49     /// everywhere when the feature is absent.
 50     pub line_width: f32,
 51     /// FILL draws only: the width of a wire pass that will ride on this
 52     /// fill (0 = none). The fill is pushed back by its own slope-scaled
 53     /// polygon offset sized to that width, so the coplanar wires win the
 54     /// depth test solidly: a w-px line samples the fill's plane up to
 55     /// (w/2 + 0.5) px off the true edge, and biasing the LINE can't cover
 56     /// that (its own depth slope is along-axis — near zero for
 57     /// contour-following wires) while the fill's slope is exactly the
 58     /// quantity needed.
 59     pub wire_base_width: f32,
 60     /// FILL draws only: the vertex colours already carry their lighting, so
 61     /// the fragment shader skips its derivative-normal flat shading and
 62     /// draws them as they are. A host that wants SMOOTH shading bakes it —
 63     /// the light is fixed in world space (`VkRenderer::set_scene_light`,
 64     /// which the host should light by), so lighting per vertex from
 65     /// interpolated normals is exact for a static light,
 66     /// and the vertex format needs no normal. False for an ordinary draw.
 67     pub prelit: bool,
 68     /// FILL draws only: draw through the SEE-THROUGH twin of the fill
 69     /// pipeline — no face culling and no depth writes (the depth TEST stays
 70     /// on, so what is drawn before the fill still hides it) — so a
 71     /// translucent mesh shows its own far side and everything behind it,
 72     /// wires included, since nothing it draws can occlude them. Blending is
 73     /// then order-dependent: the host should submit the triangles back to
 74     /// front for the current eye. False for an ordinary fill.
 75     pub see_through: bool,
 76     /// Draw `mesh` once per vertex of this mesh — INSTANCED. An instance is
 77     /// a [`Vertex3D`] read as where to put the mesh and what colour to give
 78     /// it: its position is added to every vertex of `mesh` and its colour
 79     /// multiplies theirs, so a white mesh takes each instance's colour. A
 80     /// thousand markers are then one small mesh and a thousand instances,
 81     /// where they were a thousand copies of the mesh's vertices uploaded
 82     /// whole whenever one moved. An instance mesh with no vertices draws
 83     /// nothing; `None` draws `mesh` once, as it is (the renderer binds one
 84     /// instance at the origin in white, which changes no vertex).
 85     pub instances: Option<MeshId>,
 86     /// The mesh is already in clip space: its vertices' x and y are NDC
 87     /// (-1..1, y up) and their z is ignored — each is drawn at the far plane
 88     /// (depth 0.9999), unlit, without the mvp. What a pane's background quad
 89     /// is: two triangles over (-1, -1)..(1, 1), drawn first, behind
 90     /// everything. False for an ordinary draw.
 91     ///
 92     /// This was an in-band signal until 2026-10-07: any vertex of ANY mesh
 93     /// within 0.01 of z = 9.99 was taken for a background corner, so real
 94     /// geometry spanning that plane in its own units (a 25 mm STL sphere in
 95     /// cce-model) had a ring of its points flung across the pane as spikes.
 96     pub screen_space: bool,
 97 }
 98 
 99 /// A user image standing in the 3D scene: a textured quad, unlit, depth
100 /// tested against the meshes and seen from both sides.
101 ///
102 /// Staged with `VkRenderer::stage_scene_images`, beside the scene's
103 /// `SceneDraw`s rather than as one of them: a mesh draw names a mesh and an
104 /// image draw names four corners and a texture, and the two share nothing but
105 /// the pass.
106 pub struct SceneImage {
107     /// Id from `upload_rgba`. A draw whose upload has not landed is skipped.
108     pub image: u32,
109     /// The quad's corners in the space `mvp` transforms, in the image's own
110     /// order: top-left, top-right, bottom-right, bottom-left.
111     pub corners: [[f32; 3]; 4],
112     pub mvp: [[f32; 4]; 4],
113     /// Whole-draw alpha multiplier over the image's own alpha.
114     pub opacity: f32,
115     /// Draw order: this image renders before the `SceneDraw` at this index
116     /// of the staged list, `u32::MAX` after them all. The pass blends in
117     /// submission order, so an image goes after the opaque things it may
118     /// show through to and before the translucent ones that may cover it.
119     pub before: u32,
120 }
121 
122 /// One corner of a `SceneImage` quad: `scene3d_image.wgsl`'s vertex.
123 #[repr(C)]
124 #[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
125 pub(crate) struct ImageVertex3D {
126     position: [f32; 3],
127     uv: [f32; 2],
128 }
129 
130 /// The instance a draw without instances is drawn with: at the origin, in
131 /// white, which leaves every vertex as it is (`x + 0.0` and `c * 1.0` are
132 /// exact).
133 pub(crate) const UNIT_INSTANCE: Vertex3D = Vertex3D { position: [0.0; 3], color: [1.0; 3] };
134 
135 /// `scene3d.wgsl`'s uniform block (`scene3d_image.wgsl` reads its head).
136 #[repr(C)]
137 #[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
138 pub(crate) struct SceneUniforms {
139     mvp: [[f32; 4]; 4],
140     window_size: [f32; 2],
141     window_radius: f32,
142     corner_shape: f32,
143     wire_tint: [f32; 4],
144     opacity: f32,
145     /// 1.0 on wireframe draws: the fragment shader skips the derivative-
146     /// normal flat shading, whose screen-space derivatives are degenerate on
147     /// line fragments (along-axis only) and light the wires with noise.
148     is_wire: f32,
149     /// 1.0 on `SceneDraw::prelit` draws: the flat shading is skipped too.
150     prelit: f32,
151     /// 1.0 on `SceneDraw::screen_space` draws: the vertex stage places the
152     /// vertex at its own xy in NDC, at the far plane, and skips the mvp.
153     screen_space: f32,
154     /// xyz: toward the light, world space, unit length; w unused.
155     light: [f32; 4],
156 }
157 
158 /// The direction toward the raster pass's light until a host sets one —
159 /// the light this pass always had, (-0.55, 0.45, 0.7) as the shader dotted
160 /// it with its INWARD derivative normal, said the right way round.
161 pub(crate) const DEFAULT_SCENE_LIGHT: [f32; 3] = [0.55, -0.45, -0.7];
162 
163 use super::rt::{PreparedRtScene, RtCamera, RtEnvironment, RtImage, RtMaterial, RtTriangle};
164 
165 /// What an app stages a 3D scene through: the renderer's half of the
166 /// pass, the same on every renderer (`vk::VkRenderer`, `web::WebRenderer`).
167 /// An app takes one in `Application::init_3d` (make its meshes) and
168 /// `Application::stage_3d` (stage this frame's scene).
169 pub trait Stage3D {
170     /// Upload a mesh: a triangle list for a fill draw, vertex PAIRS for a
171     /// wireframe one.
172     fn create_mesh(&mut self, verts: &[Vertex3D]) -> MeshId;
173     /// Replace a mesh's vertices (rare: a settings change, a rebuild).
174     fn update_mesh(&mut self, id: MeshId, verts: &[Vertex3D]);
175     /// Stage the next frame's scene: `draws` in order, scissored to
176     /// `scissor` (x, y, w, h, physical px).
177     fn stage_scene(&mut self, scissor: (u32, u32, u32, u32), draws: Vec<SceneDraw>);
178     /// Images standing in the staged scene (after [`stage_scene`](Self::stage_scene)).
179     fn stage_scene_images(&mut self, images: Vec<SceneImage>);
180     /// The direction TOWARD the flat shading's light, in world space; set
181     /// it once, and light a `prelit` mesh by the same vector.
182     fn set_scene_light(&mut self, toward: [f32; 3]);
183 
184     /// Replace the path tracer's scene (triangles in the space the camera's
185     /// `inv_mvp` unprojects into); the BVH is built on the CPU, here, on the
186     /// UI thread — seconds for millions of triangles, during which the
187     /// window is frozen. Fine for small scenes; a large one is built as a
188     /// [`PreparedRtScene`] on a worker and handed to
189     /// [`set_rt_scene_prepared`](Self::set_rt_scene_prepared). Rare: a
190     /// geometry rebuild. Restarts the accumulation.
191     fn set_rt_scene(&mut self, triangles: &[RtTriangle], materials: &[RtMaterial]) {
192         self.set_rt_scene_with_image(triangles, materials, None);
193     }
194     /// [`set_rt_scene`](Self::set_rt_scene) with an uploaded image standing
195     /// in the scene (the picture the raster pass draws as a `SceneImage`).
196     fn set_rt_scene_with_image(&mut self, triangles: &[RtTriangle], materials: &[RtMaterial], image: Option<RtImage>) {
197         let prepared = PreparedRtScene::new(triangles.to_vec(), materials, image, self.rt_needs_bvh());
198         self.set_rt_scene_prepared(&prepared);
199     }
200     /// Replace the path tracer's scene with one whose CPU work — packing,
201     /// and the BVH when [`rt_needs_bvh`](Self::rt_needs_bvh) — was done
202     /// ahead, on any thread: this only uploads it. Restarts the
203     /// accumulation. The scene is not consumed; keep it to upload again
204     /// after a reconnect (`Application::init_3d` runs again then).
205     fn set_rt_scene_prepared(&mut self, scene: &PreparedRtScene);
206     /// Whether this renderer's tracer traverses a CPU-built BVH — the
207     /// `with_bvh` to prepare its scenes with. False on the Vulkan
208     /// ray-query tier, which builds its own structure on the GPU from the
209     /// triangles; a scene prepared with a BVH still traces there (the BVH is
210     /// ignored), and one prepared without traces anywhere (the upload builds
211     /// it), so a wrong answer only costs time.
212     fn rt_needs_bvh(&self) -> bool {
213         true
214     }
215     /// The traced scene's sky and sun. A change restarts the accumulation.
216     fn set_rt_environment(&mut self, environment: RtEnvironment);
217     /// What a camera ray that meets nothing shows (linear RGB), or `None`
218     /// for the sky. A change restarts the accumulation.
219     fn set_rt_background(&mut self, color: Option<[f32; 3]>);
220     /// Stage one progressive pass into `pane` (physical px) for the next
221     /// frame, in place of the raster scene there. Call it every frame while
222     /// tracing: each adds a sample; a camera, pane or scene change restarts.
223     fn stage_rt(&mut self, pane: (u32, u32, u32, u32), camera: RtCamera);
224     /// True while another staged frame would still refine the traced image
225     /// — the app's cue to keep asking for frames.
226     fn rt_accumulating(&self) -> bool;
227 
228     /// The lit, textured mesh path (`draw::lit`), when this renderer has
229     /// it: `None` by default, so a renderer without it needs no change and
230     /// a host keeps its flat path. The Vulkan renderer answers `Some`.
231     fn lit(&mut self) -> Option<&mut dyn super::lit::LitStage3D> {
232         None
233     }
234 }
235 
236 /// A staged scene's uniform blocks, in the order its draws use them: one per
237 /// mesh draw, then one per image.
238 pub(crate) fn scene_uniforms(
239     draws: &[SceneDraw],
240     images: &[SceneImage],
241     window_size: [f32; 2],
242     corner_radius_px: f32,
243     light: [f32; 3],
244 ) -> Vec<SceneUniforms> {
245     let corner_shape = crate::layout::corner_shape();
246     let light = [light[0], light[1], light[2], 0.0];
247     let block = |mvp, wire_tint, opacity, is_wire, prelit, screen_space| SceneUniforms {
248         mvp,
249         window_size,
250         window_radius: corner_radius_px,
251         corner_shape,
252         wire_tint,
253         opacity,
254         is_wire,
255         prelit,
256         screen_space,
257         light,
258     };
259     let flag = |b: bool| if b { 1.0 } else { 0.0 };
260     let mut out = Vec::with_capacity(draws.len() + images.len());
261     for d in draws {
262         out.push(block(d.mvp, d.wire_tint, d.opacity, flag(d.wireframe), flag(d.prelit), flag(d.screen_space)));
263     }
264     for i in images {
265         out.push(block(i.mvp, [0.0; 4], i.opacity, 0.0, 1.0, 0.0));
266     }
267     out
268 }
269 
270 /// Six vertices per image, two triangles over its corners in staged order.
271 pub(crate) fn image_quads_3d(images: &[SceneImage]) -> Vec<ImageVertex3D> {
272     let mut verts = Vec::with_capacity(images.len() * 6);
273     for image in images {
274         let [tl, tr, br, bl] = image.corners;
275         let v = |position: [f32; 3], uv: [f32; 2]| ImageVertex3D { position, uv };
276         verts.extend([
277             v(tl, [0.0, 0.0]),
278             v(bl, [0.0, 1.0]),
279             v(tr, [1.0, 0.0]),
280             v(tr, [1.0, 0.0]),
281             v(bl, [0.0, 1.0]),
282             v(br, [1.0, 1.0]),
283         ]);
284     }
285     verts
286 }
287 
288 /// A fill carrying a wire overlay is pushed back by this slope-scaled depth
289 /// bias for wires `w` px wide (constant, slope): the slope term covers the
290 /// wires' across-width sampling offset (w/2 px) and the NEIGHBOR facet's
291 /// plane — a wire lies on edge A|B and its fragments carry A's plane depth,
292 /// while the fill under the far half of the wire is B's plane, which on a
293 /// convex surface tilts closer — hence the extra pixel of headroom.
294 pub(crate) fn wire_base_bias(w: f32) -> (f32, f32) {
295     (2.0, 1.5 + w)
296 }
297 
298 #[cfg(test)]
299 mod tests {
300     use super::*;
301 
302     fn draw(mesh: usize, screen_space: bool) -> SceneDraw {
303         SceneDraw {
304             mesh: MeshId(mesh),
305             mvp: [[0.0; 4]; 4],
306             wireframe: false,
307             wire_tint: [0.0; 4],
308             opacity: 1.0,
309             line_width: 1.0,
310             wire_base_width: 0.0,
311             prelit: false,
312             see_through: false,
313             instances: None,
314             screen_space,
315         }
316     }
317 
318     /// Only the draw that asks to be screen-space is: the shader reads the
319     /// flag per draw, never off the vertex data (the old z = 9.99 sentinel).
320     #[test]
321     fn screen_space_is_set_per_draw() {
322         let blocks = scene_uniforms(&[draw(0, true), draw(1, false)], &[], [100.0, 100.0], 0.0, DEFAULT_SCENE_LIGHT);
323         assert_eq!(blocks[0].screen_space, 1.0);
324         assert_eq!(blocks[1].screen_space, 0.0);
325         let shader = include_str!("scene3d.wgsl");
326         assert!(shader.contains("uniforms.screen_space"));
327         assert!(!shader.contains("position.z -"), "no vertex-data sentinel");
328     }
329 }