cranpose_ui_graphics/space/scene.rs
1use std::sync::Arc;
2
3use super::{
4 eye::EyePlace,
5 math::{Mat4, Vec3},
6 mesh::{Mesh, MeshHit},
7};
8use crate::{Color, Point, Rect, Size, transform::ProjectiveTransform};
9
10/// How much nearer than its focal length a camera sees: points closer to the
11/// eye than `focal_length * NEAR_PLANE` are cut away. It is the w at which
12/// [`ProjectiveTransform::front_polygon`] cuts a perspective map.
13pub const NEAR_PLANE: f32 = crate::transform::NEAR_W;
14
15/// A pinhole camera that shows a 3D scene in a composable's bounds.
16///
17/// The camera looks along its own z axis with its x to the right and y down.
18/// A point at `(x, y, z)` in the camera's frame shows at
19/// `center + (x, y) * focal_length / z`, where `center` is the middle of the
20/// bounds.
21#[derive(Clone, Copy, Debug, PartialEq)]
22pub struct Camera {
23 /// Maps scene space into the camera's frame.
24 pub view: Mat4,
25 /// The distance from the eye to the image, in logical pixels: the
26 /// distance at which one scene unit shows as one pixel.
27 pub focal_length: f32,
28}
29
30impl Camera {
31 /// A camera at `eye` that looks at `target`. `down` is the scene
32 /// direction that shows as down; it need not be square to the view.
33 pub fn look_at(eye: Vec3, target: Vec3, down: Vec3, focal_length: f32) -> Self {
34 let forward = (target - eye)
35 .normalized()
36 .unwrap_or(Vec3::new(0.0, 0.0, 1.0));
37 let right = down
38 .cross(forward)
39 .normalized()
40 .unwrap_or(Vec3::new(1.0, 0.0, 0.0));
41 let down = forward.cross(right);
42 let row = |axis: Vec3| [axis.x, axis.y, axis.z, -axis.dot(eye)];
43 Self {
44 view: Mat4::from_rows([row(right), row(down), row(forward), [0.0, 0.0, 0.0, 1.0]]),
45 focal_length,
46 }
47 }
48
49 /// The focal length that shows `vertical_fov` radians across `height`
50 /// pixels.
51 pub fn focal_length_for(height: f32, vertical_fov: f32) -> f32 {
52 height * 0.5 / (vertical_fov * 0.5).tan()
53 }
54
55 /// The eye's position in scene space.
56 pub fn eye(&self) -> Vec3 {
57 self.view
58 .inverse()
59 .map_or(Vec3::ZERO, |inverse| inverse.transform_point(Vec3::ZERO))
60 }
61
62 /// The camera-frame depth below which the camera sees nothing.
63 pub fn near(&self) -> f32 {
64 self.focal_length * NEAR_PLANE
65 }
66
67 /// Where `point` shows in bounds of `size`; `None` when it lies nearer
68 /// than [`Self::near`] or behind the eye.
69 pub fn project(&self, point: Vec3, size: Size) -> Option<Point> {
70 let seen = self.view.transform_point(point);
71 (seen.z >= self.near()).then(|| {
72 let scale = self.focal_length / seen.z;
73 Point::new(
74 size.width * 0.5 + seen.x * scale,
75 size.height * 0.5 + seen.y * scale,
76 )
77 })
78 }
79
80 /// The map from the scene this camera sees into the frame in which
81 /// `window`, a rect on the frame's plane, is a doorway into the scene:
82 /// the camera stands in the doorway's middle, looking along the frame's
83 /// z, with its x and y along the frame's, a scene unit to a frame unit.
84 /// What the camera sees lies behind the doorway.
85 pub fn doorway(&self, window: Rect) -> Mat4 {
86 self.view.then(&Mat4::translation(Vec3::new(
87 window.x + window.width * 0.5,
88 window.y + window.height * 0.5,
89 0.0,
90 )))
91 }
92
93 /// The ray from the eye through `point` of bounds of `size`, in scene
94 /// space, with a direction that reaches depth one in the camera's frame
95 /// at distance one.
96 pub fn ray(&self, point: Point, size: Size) -> Option<Ray> {
97 let inverse = self.view.inverse()?;
98 let through = Vec3::new(
99 (point.x - size.width * 0.5) / self.focal_length,
100 (point.y - size.height * 0.5) / self.focal_length,
101 1.0,
102 );
103 Some(Ray {
104 origin: inverse.transform_point(Vec3::ZERO),
105 direction: inverse.transform_vector(through),
106 })
107 }
108}
109
110/// A half line in scene space.
111#[derive(Clone, Copy, Debug, PartialEq)]
112pub struct Ray {
113 /// Where the ray starts.
114 pub origin: Vec3,
115 /// Which way the ray goes; distances along the ray count in its length.
116 pub direction: Vec3,
117}
118
119/// A light from one direction, with an even part that reaches every side.
120#[derive(Clone, Copy, Debug, PartialEq)]
121pub struct Light {
122 /// The direction the light travels, in scene space.
123 pub direction: Vec3,
124 /// How bright a surface the light does not reach is, from 0 to 1: the
125 /// side of a lit surface turned away from the light, and any surface in
126 /// shadow.
127 pub ambient: f32,
128 /// Whether the surfaces that cast shadows cast them on the scene's
129 /// surfaces.
130 pub shadows: bool,
131 /// The angle the light spans as the scene sees it, in radians: 0 for a
132 /// point far away, whose shadows have hard edges. A light of some size
133 /// casts shadows that are sharp where a caster meets a surface and soft
134 /// farther from it, as the sun's 0.0093 does and a wide lamp's more.
135 pub size: f32,
136}
137
138impl Light {
139 /// How bright a surface is, from `ambient` to 1, where its side that
140 /// faces the viewer has `normal`: the light reaches only a side that
141 /// faces it.
142 pub fn brightness(&self, normal: Vec3) -> f32 {
143 let facing = match (self.direction.normalized(), normal.normalized()) {
144 (Some(direction), Some(normal)) => (-normal.dot(direction)).max(0.0),
145 _ => 0.0,
146 };
147 self.ambient + (1.0 - self.ambient) * facing
148 }
149
150 /// The map from scene space to a shadow map of this light that holds
151 /// `casters`: x and y from -1 to 1 across the map, and depth from 0 at
152 /// the caster nearest the light to 1 at the farthest. A point farther
153 /// along the light than a caster's depth there lies in its shadow.
154 /// `None` without casters, or for a light with no direction.
155 pub fn shadow_projection<'a>(
156 &self,
157 casters: impl IntoIterator<Item = &'a MeshSurface>,
158 ) -> Option<Mat4> {
159 const MARGIN: f32 = 0.02;
160 let forward = self.direction.normalized()?;
161 let hint = if forward.y.abs() < 0.9 {
162 Vec3::new(0.0, 1.0, 0.0)
163 } else {
164 Vec3::new(1.0, 0.0, 0.0)
165 };
166 let right = hint.cross(forward).normalized()?;
167 let up = forward.cross(right);
168 let view = Mat4::from_rows([
169 [right.x, right.y, right.z, 0.0],
170 [up.x, up.y, up.z, 0.0],
171 [forward.x, forward.y, forward.z, 0.0],
172 [0.0, 0.0, 0.0, 1.0],
173 ]);
174 let mut low = Vec3::new(f32::INFINITY, f32::INFINITY, f32::INFINITY);
175 let mut high = -low;
176 for caster in casters {
177 let (caster_low, caster_high) = caster.mesh.bounds_in(&caster.transform.then(&view));
178 (low, high) = (low.min(caster_low), high.max(caster_high));
179 }
180 let extent = high - low;
181 if !(extent.x.is_finite() && extent.y.is_finite() && extent.z.is_finite()) {
182 return None;
183 }
184 let pad = |extent: f32| (extent * MARGIN).max(1.0);
185 let (low, high) = (
186 low - Vec3::new(pad(extent.x), pad(extent.y), pad(extent.z)),
187 high + Vec3::new(pad(extent.x), pad(extent.y), pad(extent.z)),
188 );
189 let extent = high - low;
190 let fit = Mat4::from_rows([
191 [2.0 / extent.x, 0.0, 0.0, -1.0 - 2.0 * low.x / extent.x],
192 [0.0, 2.0 / extent.y, 0.0, -1.0 - 2.0 * low.y / extent.y],
193 [0.0, 0.0, 1.0 / extent.z, -low.z / extent.z],
194 [0.0, 0.0, 0.0, 1.0],
195 ]);
196 Some(view.then(&fit))
197 }
198}
199
200/// A scene's camera and light, which a `scene3d` modifier shows its
201/// children's mesh surfaces through.
202///
203/// A scene inside another scene's space is a doorway into a space of its
204/// own: the rect its node takes in its frame opens onto that space, which
205/// lies behind it, placed so the doorway stands where the inner scene's
206/// camera stands (see [`Camera::doorway`]). The window's eye sees through
207/// doorways from the space it stands in, as deep as they show on screen,
208/// and walks through them (see [`EyePlace`]).
209#[derive(Clone, Debug, PartialEq)]
210pub struct Scene3d {
211 /// The camera the scene is seen through. For a scene behind a doorway,
212 /// the one that stands in the doorway; for the window's scene with an
213 /// [`eye`](Self::eye), the eye's, in the space the eye stands in.
214 pub camera: Camera,
215 /// The light on the scene's lit surfaces, or `None` for none.
216 pub light: Option<Light>,
217 /// For the window's scene, where its eye stands among the spaces behind
218 /// the scene's doorways and where the renderer leaves what the eye can
219 /// bump into; `None` for a camera in the scene's own space that walks
220 /// through nothing. A scene behind a doorway leaves it out.
221 pub eye: Option<EyePlace>,
222}
223
224/// A composable's content drawn on a mesh placed in a scene.
225#[derive(Clone, Debug, PartialEq)]
226pub struct MeshSurface {
227 /// The mesh the content is drawn on.
228 pub mesh: Arc<Mesh>,
229 /// Places the mesh in the scene.
230 pub transform: Mat4,
231 /// How the content meets the scene's light and what lies behind it.
232 pub material: Material,
233 /// Whether the surface casts a shadow from the scene's light. Every lit
234 /// and unlit surface takes the shadows that fall on it.
235 pub casts_shadow: bool,
236 /// Whether an eye that walks into the surface (see
237 /// [`EyeWorld::walk`](super::EyeWorld::walk)) stops there. A doorway in
238 /// the surface's content lets it through.
239 pub solid: bool,
240}
241
242/// How a surface's content meets the scene's light and what lies behind
243/// it.
244#[derive(Clone, Debug, PartialEq)]
245pub enum Material {
246 /// Shaded by the angle the light falls at, and darkened where a shadow
247 /// falls on it.
248 Lit,
249 /// The content as it is, as a screen shows it, darkened only where a
250 /// shadow falls on it.
251 Unlit,
252 /// The content at its full colour, as fire or a lamp shows: neither the
253 /// light nor a shadow changes it.
254 Emissive,
255 /// Adds the content's colour to what lies behind it, as light does: a
256 /// halo, a flame or a spark. It hides nothing, and surfaces nearer the
257 /// eye hide it.
258 Glow,
259 /// The scene behind shows through, bent by the surface's curve, with the
260 /// light's highlight on it and the content drawn over it. Glass shows
261 /// the surfaces that are not glass behind it.
262 Glass(Glass),
263 /// A shape an app's shader finds inside the mesh, ray by ray, as the
264 /// camera sees it, with the depth of what each ray meets: the mesh only
265 /// bounds it. A volume casts no shadow.
266 Volume(Volume),
267}
268
269/// An app's shader that draws inside a mesh: each pixel the mesh covers
270/// asks it what the camera's ray through the pixel meets.
271///
272/// `shader` is WGSL that defines
273///
274/// ```text
275/// fn volume(eye: vec3<f32>, direction: vec3<f32>, entry: f32, exit: f32, footprint: f32) -> VolumeHit
276/// ```
277///
278/// in the mesh's own coordinates: the eye; the ray's direction, of length
279/// one; how far along the ray it enters the mesh's bounds, zero for an eye
280/// inside them, and leaves the mesh; and how wide a pixel is, in radians.
281/// It returns a `VolumeHit`: the colour, premultiplied, nothing at an alpha
282/// of zero; and the point the ray met, which sets the pixel's depth.
283/// `volume_param(index)` reads `params[index]`, `volume_content(uv)` the
284/// surface's content and `volume_light()` the scene's light, in the mesh's
285/// coordinates. The renderer puts
286/// [`MESH_VOLUME_PRELUDE_WGSL`](crate::framework_shaders::MESH_VOLUME_PRELUDE_WGSL),
287/// which declares them, before it. The mesh's normals must point out of it.
288#[derive(Clone, Debug)]
289pub struct Volume {
290 /// The app's WGSL. Keep one `Arc` per shader: the renderer builds a
291 /// pipeline for each.
292 pub shader: Arc<str>,
293 /// The values the shader reads.
294 pub params: [f32; Volume::PARAMS],
295}
296
297impl Volume {
298 /// How many values a volume's shader reads.
299 pub const PARAMS: usize = 32;
300}
301
302impl PartialEq for Volume {
303 fn eq(&self, other: &Self) -> bool {
304 Arc::ptr_eq(&self.shader, &other.shader) && self.params == other.params
305 }
306}
307
308/// How glass bends and tints the scene behind it.
309#[derive(Clone, Copy, Debug, PartialEq)]
310pub struct Glass {
311 /// The refractive index: 1 bends nothing, window glass is 1.5 and
312 /// diamond 2.4.
313 pub index: f32,
314 /// How far a ray travels inside the glass, in scene units: a ball's
315 /// diameter for a solid ball. A ray bends entering the glass and again
316 /// leaving it, as through a solid ball, so a thick ball shows the scene
317 /// behind it upside down.
318 pub thickness: f32,
319 /// The colour the glass tints what shows through it; white for clear
320 /// glass.
321 pub tint: Color,
322}
323
324impl Glass {
325 /// Clear window glass a ray crosses for `thickness` scene units.
326 pub const fn clear(thickness: f32) -> Self {
327 Self {
328 index: 1.5,
329 thickness,
330 tint: Color(1.0, 1.0, 1.0, 1.0),
331 }
332 }
333}
334
335impl MeshSurface {
336 /// The content of a composable of `size` on the mesh: the content point
337 /// where the ray meets the mesh, the triangle it meets, and how far along
338 /// the ray.
339 pub fn cast(&self, ray: Ray, size: Size) -> Option<SurfaceHit> {
340 let inverse = self.transform.inverse()?;
341 let hit = self.mesh.cast(
342 inverse.transform_point(ray.origin),
343 inverse.transform_vector(ray.direction),
344 )?;
345 Some(SurfaceHit::of(hit, size))
346 }
347
348 /// The content point of a composable of `size` where the ray meets the
349 /// plane of `triangle`: a drag that leaves the mesh's edge goes on along
350 /// it.
351 pub fn cast_plane(&self, ray: Ray, triangle: u32, size: Size) -> Option<Point> {
352 let inverse = self.transform.inverse()?;
353 let uv = self.mesh.cast_plane(
354 inverse.transform_point(ray.origin),
355 inverse.transform_vector(ray.direction),
356 triangle,
357 )?;
358 Some(content_point(uv, size))
359 }
360
361 /// The projective map that carries the content of a composable of
362 /// `size` from the mesh to the bounds of `viewport` that `camera` shows.
363 /// It is exact for a flat mesh; for a curved one it follows the plane
364 /// that touches the mesh at the content's middle. It sends the part
365 /// nearer than the camera's near plane to w below [`NEAR_PLANE`], which
366 /// [`ProjectiveTransform::front_polygon`] cuts away. `None` for content
367 /// of no size, or for a mesh with no texture plane.
368 pub fn content_transform(
369 &self,
370 camera: &Camera,
371 viewport: Size,
372 size: Size,
373 ) -> Option<ProjectiveTransform> {
374 if !(size.width > 0.0 && size.height > 0.0) {
375 return None;
376 }
377 let plane = self.mesh.texture_plane()?;
378 let to_camera = self.transform.then(&camera.view);
379 let origin = to_camera.transform_point(plane.origin);
380 let right = to_camera.transform_vector(plane.u_axis) * (1.0 / size.width);
381 let down = to_camera.transform_vector(plane.v_axis) * (1.0 / size.height);
382 let (center_x, center_y) = (viewport.width * 0.5, viewport.height * 0.5);
383 let focal_length = camera.focal_length;
384 let column = |v: Vec3| {
385 [
386 v.x + center_x * v.z / focal_length,
387 v.y + center_y * v.z / focal_length,
388 v.z / focal_length,
389 ]
390 };
391 let [a, b, c] = [column(right), column(down), column(origin)];
392 Some(ProjectiveTransform::from_rows([
393 [a[0], b[0], c[0]],
394 [a[1], b[1], c[1]],
395 [a[2], b[2], c[2]],
396 ]))
397 }
398
399 /// The map from the content space of a composable of `size` on the mesh
400 /// into the scene: x and y in the content's pixels from its top left, z
401 /// into the surface, away from the side it shows its content to, a unit
402 /// as long as a content pixel. A mesh surface inside this content is
403 /// placed in that space. It is exact for a flat mesh; for a curved one it
404 /// follows the plane that touches the mesh at the content's middle.
405 /// `None` for content of no size, or for a mesh with no texture plane.
406 pub fn content_to_scene(&self, size: Size) -> Option<Mat4> {
407 if !(size.width > 0.0 && size.height > 0.0) {
408 return None;
409 }
410 let plane = self.mesh.texture_plane()?;
411 let right = self.transform.transform_vector(plane.u_axis) * (1.0 / size.width);
412 let down = self.transform.transform_vector(plane.v_axis) * (1.0 / size.height);
413 let into = right.cross(down).normalized()? * (right.length() * down.length()).sqrt();
414 Some(Mat4::from_axes(
415 right,
416 down,
417 into,
418 self.transform.transform_point(plane.origin),
419 ))
420 }
421
422 /// This surface with its transform followed by `to_scene`: a surface
423 /// inside another's content, placed in the scene through the other's
424 /// [`content_to_scene`](Self::content_to_scene). Glass keeps its
425 /// thickness in the units of the space it is placed from.
426 pub fn placed_by(&self, to_scene: &Mat4) -> Self {
427 let material = match &self.material {
428 Material::Glass(glass) => Material::Glass(Glass {
429 thickness: glass.thickness * to_scene.length_scale(),
430 ..*glass
431 }),
432 material => material.clone(),
433 };
434 Self {
435 mesh: Arc::clone(&self.mesh),
436 transform: self.transform.then(to_scene),
437 material,
438 casts_shadow: self.casts_shadow,
439 solid: self.solid,
440 }
441 }
442
443 /// Where the content point `point` of a composable of `size` lies in the
444 /// scene.
445 pub fn point_at(&self, point: Point, size: Size) -> Option<Vec3> {
446 let uv = [
447 point.x / size.width.max(f32::MIN_POSITIVE),
448 point.y / size.height.max(f32::MIN_POSITIVE),
449 ];
450 self.mesh
451 .point_at(uv)
452 .map(|position| self.transform.transform_point(position))
453 }
454}
455
456/// Where a ray meets a [`MeshSurface`].
457#[derive(Clone, Copy, Debug, PartialEq)]
458pub struct SurfaceHit {
459 /// How far along the ray, in units of its direction.
460 pub distance: f32,
461 /// The mesh triangle the ray meets.
462 pub triangle: u32,
463 /// The content point at the hit.
464 pub point: Point,
465}
466
467impl SurfaceHit {
468 fn of(hit: MeshHit, size: Size) -> Self {
469 Self {
470 distance: hit.distance,
471 triangle: hit.triangle,
472 point: content_point(hit.uv, size),
473 }
474 }
475}
476
477fn content_point([u, v]: [f32; 2], size: Size) -> Point {
478 Point::new(u * size.width, v * size.height)
479}
480
481/// Where a layer sits in 3D, as its graphics layer carries it.
482///
483/// Inside a scene, every layer takes a frame from its parent's: its place
484/// in its parent's layout, turned into 3D on the parent's plane, with z
485/// away from the side the content shows to. A layer that carries a space
486/// changes the frame its descendants take.
487#[derive(Clone, Debug, PartialEq)]
488pub enum LayerSpace {
489 /// The layer shows a scene: its children's frames are the scene's
490 /// space. Inside another scene, the layer's rect is a doorway into it.
491 Scene(Scene3d),
492 /// The layer's content is drawn on a mesh placed in the layer's frame,
493 /// and its children's frames lie in that content.
494 Surface(MeshSurface),
495 /// Moves the frames of the layer's descendants in its own frame, as a
496 /// group of 3D objects moves together. The layer's own draws stay where
497 /// they would be without it.
498 Transform(Mat4),
499}