rlx_core/render/camera.rs
1//! The shared 3D camera (ADR-0257): an orbit camera that projects world points
2//! through a perspective lens, for every pipeline that draws a 3D primitive.
3//!
4//! Two halves, one function. `CAMERA_WGSL` is prepended to each 3D pipeline's
5//! shader and projects on the GPU; [`CameraView`] is the same matrix on the CPU,
6//! where a scene clips its primitives against the near plane and culls what
7//! lies outside the frustum before anything is uploaded. The two are held equal
8//! by a test that runs the WGSL `project()` in a compute pass and compares it
9//! with [`CameraView::clip`] on a fixed set of points.
10//!
11//! # The space
12//!
13//! The camera orbits the world origin. `yaw` turns it about the world's `+y`
14//! axis, `pitch` raises it above the `xz` plane, and `distance` is how far the
15//! eye sits from the origin. At `yaw = 0, pitch = 0` the eye is on `+z` looking
16//! down `-z`, with world `+x` to the right and `+y` up on screen.
17//!
18//! Clip space puts the view depth in `w`, so the perspective divide is a divide
19//! by the distance in front of the eye along the view axis. The `z` row is zero:
20//! no 3D pipeline has a depth attachment. Additive light needs no occlusion
21//! (ADR-0044), and a `solid` frame occludes by drawing its strokes far to near
22//! rather than by a depth test (ADR-0263).
23//!
24//! # Depth of field is per primitive, not per pixel
25//!
26//! [`Lens::coc`] is the thin-lens circle of confusion at a view depth,
27//! `aperture * |depth - focus| / depth`, in pixels and clamped to the tier's
28//! cap. A pipeline evaluates it at each endpoint of what it draws and widens the
29//! primitive by it, so one line can be sharp at the focal plane and soft at both
30//! ends (ADR-0257). The additive scenes this serves have no single depth per
31//! pixel for a post-process blur to read.
32//!
33//! # The framing controls keep their meaning
34//!
35//! The aspect is the **render target's**, handed in by the caller, never an
36//! internal grid's (ADR-0037). The engine-wide `zoom` divides the field of
37//! view, and `pan_x` / `pan_y` shift after the projection, in the same units
38//! the 2D [`ViewTransform`](crate::render::scenes::lines::ViewTransform) pans
39//! in: `pan_x` is divided by the aspect on the way to clip space, so one unit of
40//! pan moves the picture the same number of pixels on both axes.
41
42// Hot-path panic-denial pragma (the hygiene guard scans `render/`). A view is
43// built once per frame and every primitive is projected through it.
44#![deny(
45 clippy::unwrap_used,
46 clippy::expect_used,
47 clippy::indexing_slicing,
48 clippy::panic,
49 clippy::unreachable
50)]
51
52use crate::render::scenes::{
53 CapOverflow, OverflowContext, ParamGroup, ParamKind, ParamSpec, default_of,
54};
55
56/// The WGSL half: the `Camera` uniform's shape and `project()`, prepended to
57/// each 3D pipeline's own shader. It declares no binding.
58pub(crate) const CAMERA_WGSL: &str = include_str!("camera.wgsl");
59
60/// How close to the eye a primitive may reach before it is clipped, in world
61/// units. A point nearer than this projects through a divide by nearly zero.
62pub const NEAR: f32 = 0.05;
63
64/// The widest field of view the lens opens to, in radians, after `zoom` has
65/// divided it. Past about 170 degrees the tangent grows without bound and the
66/// picture is all edge.
67pub(crate) const MAX_FOV: f32 = 3.0;
68
69/// The narrowest, in radians. `zoom` divides the field of view, so a large
70/// `zoom` would otherwise reach a lens that magnifies without limit.
71pub(crate) const MIN_FOV: f32 = 0.01;
72
73/// The steepest `pitch` the orbit reaches, in radians, just short of straight
74/// up or down. At a quarter turn the view axis is parallel to world `+y` and
75/// the screen's right-hand direction is undefined.
76const MAX_PITCH: f32 = 1.55;
77
78/// An orbit camera, as a preset binds it (ADR-0257).
79#[derive(Debug, Clone, Copy, PartialEq)]
80pub struct Camera3d {
81 /// Turn about world `+y`, in radians. A preset writing `time * 0.05` here
82 /// orbits slowly.
83 pub yaw: f32,
84 /// Elevation above the `xz` plane, in radians; clamped short of a quarter
85 /// turn either way.
86 pub pitch: f32,
87 /// Eye to orbit target, in world units.
88 pub distance: f32,
89 /// The vertical field of view, in radians, before `zoom` divides it.
90 pub fov: f32,
91 /// Where the focal plane sits in the scene's volume: `0` at its nearest
92 /// extent, `1` at its farthest. Normalized so an author never deals in world
93 /// units; [`CameraView::focal_depth`] resolves it against the volume.
94 pub focus: f32,
95 /// The lens's aperture, as the circle of confusion in pixels a point would
96 /// have at infinite depth. `0` is a pinhole and blurs nothing.
97 pub aperture: f32,
98}
99
100impl Camera3d {
101 /// The view this camera gives onto a render target of `aspect` (width over
102 /// height), under the engine-wide `zoom` and `pan` (ADR-0018).
103 ///
104 /// Every input is taken as a raw bound value and made safe here, once: a
105 /// non-finite or out-of-range yaw, pitch, distance, field of view, zoom or
106 /// aspect resolves to the nearest usable camera rather than a NaN matrix.
107 pub fn view(&self, aspect: f32, zoom: f32, pan: [f32; 2]) -> CameraView {
108 let finite = |v: f32, fallback: f32| if v.is_finite() { v } else { fallback };
109 let yaw = finite(self.yaw, 0.0);
110 let pitch = finite(self.pitch, 0.0).clamp(-MAX_PITCH, MAX_PITCH);
111 let distance = finite(self.distance, 1.0).max(NEAR * 2.0);
112 let zoom = finite(zoom, 1.0).max(1e-3);
113 let fov = (finite(self.fov, 1.0) / zoom).clamp(MIN_FOV, MAX_FOV);
114 let aspect = finite(aspect, 1.0).max(0.1);
115 let [pan_x, pan_y] = [finite(pan[0], 0.0), finite(pan[1], 0.0)];
116
117 let (sy, cy) = yaw.sin_cos();
118 let (sp, cp) = pitch.sin_cos();
119 let eye = [distance * cp * sy, distance * sp, distance * cp * cy];
120 // The view axis, from the eye toward the origin: `-eye / distance`,
121 // written from the angles so it is unit length without a square root.
122 let forward = [-cp * sy, -sp, -cp * cy];
123 // Screen right: `forward x up`, which for up = +y is the horizontal
124 // (cos yaw, 0, -sin yaw) whatever the pitch.
125 let right = [cy, 0.0, -sy];
126 // Screen up: `right x forward`, unit because the two are orthonormal.
127 let up = cross(right, forward);
128
129 let s = 1.0 / (0.5 * fov).tan();
130 let sx = s / aspect;
131 let (re, ue, fe) = (dot(right, eye), dot(up, eye), dot(forward, eye));
132 // Rows of the world-to-clip matrix acting on (p, 1):
133 // clip.x = (right . (p - eye)) * s / aspect + (pan_x / aspect) * depth
134 // clip.y = (up . (p - eye)) * s + pan_y * depth
135 // clip.z = 0
136 // clip.w = forward . (p - eye) -- the depth
137 // so the divide by `w` lands the pan as a constant shift in NDC.
138 let px = pan_x / aspect;
139 let row0 = [
140 right[0] * sx + forward[0] * px,
141 right[1] * sx + forward[1] * px,
142 right[2] * sx + forward[2] * px,
143 -re * sx - fe * px,
144 ];
145 let row1 = [
146 up[0] * s + forward[0] * pan_y,
147 up[1] * s + forward[1] * pan_y,
148 up[2] * s + forward[2] * pan_y,
149 -ue * s - fe * pan_y,
150 ];
151 let row3 = [forward[0], forward[1], forward[2], -fe];
152 let rows = [row0, row1, [0.0; 4], row3];
153 // Column-major, as WGSL's `mat4x4` is laid out.
154 let mut view_proj = [[0.0f32; 4]; 4];
155 for (c, column) in view_proj.iter_mut().enumerate() {
156 for (r, cell) in column.iter_mut().enumerate() {
157 *cell = rows
158 .get(r)
159 .and_then(|row| row.get(c))
160 .copied()
161 .unwrap_or(0.0);
162 }
163 }
164 CameraView {
165 view_proj,
166 aspect,
167 distance,
168 }
169 }
170}
171
172/// One frame's projection: the matrix `CAMERA_WGSL` multiplies by, and what
173/// the CPU needs to clip against it.
174#[derive(Debug, Clone, Copy, PartialEq)]
175pub struct CameraView {
176 /// World to clip, column-major.
177 pub view_proj: [[f32; 4]; 4],
178 /// The aspect the view was built for, after its clamp.
179 pub aspect: f32,
180 /// Eye to orbit target, after its clamp.
181 pub distance: f32,
182}
183
184impl CameraView {
185 /// `p` in clip space. **The CPU mirror of `project()` in `CAMERA_WGSL`**,
186 /// the same four dot products in the same order: a column-major matrix times
187 /// `(p, 1)`.
188 pub fn clip(&self, p: [f32; 3]) -> [f32; 4] {
189 let [c0, c1, c2, c3] = self.view_proj;
190 let mut out = [0.0f32; 4];
191 for (r, cell) in out.iter_mut().enumerate() {
192 let at = |c: &[f32; 4]| c.get(r).copied().unwrap_or(0.0);
193 *cell = at(&c0) * p[0] + at(&c1) * p[1] + at(&c2) * p[2] + at(&c3);
194 }
195 out
196 }
197
198 /// The view depth of the focal plane, for a volume of bounding radius
199 /// `radius` about the orbit target and a normalized `focus`: `0` the
200 /// volume's nearest extent, `1` its farthest. Never nearer than [`NEAR`].
201 ///
202 /// Written as `near + focus * (far - near)` and not as
203 /// `near + 2 * focus * radius`: the two differ in the last bit, and a
204 /// reference depth that moves by a bit moves every stroke width with it.
205 pub fn focal_depth(&self, focus: f32, radius: f32) -> f32 {
206 let near = self.distance - radius;
207 let far = self.distance + radius;
208 let focus = if focus.is_finite() { focus } else { 0.5 };
209 (near + focus * (far - near)).max(NEAR)
210 }
211
212 /// The view depth of `p`: its distance in front of the eye along the view
213 /// axis. Negative behind the eye.
214 pub fn depth(&self, p: [f32; 3]) -> f32 {
215 self.clip(p)[3]
216 }
217
218 /// The segment `a -> b` with any part nearer than [`NEAR`] cut away, or
219 /// `None` when all of it is.
220 ///
221 /// **Clipped, not culled**: a segment crossing the near plane keeps the
222 /// part in front of it, so an edge the camera orbits through shortens
223 /// instead of blinking out. The cut is taken in world space, where depth is
224 /// linear along the segment, so the new endpoint lies exactly on the plane.
225 pub fn clip_near(&self, a: [f32; 3], b: [f32; 3]) -> Option<([f32; 3], [f32; 3])> {
226 let (da, db) = (self.depth(a), self.depth(b));
227 match (da >= NEAR, db >= NEAR) {
228 (true, true) => Some((a, b)),
229 (false, false) => None,
230 (a_in, _) => {
231 let t = (NEAR - da) / (db - da);
232 let cut = lerp3(a, b, t);
233 Some(if a_in { (a, cut) } else { (cut, b) })
234 }
235 }
236 }
237
238 /// Whether the segment `a -> b`, both ends at or past [`NEAR`], lies wholly
239 /// outside one edge of the frame, allowing `margin` in normalized device
240 /// units for the stroke's own width.
241 ///
242 /// Conservative on purpose: a segment both of whose ends are outside
243 /// **different** edges may still cross the frame, and is kept.
244 pub fn outside(&self, a: [f32; 3], b: [f32; 3], margin: f32) -> bool {
245 let ndc = |p: [f32; 3]| {
246 let c = self.clip(p);
247 [c[0] / c[3], c[1] / c[3]]
248 };
249 let ([ax, ay], [bx, by]) = (ndc(a), ndc(b));
250 let edge = 1.0 + margin;
251 (ax > edge && bx > edge)
252 || (ax < -edge && bx < -edge)
253 || (ay > edge && by > edge)
254 || (ay < -edge && by < -edge)
255 }
256}
257
258/// A lens: the CPU half of `coc()` in `CAMERA_WGSL`, with its inputs made
259/// safe once.
260#[derive(Debug, Clone, Copy, PartialEq)]
261pub struct Lens {
262 /// Aperture in pixels, finite and non-negative.
263 pub aperture: f32,
264 /// The focal plane's view depth.
265 pub focal_depth: f32,
266 /// The largest circle of confusion drawn, in pixels — the tier's cap.
267 pub max_coc: f32,
268}
269
270impl Lens {
271 /// A lens with `aperture` and `max_coc` held finite and non-negative.
272 pub fn new(aperture: f32, focal_depth: f32, max_coc: f32) -> Self {
273 let safe = |v: f32| if v.is_finite() { v.max(0.0) } else { 0.0 };
274 Self {
275 aperture: safe(aperture),
276 focal_depth: focal_depth.max(NEAR),
277 max_coc: safe(max_coc),
278 }
279 }
280
281 /// The circle of confusion at view depth `depth`, as a radius in pixels.
282 /// **Mirrors `coc()` in `CAMERA_WGSL`**, term for term.
283 pub fn coc(&self, depth: f32) -> f32 {
284 let blur = self.aperture * (depth - self.focal_depth).abs() / depth;
285 blur.clamp(0.0, self.max_coc)
286 }
287}
288
289/// The `Camera` uniform `CAMERA_WGSL` declares, as it is uploaded.
290///
291/// **Field order is the WGSL struct's**, and both are `vec4`-aligned, so the
292/// Rust layout is the uniform layout with no padding to keep in step.
293#[repr(C)]
294#[derive(Clone, Copy, Debug, PartialEq, bytemuck::Pod, bytemuck::Zeroable)]
295pub struct CameraUniform {
296 /// [`CameraView::view_proj`].
297 pub view_proj: [[f32; 4]; 4],
298 /// `[target width px, target height px, reference depth, volume's nearest
299 /// view depth]`.
300 pub viewport: [f32; 4],
301 /// `[aperture px, focal depth, max circle of confusion px, volume's depth
302 /// span]`.
303 pub lens: [f32; 4],
304}
305
306impl CameraUniform {
307 /// The uniform for `view` on a `width` x `height` target through `lens`,
308 /// with pixel widths stated at the lens's focal plane. The volume is
309 /// unset, so `volume_depth()` in `CAMERA_WGSL` reads `0` everywhere until
310 /// [`with_volume`](Self::with_volume) names one.
311 pub fn new(view: &CameraView, width: u32, height: u32, lens: Lens) -> Self {
312 Self {
313 view_proj: view.view_proj,
314 viewport: [
315 width.max(1) as f32,
316 height.max(1) as f32,
317 lens.focal_depth,
318 0.0,
319 ],
320 lens: [lens.aperture, lens.focal_depth, lens.max_coc, 0.0],
321 }
322 }
323
324 /// This uniform with the scene's volume spanning view depths
325 /// `near_extent` to `near_extent + span`: the 0-nearest, 1-farthest scale
326 /// `focus` is stated on, which `fog` reads (ADR-0263).
327 pub fn with_volume(mut self, near_extent: f32, span: f32) -> Self {
328 self.viewport[3] = near_extent;
329 self.lens[3] = span;
330 self
331 }
332}
333
334/// How far past the frame a primitive may lie before it is culled, in
335/// normalized device units — room for the stroke's own width, so a line whose
336/// centre has left the frame does not take its visible edge with it.
337pub(crate) const CULL_MARGIN: f32 = 0.05;
338
339/// `yaw`, shared: the orbit's turn about the volume.
340pub(crate) const YAW: ParamSpec = ParamSpec {
341 name: "yaw",
342 default: 0.0,
343 range: Some([-std::f32::consts::PI, std::f32::consts::PI]),
344 doc: "Turns the camera around the scene's volume, in radians; bind it to a slow clock to orbit.",
345 kind: ParamKind::Modal,
346 group: ParamGroup::Motion,
347 main: false,
348};
349
350/// `pitch`, shared: the orbit's elevation.
351pub(crate) const PITCH: ParamSpec = ParamSpec {
352 name: "pitch",
353 default: 0.25,
354 range: Some([-MAX_PITCH, MAX_PITCH]),
355 doc: "Raises the camera above the scene's volume, in radians; negative looks up from below.",
356 kind: ParamKind::Modal,
357 group: ParamGroup::Motion,
358 main: false,
359};
360
361/// `distance`, shared: eye to orbit target.
362pub(crate) const DISTANCE: ParamSpec = ParamSpec {
363 name: "distance",
364 default: 3.5,
365 range: Some([1.5, 8.0]),
366 doc: "How far the camera sits from the centre of the scene's volume; nearer exaggerates the perspective.",
367 kind: ParamKind::Modal,
368 group: ParamGroup::Motion,
369 main: false,
370};
371
372/// `fov`, shared: the vertical field of view.
373pub(crate) const FOV: ParamSpec = ParamSpec {
374 name: "fov",
375 default: 0.8,
376 range: Some([0.2, 2.0]),
377 doc: "The camera's vertical field of view in radians; zoom divides it.",
378 kind: ParamKind::Modal,
379 group: ParamGroup::Motion,
380 main: false,
381};
382
383/// `focus`, shared: the focal plane, normalized across the volume's depth.
384pub(crate) const FOCUS: ParamSpec = ParamSpec {
385 name: "focus",
386 default: 0.5,
387 range: Some([0.0, 1.0]),
388 doc: "Where the focal plane sits in the depth of the scene's volume: 0 at its nearest point, 1 at its farthest.",
389 kind: ParamKind::Modal,
390 group: ParamGroup::Light,
391 main: true,
392};
393
394/// `aperture`, shared: the far field's circle of confusion, in pixels. Its
395/// range tops out at the highest tier's cap; a lower tier draws at its own cap
396/// and announces it.
397pub(crate) const APERTURE: ParamSpec = ParamSpec {
398 name: "aperture",
399 default: 0.0,
400 range: Some([0.0, crate::render::TierConfig::RICH.max_coc_px as f32]),
401 doc: "The blur of the far background, in pixels; strokes nearer than the focal plane blur more, up to the tier's cap. 0 keeps every stroke sharp, and wider costs fill.",
402 kind: ParamKind::Modal,
403 group: ParamGroup::Light,
404 main: true,
405};
406
407/// `fog`, shared: light falling toward black with depth across the volume
408/// (ADR-0263). `0` is off, and every stroke keeps the light it had.
409pub(crate) const FOG: ParamSpec = ParamSpec {
410 name: "fog",
411 default: 0.0,
412 range: Some([0.0, 1.0]),
413 doc: "Fades strokes toward black with depth: at 1 the farthest point of the scene's volume is black and the nearest keeps its light. 0 is off.",
414 kind: ParamKind::Modal,
415 group: ParamGroup::Light,
416 main: false,
417};
418
419/// `solid`, shared: whether the frame's strokes occlude (ADR-0263). Read per
420/// frame, and solid at [`SOLID_AT`] and above; `0` is the additive glow.
421pub(crate) const SOLID: ParamSpec = ParamSpec {
422 name: "solid",
423 default: 0.0,
424 range: Some([0.0, 1.0]),
425 doc: "1 paints near strokes over far ones, so the scene reads as an object and crossings stop brightening; 0 is the additive glow. Solid sorts every stroke by depth each frame.",
426 kind: ParamKind::Modal,
427 group: ParamGroup::Light,
428 main: false,
429};
430
431/// The `solid` value at and above which a frame is drawn solid: the switch is
432/// two-valued, and the threshold sits between its two values so a bound
433/// `select(...)` or an eased value flips once, half-way.
434pub(crate) const SOLID_AT: f32 = 0.5;
435
436/// The camera params a 3D system binds, as one block (ADR-0258), in the
437/// shape of [`PanParams`](crate::render::scenes::common::PanParams): the
438/// `ParamSpec`s declared once here and spliced into each system's `PARAMS`,
439/// and a setter each scene's `set_param` delegates to.
440///
441/// **Nothing here clamps.** [`Camera3d::view`] and [`Lens::new`] make every
442/// raw bound value safe where it is read.
443#[derive(Debug, Clone, Copy, PartialEq)]
444pub(crate) struct CameraParams {
445 /// `yaw`, in radians.
446 pub yaw: f32,
447 /// `pitch`, in radians.
448 pub pitch: f32,
449 /// `distance`, in world units.
450 pub distance: f32,
451 /// `fov`, in radians.
452 pub fov: f32,
453 /// `focus`, `0` the volume's nearest extent and `1` its farthest.
454 pub focus: f32,
455 /// `aperture`, in pixels.
456 pub aperture: f32,
457 /// `fog`, `0` off and `1` the farthest extent black.
458 pub fog: f32,
459 /// `solid`, solid at [`SOLID_AT`] and above.
460 pub solid: f32,
461}
462
463impl Default for CameraParams {
464 fn default() -> Self {
465 let rest = |name| default_of(&Self::SPECS, name);
466 Self {
467 yaw: rest("yaw"),
468 pitch: rest("pitch"),
469 distance: rest("distance"),
470 fov: rest("fov"),
471 focus: rest("focus"),
472 aperture: rest("aperture"),
473 fog: rest("fog"),
474 solid: rest("solid"),
475 }
476 }
477}
478
479/// One frame of the shared lens logic, for a scene to clip, cull and draw by.
480#[derive(Debug, Clone, Copy, PartialEq)]
481pub(crate) struct CameraFrame {
482 /// The projection, for clipping and culling on the CPU.
483 pub view: CameraView,
484 /// The uniform every 3D pipeline binds.
485 pub uniform: CameraUniform,
486 /// The cull margin, in normalized device units, widened by the blur cap
487 /// when the lens blurs at all.
488 pub margin: f32,
489 /// The view depth of the volume's nearest extent.
490 pub near_extent: f32,
491 /// The view depth from the volume's nearest extent to its farthest.
492 pub span: f32,
493 /// The blur clamp to announce when `aperture` is past the tier's cap.
494 pub blur: Option<CapOverflow>,
495 /// `fog`, made safe: finite and in `[0, 1]`.
496 pub fog: f32,
497 /// Whether this frame is drawn solid: near strokes painted over far ones
498 /// in a back-to-front order rather than added (ADR-0263).
499 pub solid: bool,
500}
501
502impl CameraFrame {
503 /// Where view depth `depth` lies across the volume: `0` at its nearest
504 /// extent, `1` at its farthest, clamped. **Mirrors `volume_depth()` in
505 /// `CAMERA_WGSL`**, so a scene that colours or fogs on the CPU reads the
506 /// scale the `seg3d` pipeline fogs on.
507 pub fn volume_depth(&self, depth: f32) -> f32 {
508 if self.span > 0.0 {
509 ((depth - self.near_extent) / self.span).clamp(0.0, 1.0)
510 } else {
511 0.0
512 }
513 }
514
515 /// The share of its light a point at view depth `depth` keeps under this
516 /// frame's `fog`: `1 - fog * volume_depth`. **Mirrors `fog_light()` in
517 /// `CAMERA_WGSL`**, and is exactly `1.0` at `fog = 0`.
518 pub fn fog_light(&self, depth: f32) -> f32 {
519 (1.0 - self.fog * self.volume_depth(depth)).clamp(0.0, 1.0)
520 }
521}
522
523impl CameraParams {
524 /// The specs, in the order a system splices them.
525 pub(crate) const SPECS: [ParamSpec; 8] =
526 [YAW, PITCH, DISTANCE, FOV, FOCUS, APERTURE, FOG, SOLID];
527
528 /// Store `value` if `name` is one of the camera's, and say whether it
529 /// was.
530 pub(crate) fn set(&mut self, name: &str, value: f32) -> bool {
531 match name {
532 "yaw" => self.yaw = value,
533 "pitch" => self.pitch = value,
534 "distance" => self.distance = value,
535 "fov" => self.fov = value,
536 "focus" => self.focus = value,
537 "aperture" => self.aperture = value,
538 "fog" => self.fog = value,
539 "solid" => self.solid = value,
540 _ => return false,
541 }
542 true
543 }
544
545 /// Back to the declared defaults.
546 pub(crate) fn reset(&mut self) {
547 *self = Self::default();
548 }
549
550 /// The orbit camera these params describe.
551 pub(crate) fn camera(&self) -> Camera3d {
552 Camera3d {
553 yaw: self.yaw,
554 pitch: self.pitch,
555 distance: self.distance,
556 fov: self.fov,
557 focus: self.focus,
558 aperture: self.aperture,
559 }
560 }
561
562 /// This frame's view, lens and cull margin onto a `target` of pixels at
563 /// `aspect` (the render target's, ADR-0037), for a volume of bounding
564 /// `radius` about the orbit target, with blur held to the tier's `max_coc`.
565 ///
566 /// The aperture is the far field's blur, the one the lens approaches and
567 /// never passes behind focus; past the cap that is what draws shallower,
568 /// and [`CameraFrame::blur`] carries the clamp. The near side is unbounded
569 /// and saturates at the cap by design, so it is never judged (ADR-0257).
570 pub(crate) fn frame(
571 &self,
572 aspect: f32,
573 zoom: f32,
574 pan: [f32; 2],
575 target: (u32, u32),
576 radius: f32,
577 max_coc: f32,
578 ) -> CameraFrame {
579 let camera = self.camera();
580 let view = camera.view(aspect, zoom, pan);
581 let near_extent = view.distance - radius;
582 let far_extent = view.distance + radius;
583 let lens = Lens::new(
584 camera.aperture,
585 view.focal_depth(camera.focus, radius),
586 max_coc,
587 );
588 let blur = (lens.aperture > lens.max_coc).then(|| CapOverflow {
589 dropped: 0,
590 context: OverflowContext::Blur(lens.aperture.ceil() as u32),
591 cap: lens.max_coc as usize,
592 });
593 // A blurred stroke reaches past its centreline by up to the cap, so
594 // the cull keeps that much more of the frame's surround.
595 let margin = if lens.aperture > 0.0 {
596 CULL_MARGIN + 2.0 * lens.max_coc / target.1.max(1) as f32
597 } else {
598 CULL_MARGIN
599 };
600 let span = far_extent - near_extent;
601 let fog = if self.fog.is_finite() {
602 self.fog.clamp(0.0, 1.0)
603 } else {
604 0.0
605 };
606 CameraFrame {
607 view,
608 uniform: CameraUniform::new(&view, target.0, target.1, lens)
609 .with_volume(near_extent, span),
610 margin,
611 near_extent,
612 span,
613 blur,
614 fog,
615 // `NaN >= 0.5` is false, so a non-finite binding draws the glow.
616 solid: self.solid >= SOLID_AT,
617 }
618 }
619}
620
621fn dot(a: [f32; 3], b: [f32; 3]) -> f32 {
622 a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
623}
624
625fn cross(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
626 [
627 a[1] * b[2] - a[2] * b[1],
628 a[2] * b[0] - a[0] * b[2],
629 a[0] * b[1] - a[1] * b[0],
630 ]
631}
632
633fn lerp3(a: [f32; 3], b: [f32; 3], t: f32) -> [f32; 3] {
634 [
635 a[0] + (b[0] - a[0]) * t,
636 a[1] + (b[1] - a[1]) * t,
637 a[2] + (b[2] - a[2]) * t,
638 ]
639}
640
641#[cfg(test)]
642mod tests;