Skip to main content

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}