GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
examples/probe3d/scene.rs (15.3K)
1 //! The 3D probe's scene: one app whose frame is a 3D pane under a strip of
2 //! UI, staged through the portable `Application::init_3d` / `stage_3d`, so
3 //! the same code runs on the Vulkan renderer (`probe3d_native`) and the
4 //! WebGPU one (`probe3d_web`) and the two can be compared pixel for pixel.
5 //!
6 //! It reaches every path the scene pass has: the screen-space background
7 //! quad (`SceneDraw::screen_space`), flat-shaded fills (the derivative normal),
8 //! a prelit fill, a fill carrying a wire overlay (the depth-biased fill and
9 //! the line pipeline, wires tinted), a see-through translucent fill and the
10 //! wires riding it, an image standing in the scene before the translucent
11 //! draw, an INSTANCED draw (one white cube drawn for a row of coloured
12 //! instances), a light the host sets, and a frosted plate over the pane whose
13 //! blur samples the backdrop the scene left. And one regression: a small
14 //! sphere modelled in its own units around z = 9.99 and scaled into place,
15 //! which the old in-band background sentinel (any vertex within 0.01 of
16 //! z = 9.99) tore into spikes across the pane.
17
18 use cce_ui::engine::{
19 AppSender, Application, LogicalPosition, LogicalSize, MeshId, RtCamera, RtImage, RtMaterial, RtTriangle, SceneDraw,
20 SceneImage, Stage3D, Vertex3D, WindowSettings,
21 };
22 use cce_ui::scene::layout::Rect;
23 use cce_ui::scene::paint::{DisplayList, PaintCtx, PlateSpec};
24 use cce_ui::scene::{Frost, Material};
25 use cce_ui::widget::{ElementState, KeyEvent, MouseButton, MouseScrollDelta};
26 use glam::{Mat4, Vec3};
27
28 pub const W: u32 = 1280;
29 pub const H: u32 = 800;
30 /// The UI strip's width; the 3D pane is the rest of the window.
31 const STRIP: f32 = 300.0;
32
33 /// The traced probe's sample count: frames staged before it stops.
34 pub const TRACE_FRAMES: u32 = 8;
35
36 pub struct Probe3d<const TRACE: bool> {
37 image: u32,
38 meshes: Option<Meshes>,
39 /// Traced frames staged so far.
40 traced: u32,
41 }
42
43 pub type Raster = Probe3d<false>;
44 pub type Traced = Probe3d<true>;
45
46 /// The image's corners in the scene, as both passes place it.
47 const IMAGE_CORNERS: [[f32; 3]; 4] = [[-2.6, 1.6, -1.6], [-0.6, 1.6, -1.6], [-0.6, 0.35, -1.6], [-2.6, 0.35, -1.6]];
48
49 /// The far-z sphere, in its own units: centred ON z = 9.99, so its meridians
50 /// at longitude 0 and 180 degrees sit exactly on that plane.
51 const FAR_Z_CENTER: Vec3 = Vec3::new(0.0, 0.0, 9.99);
52 const FAR_Z_RADIUS: f32 = 4.0;
53
54 /// Where the far-z sphere's units land in the scene: a tenth of their size,
55 /// its centre at (-0.3, -0.3, 1.1), so it reads as a 0.4-radius ball in front
56 /// of the big sphere.
57 fn far_z_model() -> Mat4 {
58 Mat4::from_translation(Vec3::new(-0.3, -0.3, 1.1)) * Mat4::from_scale(Vec3::splat(0.1)) * Mat4::from_translation(-FAR_Z_CENTER)
59 }
60
61 struct Meshes {
62 background: MeshId,
63 cube: MeshId,
64 prelit: MeshId,
65 sphere: MeshId,
66 sphere_wires: MeshId,
67 glass: MeshId,
68 glass_wires: MeshId,
69 /// A small white cube, and the row of instances it is drawn for.
70 marker: MeshId,
71 marker_instances: MeshId,
72 /// The sphere spanning z = 9.99 in its own units (`far_z_model`).
73 far_z: MeshId,
74 }
75
76 fn r(x: f32, y: f32, w: f32, h: f32) -> Rect {
77 Rect { x, y, width: w, height: h }
78 }
79
80 fn v(p: Vec3, c: [f32; 3]) -> Vertex3D {
81 Vertex3D { position: p.to_array(), color: c }
82 }
83
84 /// A box's triangles, counter-clockwise seen from outside, a colour a face.
85 fn cuboid(center: Vec3, half: Vec3, colors: [[f32; 3]; 6]) -> Vec<Vertex3D> {
86 let c = |x: f32, y: f32, z: f32| center + half * Vec3::new(x, y, z);
87 let faces = [
88 [c(1., -1., -1.), c(1., 1., -1.), c(1., 1., 1.), c(1., -1., 1.)], // +x
89 [c(-1., -1., 1.), c(-1., 1., 1.), c(-1., 1., -1.), c(-1., -1., -1.)], // -x
90 [c(-1., 1., -1.), c(-1., 1., 1.), c(1., 1., 1.), c(1., 1., -1.)], // +y
91 [c(-1., -1., 1.), c(-1., -1., -1.), c(1., -1., -1.), c(1., -1., 1.)], // -y
92 [c(-1., -1., 1.), c(1., -1., 1.), c(1., 1., 1.), c(-1., 1., 1.)], // +z
93 [c(1., -1., -1.), c(-1., -1., -1.), c(-1., 1., -1.), c(1., 1., -1.)], // -z
94 ];
95 let mut out = Vec::new();
96 for (f, q) in faces.iter().enumerate() {
97 for i in [0, 1, 2, 0, 2, 3] {
98 out.push(v(q[i], colors[f]));
99 }
100 }
101 out
102 }
103
104 /// A box's twelve edges as vertex pairs.
105 fn cuboid_edges(center: Vec3, half: Vec3, color: [f32; 3]) -> Vec<Vertex3D> {
106 let mut out = Vec::new();
107 for a in 0..8u32 {
108 for b in a + 1..8 {
109 if (a ^ b).count_ones() == 1 {
110 for k in [a, b] {
111 let s = |bit: u32| if k & bit != 0 { 1.0 } else { -1.0 };
112 out.push(v(center + half * Vec3::new(s(1), s(2), s(4)), color));
113 }
114 }
115 }
116 }
117 out
118 }
119
120 /// A UV sphere's triangles (outward, counter-clockwise) and its latitude and
121 /// longitude lines as vertex pairs.
122 fn sphere(center: Vec3, radius: f32, stacks: u32, slices: u32) -> (Vec<Vertex3D>, Vec<Vertex3D>) {
123 let p = |i: u32, j: u32| {
124 let th = std::f32::consts::PI * i as f32 / stacks as f32;
125 let ph = std::f32::consts::TAU * j as f32 / slices as f32;
126 center + radius * Vec3::new(th.sin() * ph.cos(), th.cos(), th.sin() * ph.sin())
127 };
128 let color = |q: Vec3| {
129 let n = (q - center) / radius;
130 [0.35 + 0.3 * n.x, 0.45 + 0.25 * n.y, 0.65 + 0.2 * n.z]
131 };
132 let mut tris = Vec::new();
133 let mut lines = Vec::new();
134 for i in 0..stacks {
135 for j in 0..slices {
136 let (a, b, c, d) = (p(i, j), p(i + 1, j), p(i + 1, j + 1), p(i, j + 1));
137 for q in [a, c, b, a, d, c] {
138 tris.push(v(q, color(q)));
139 }
140 for q in [a, b, a, d] {
141 lines.push(v(q, color(q)));
142 }
143 }
144 }
145 (tris, lines)
146 }
147
148 /// The traced scene: the raster meshes' triangles, a material each.
149 fn traced_scene() -> (Vec<RtTriangle>, Vec<RtMaterial>) {
150 let mut tris = Vec::new();
151 let mut mats = Vec::new();
152 let mut add = |verts: Vec<Vertex3D>, albedo: [f32; 3], emission: [f32; 3]| {
153 let m = mats.len() as u32;
154 mats.push(RtMaterial { albedo, emission });
155 for t in verts.as_chunks::<3>().0 {
156 tris.push(RtTriangle { p0: t[0].position, p1: t[1].position, p2: t[2].position, material: m });
157 }
158 };
159 add(cuboid(Vec3::new(0.0, -0.75, 0.0), Vec3::new(3.0, 0.05, 2.4), [[0.3; 3]; 6]), [0.55, 0.57, 0.55], [0.0; 3]);
160 add(cuboid(Vec3::new(-1.6, 0.0, 0.2), Vec3::splat(0.6), [[0.0; 3]; 6]), [0.8, 0.35, 0.25], [0.0; 3]);
161 add(sphere(Vec3::new(0.4, 0.3, -0.4), 0.9, 12, 20).0, [0.45, 0.55, 0.75], [0.0; 3]);
162 add(cuboid(Vec3::new(1.5, 0.1, 1.2), Vec3::splat(0.55), [[0.0; 3]; 6]), [0.3, 0.7, 0.9], [0.4, 0.9, 1.1]);
163 let model = far_z_model();
164 let far_z = sphere(FAR_Z_CENTER, FAR_Z_RADIUS, 10, 16).0.into_iter().map(|p| v(model.transform_point3(Vec3::from(p.position)), p.color)).collect();
165 add(far_z, [0.9, 0.75, 0.3], [0.0; 3]);
166 (tris, mats)
167 }
168
169 impl<const TRACE: bool> Probe3d<TRACE> {
170 fn camera(size: LogicalSize, scale: f64) -> (Mat4, (u32, u32, u32, u32)) {
171 let s = scale as f32;
172 let (pw, ph) = ((size.width - STRIP) * s, size.height * s);
173 let proj = Mat4::perspective_rh(40f32.to_radians(), pw / ph, 0.1, 100.0);
174 let view = Mat4::look_at_rh(Vec3::new(3.2, 2.4, 5.6), Vec3::new(0.0, 0.2, 0.0), Vec3::Y);
175 // The scissor is the pane; the projection is of the pane, offset into
176 // it by shifting NDC x (the pass draws in window space).
177 let ndc_w = pw / (size.width * s);
178 let shift = Mat4::from_translation(Vec3::new(1.0 - ndc_w, 0.0, 0.0)) * Mat4::from_scale(Vec3::new(ndc_w, 1.0, 1.0));
179 (shift * proj * view, ((STRIP * s) as u32, 0, pw as u32, ph as u32))
180 }
181
182 /// The traced pane's camera: the pane's own projection, unshifted —
183 /// the tracer's image is the pane.
184 fn trace_camera(size: LogicalSize, scale: f64) -> RtCamera {
185 let s = scale as f32;
186 let (pw, ph) = ((size.width - STRIP) * s, size.height * s);
187 let proj = Mat4::perspective_rh(40f32.to_radians(), pw / ph, 0.1, 100.0);
188 let view = Mat4::look_at_rh(Vec3::new(3.2, 2.4, 5.6), Vec3::new(0.0, 0.2, 0.0), Vec3::Y);
189 RtCamera { inv_mvp: (proj * view).inverse().to_cols_array_2d() }
190 }
191 }
192
193 impl<const TRACE: bool> Application for Probe3d<TRACE> {
194 type Message = ();
195
196 fn create(_sender: AppSender<()>) -> Self {
197 let (iw, ih) = (64u32, 40u32);
198 let mut px = Vec::with_capacity((iw * ih * 4) as usize);
199 for y in 0..ih {
200 for x in 0..iw {
201 let a = if (x / 8 + y / 8) % 2 == 0 { 255 } else { 120 };
202 px.extend([(x * 255 / (iw - 1)) as u8, (y * 255 / (ih - 1)) as u8, 200, a]);
203 }
204 }
205 Probe3d { image: cce_ui::draw::upload_rgba(px, iw, ih), meshes: None, traced: 0 }
206 }
207
208 fn settings(&self) -> WindowSettings {
209 WindowSettings { title: "probe3d".into(), app_id: "cce-probe3d".into(), width: W, height: H, fullscreen: false, min_size: None }
210 }
211
212 fn load_system_fonts(&self) -> bool {
213 true
214 }
215
216 fn display_list_text(&self) -> bool {
217 true
218 }
219
220 fn init_3d(&mut self, stage: &mut dyn Stage3D) {
221 // NDC corners: the draw is `screen_space`, so z is ignored.
222 let bg = |x: f32, y: f32, c: [f32; 3]| Vertex3D { position: [x, y, 0.0], color: c };
223 let (top, bottom) = ([0.10, 0.12, 0.20], [0.30, 0.26, 0.22]);
224 let background = stage.create_mesh(&[
225 bg(-1.0, -1.0, bottom),
226 bg(1.0, -1.0, bottom),
227 bg(1.0, 1.0, top),
228 bg(-1.0, -1.0, bottom),
229 bg(1.0, 1.0, top),
230 bg(-1.0, 1.0, top),
231 ]);
232 let shades = [[0.8, 0.3, 0.2], [0.7, 0.35, 0.25], [0.9, 0.5, 0.3], [0.5, 0.2, 0.15], [0.85, 0.4, 0.3], [0.6, 0.25, 0.2]];
233 let cube = stage.create_mesh(&cuboid(Vec3::new(-1.6, 0.0, 0.2), Vec3::splat(0.6), shades));
234 let floor = [[0.30, 0.32, 0.30]; 6];
235 let mut prelit_verts = cuboid(Vec3::new(0.0, -0.75, 0.0), Vec3::new(3.0, 0.05, 2.4), floor);
236 for (i, p) in prelit_verts.iter_mut().enumerate() {
237 let k = 0.7 + 0.3 * ((i / 6) as f32 / 5.0);
238 p.color = [p.color[0] * k, p.color[1] * k, p.color[2] * k];
239 }
240 let prelit = stage.create_mesh(&prelit_verts);
241 let mut far_z_verts = sphere(FAR_Z_CENTER, FAR_Z_RADIUS, 10, 16).0;
242 for p in &mut far_z_verts {
243 p.color = [0.9, 0.75, 0.3];
244 }
245 let far_z = stage.create_mesh(&far_z_verts);
246 let (tris, lines) = sphere(Vec3::new(0.4, 0.3, -0.4), 0.9, 12, 20);
247 let sphere = stage.create_mesh(&tris);
248 let sphere_wires = stage.create_mesh(&lines);
249 let glass_c = Vec3::new(1.5, 0.1, 1.2);
250 let glass = stage.create_mesh(&cuboid(glass_c, Vec3::splat(0.55), [[0.3, 0.7, 0.9]; 6]));
251 let glass_wires = stage.create_mesh(&cuboid_edges(glass_c, Vec3::splat(0.55), [0.9, 0.95, 1.0]));
252 let marker = stage.create_mesh(&cuboid(Vec3::ZERO, Vec3::splat(0.12), [[1.0; 3]; 6]));
253 let row: Vec<Vertex3D> = (0..8)
254 .map(|i| {
255 let t = i as f32 / 7.0;
256 v(Vec3::new(-2.4 + 0.55 * i as f32, -0.58, 1.9), [0.9 - 0.6 * t, 0.4 + 0.4 * t, 0.3 + 0.6 * t])
257 })
258 .collect();
259 let marker_instances = stage.create_mesh(&row);
260 stage.set_scene_light([0.6, 0.7, 0.4]);
261 self.meshes = Some(Meshes { background, cube, prelit, sphere, sphere_wires, glass, glass_wires, marker, marker_instances, far_z });
262 if TRACE {
263 let (tris, mats) = traced_scene();
264 stage.set_rt_scene_with_image(&tris, &mats, Some(RtImage { image: self.image, corners: IMAGE_CORNERS, opacity: 0.9 }));
265 }
266 }
267
268 fn stage_3d(&mut self, stage: &mut dyn Stage3D, size: LogicalSize, scale: f64) -> bool {
269 if TRACE {
270 // A sample a frame for TRACE_FRAMES frames, then the backdrop
271 // keeps the result.
272 if self.traced >= TRACE_FRAMES {
273 return false;
274 }
275 let (_, pane) = Self::camera(size, scale);
276 stage.stage_rt(pane, Self::trace_camera(size, scale));
277 self.traced += 1;
278 return self.traced < TRACE_FRAMES;
279 }
280 let Some(m) = &self.meshes else { return false };
281 let (mvp, scissor) = Self::camera(size, scale);
282 let mvp = mvp.to_cols_array_2d();
283 let draw = |mesh| SceneDraw {
284 mesh,
285 mvp,
286 wireframe: false,
287 wire_tint: [0.0; 4],
288 opacity: 1.0,
289 line_width: 1.0,
290 wire_base_width: 0.0,
291 prelit: false,
292 see_through: false,
293 instances: None,
294 screen_space: false,
295 };
296 let far_z_mvp = (Mat4::from_cols_array_2d(&mvp) * far_z_model()).to_cols_array_2d();
297 let draws = vec![
298 SceneDraw { screen_space: true, ..draw(m.background) },
299 SceneDraw { prelit: true, ..draw(m.prelit) },
300 draw(m.cube),
301 SceneDraw { instances: Some(m.marker_instances), ..draw(m.marker) },
302 SceneDraw { mvp: far_z_mvp, ..draw(m.far_z) },
303 SceneDraw { wire_base_width: 1.0, ..draw(m.sphere) },
304 SceneDraw { wireframe: true, wire_tint: [1.0, 1.0, 1.0, 0.6], ..draw(m.sphere_wires) },
305 SceneDraw { see_through: true, opacity: 0.45, ..draw(m.glass) },
306 SceneDraw { wireframe: true, see_through: true, ..draw(m.glass_wires) },
307 ];
308 stage.stage_scene(scissor, draws);
309 stage.stage_scene_images(vec![SceneImage {
310 image: self.image,
311 corners: IMAGE_CORNERS,
312 mvp,
313 opacity: 0.9,
314 before: 7,
315 }]);
316 false
317 }
318
319 fn update(&mut self, _: (), _: &mut bool, _: &mut bool) {}
320 fn tick(&mut self, _: f32, _: &mut bool) {}
321
322 fn display_list(&mut self, size: LogicalSize, _scale: f64) -> Option<DisplayList> {
323 let (_, h) = (size.width, size.height);
324 let mut pc = PaintCtx::new();
325 // The strip: a pane plate the UI stands on. The 3D pane is left
326 // unpainted, so the backdrop shows there.
327 pc.plate_spec(&PlateSpec {
328 rect: r(0.0, 0.0, STRIP, h),
329 material: Material::pane(),
330 window_corners: (true, false, false, true),
331 depth: cce_ui::layout::bevel_width(),
332 });
333 pc.text_with("3D probe".to_string(), 24.0, 24.0, 20.0, [230, 230, 236], Some("DejaVu Sans".into()), None);
334 // A frosted plate over the pane: its blur samples the backdrop.
335 let m = Material::opaque([0.0015, 0.0015, 0.0024, 0.25])
336 .with_frost(Frost::Frosted { compression: 0.6, refraction: 0.0, radius: Frost::DEFAULT_RADIUS });
337 pc.plate_spec(&PlateSpec { rect: r(STRIP + 40.0, h - 170.0, 420.0, 130.0), material: m, window_corners: (false, false, false, false), depth: 8.0 });
338 pc.text_with("frosted over the scene".to_string(), STRIP + 64.0, h - 120.0, 16.0, [240, 240, 246], Some("DejaVu Sans".into()), None);
339 Some(pc.finish())
340 }
341
342 fn handle_pointer_move(&mut self, _: LogicalPosition, _: &mut bool) {}
343 fn handle_mouse_input(&mut self, _: MouseButton, _: ElementState, _: LogicalPosition, _: &mut bool) -> Option<()> {
344 None
345 }
346 fn handle_mouse_wheel(&mut self, _: &MouseScrollDelta, _: LogicalPosition, _: &mut bool) {}
347 fn handle_key_input(&mut self, _: &KeyEvent, _: &mut bool) -> Option<()> {
348 None
349 }
350 }