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 }