Skip to main content

ponko/three/
mod.rs

1//! 3D: meshes with materials, drawn from entities with a [`Transform3D`] and
2//! a [`Model`], through a perspective [`Camera3D`], lit by a sun and ambient
3//! light ([`Lighting`]). Behind the `3d` feature.
4//!
5//! The 3D pass is drawn first, with a depth buffer; 2D sprites, text and UI
6//! go on top of it. Games without 3D models never run it, and builds without
7//! the feature don't have it at all.
8//!
9//! Coordinates are right-handed with Y up (as in glTF): a camera with no
10//! rotation looks down -Z.
11
12mod animator;
13mod api;
14mod clip;
15mod collect;
16mod compute;
17mod effect;
18pub(crate) mod gi;
19mod gltf;
20mod ik;
21pub(crate) mod instances;
22pub use instances::{InstanceBatch, InstanceBatchOptions};
23mod lod;
24pub(crate) mod occlusion;
25pub use gi::{GlobalIllumination, MAX_GI_PROBES, MAX_GI_PROBES_PER_FRAME};
26pub use ik::{look_at_joint, two_bone_ik};
27pub use lod::{LodGroup, LodLevel, LodPick, LodRenderer, LodSettings, LodShadow};
28pub(crate) mod lights;
29mod mesh;
30pub use lights::{LIGHT_SHADOW_ATLAS_SIZE, LIGHT_SHADOW_SIZE, MAX_LIGHT_SHADOW_VIEWS, PointLight3D, SpotLight3D};
31pub(crate) use lights::{MAX_LIGHT_SHADOW_RESOLUTION, MIN_LIGHT_SHADOW_RESOLUTION};
32pub(crate) mod shadow_atlas;
33pub(crate) use shadow_atlas::{MAX_LIGHT_SHADOW_ATLAS, MIN_LIGHT_SHADOW_ATLAS};
34pub(crate) mod particles;
35pub use particles::{
36    ParticleBlend3D, ParticleCurve, ParticleEmitter3D, ParticleGradient, ParticleSettings3D, ParticleShape3D,
37    ParticleSpace3D,
38};
39mod decal;
40mod post;
41pub(crate) mod projected;
42pub use projected::{Decal3D, MAX_DECALS, MAX_DECALS_PER_CLUSTER, ReceivesDecals};
43pub(crate) mod probe;
44pub use probe::ReflectionProbe3D;
45mod quality;
46mod skin;
47pub(crate) mod strips;
48pub use strips::{Line3D, Trail3D};
49pub(crate) mod text3d;
50pub use text3d::{Text3D, TextAlign3D};
51mod far_terrain;
52mod sky;
53mod target;
54mod terrain;
55pub use far_terrain::FarTerrain;
56mod texture_array;
57
58pub use animator::{
59    AnimationCondition, AnimationEvent, AnimationGraph, AnimationLayer, AnimationParameter, AnimationState,
60    AnimationTransition, Animator, JointMask, LayerBlend, Motion, MotionChild, RootMotion,
61};
62pub(crate) use api::GpuMaterialDesc;
63#[cfg(feature = "window")]
64pub(crate) use api::MaterialTextures;
65pub use clip::{AnimationClip, AnimationTrack, Interpolation, JointAnimation, MorphAnimation};
66pub(crate) mod motion;
67#[cfg(feature = "window")]
68pub(crate) use collect::{FADED, Instance3D, MAX_CASCADES, MAX_SHADOW_VIEWS, shadow_views};
69pub(crate) use collect::{Frame3D, MAX_SHADOW_RESOLUTION, MIN_SHADOW_RESOLUTION, SHADOW_MAP_SIZE};
70pub use compute::{Compute, ComputePass, GpuBuffer};
71pub use effect::{Effect, ScreenEffect};
72pub use mesh::{MeshData, MorphFrame, MorphTarget};
73pub use post::{AmbientOcclusion, AntiAliasing, MAX_RENDER_SCALE, MIN_RENDER_SCALE, Post, Tonemap};
74#[cfg(feature = "window")]
75pub(crate) use post::{PostUniform, scaled_size};
76pub use quality::{GraphicsQuality, QualityLevel};
77#[cfg(feature = "window")]
78pub(crate) use skin::blend as blend_skin;
79pub use skin::{AttachToJoint, Joint, MorphWeights, Skeleton, SkinOf};
80pub(crate) use skin::{MeshMorph, MeshSkin, SkinFrame};
81#[cfg(all(test, feature = "window"))]
82pub(crate) use skin::{SkinDraw, normalized as normalized_weights};
83pub use sky::Sky;
84pub(crate) use sky::SkyLight;
85pub use target::TargetCamera;
86pub use terrain::{Heightfield, TERRAIN_LAYERS, TERRAIN_LODS, TerrainMaterialDesc, TerrainTile, TerrainTileData};
87pub use texture_array::{TextureArray, TextureKind};
88
89use glam::{Mat3, Mat4, Quat, Vec2, Vec3, Vec4};
90
91use crate::{Color, Texture};
92
93/// Position, rotation and scale of a 3D entity.
94///
95/// ```
96/// # use ponko::*;
97/// let crate_box = Transform3D::at(vec3(0.0, 0.5, -3.0)).rotated(Quat::from_rotation_y(0.3));
98/// # let _ = crate_box;
99/// ```
100#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
101#[serde(default)]
102#[repr(C, align(16))] // shared with C/C++ (`CrabbyTransform3D`): 48 bytes everywhere
103pub struct Transform3D {
104    /// Which way it faces.
105    pub rotation: Quat,
106    /// Where it is, in world units.
107    pub position: Vec3,
108    /// Size along its own axes. Default `(1, 1, 1)`.
109    pub scale: Vec3,
110}
111
112impl Default for Transform3D {
113    fn default() -> Self {
114        Self {
115            position: Vec3::ZERO,
116            rotation: Quat::IDENTITY,
117            scale: Vec3::ONE,
118        }
119    }
120}
121
122impl Transform3D {
123    /// At `position`, not rotated, scale 1.
124    ///
125    /// ```
126    /// # use ponko::*;
127    /// let t = Transform3D::at(vec3(1.0, 2.0, 3.0));
128    /// assert_eq!(t.scale, Vec3::ONE);
129    /// ```
130    #[must_use]
131    pub fn at(position: Vec3) -> Self {
132        Self {
133            position,
134            ..Self::default()
135        }
136    }
137
138    /// The same, rotated by `rotation`.
139    ///
140    /// ```
141    /// # use ponko::*;
142    /// let t = Transform3D::default().rotated(Quat::from_rotation_y(1.0));
143    /// # let _ = t;
144    /// ```
145    #[must_use]
146    pub fn rotated(self, rotation: Quat) -> Self {
147        Self { rotation, ..self }
148    }
149
150    /// The same, scaled by `scale` on every axis.
151    ///
152    /// ```
153    /// # use ponko::*;
154    /// assert_eq!(Transform3D::default().scaled(2.0).scale, Vec3::splat(2.0));
155    /// ```
156    #[must_use]
157    pub fn scaled(self, scale: f32) -> Self {
158        Self {
159            scale: Vec3::splat(scale),
160            ..self
161        }
162    }
163
164    /// The same, turned so its -Z axis points at `target` (with +Y as up).
165    ///
166    /// ```
167    /// # use ponko::*;
168    /// let t = Transform3D::at(vec3(0.0, 0.0, 5.0)).looking_at(Vec3::ZERO);
169    /// assert!((t.forward() - vec3(0.0, 0.0, -1.0)).length() < 1e-5);
170    /// ```
171    #[must_use]
172    pub fn looking_at(self, target: Vec3) -> Self {
173        Self {
174            rotation: look_rotation(target - self.position),
175            ..self
176        }
177    }
178
179    /// The direction it faces: its -Z axis.
180    ///
181    /// ```
182    /// # use ponko::*;
183    /// assert_eq!(Transform3D::default().forward(), vec3(0.0, 0.0, -1.0));
184    /// ```
185    #[must_use]
186    pub fn forward(&self) -> Vec3 {
187        self.rotation * Vec3::NEG_Z
188    }
189
190    /// The matrix from its local space to the world.
191    ///
192    /// ```
193    /// # use ponko::*;
194    /// let m = Transform3D::at(vec3(1.0, 0.0, 0.0)).matrix();
195    /// assert_eq!(m.transform_point3(Vec3::ZERO), vec3(1.0, 0.0, 0.0));
196    /// ```
197    #[must_use]
198    pub fn matrix(&self) -> Mat4 {
199        Mat4::from_scale_rotation_translation(self.scale, self.rotation, self.position)
200    }
201}
202
203/// The rotation that turns -Z towards `direction`, keeping +Y up (or +Z up
204/// when looking straight up or down).
205fn look_rotation(direction: Vec3) -> Quat {
206    let forward = direction.try_normalize().unwrap_or(Vec3::NEG_Z);
207    let up = if forward.y.abs() > 0.999 { Vec3::Z } else { Vec3::Y };
208    let right = forward.cross(up).normalize();
209    let up = right.cross(forward);
210    Quat::from_mat3(&Mat3::from_cols(right, up, -forward))
211}
212
213/// A mesh on the GPU: triangles, split into parts that each have a
214/// [`Material`]. Made with [`Ctx::create_mesh`](crate::Ctx::create_mesh) or
215/// loaded with [`Ctx::load_mesh`](crate::Ctx::load_mesh); a cheap handle.
216///
217/// ```no_run
218/// # use ponko::*;
219/// # fn f(ctx: &mut Ctx) {
220/// let tree: Mesh = ctx.load_mesh("tree.glb");
221/// ctx.world.spawn((Transform3D::default(), Model::new(tree)));
222/// # }
223/// ```
224#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
225#[repr(C)] // shared with C/C++ (`CrabbyMesh`)
226pub struct Mesh {
227    pub(crate) id: u32,
228}
229
230impl Mesh {
231    /// No mesh: a [`Model`] showing it draws nothing and casts no shadow, at
232    /// almost no cost. For models that are sometimes hidden, such as
233    /// plants beyond their draw distance. Unlike a freed mesh, it never
234    /// becomes another mesh, since no mesh is ever made with its id.
235    ///
236    /// ```
237    /// # use ponko::*;
238    /// Game::new("t", 1, 1).run_headless(0, |ctx| {
239    ///     let material = ctx.default_material();
240    ///     let bush = ctx.create_mesh(&MeshData::cube(1.0), material);
241    ///     ctx.world.spawn((Transform3D::default(), Model::new(Mesh::NONE)));
242    ///     assert!(!ctx.is_mesh_live(Mesh::NONE));
243    ///     assert_ne!(bush, Mesh::NONE);
244    /// });
245    /// ```
246    pub const NONE: Mesh = Mesh { id: u32::MAX };
247}
248
249/// What one mesh cost to draw in the last frame, from
250/// [`Ctx::mesh_costs`](crate::Ctx::mesh_costs).
251#[derive(Debug, Clone, PartialEq)]
252pub struct MeshCost {
253    /// The mesh.
254    pub mesh: Mesh,
255    /// The file it was loaded from, if it came from one (glTF meshes are
256    /// `file.glb#index`-style paths as they were loaded).
257    pub name: Option<String>,
258    /// Models the camera drew with it.
259    pub models: usize,
260    /// Triangles the camera's passes drew with it.
261    pub triangles: usize,
262    /// Models drawn with it into the shadow maps, all cascades together.
263    pub shadow_models: usize,
264    /// Triangles drawn with it into the shadow maps, all cascades together.
265    pub shadow_triangles: usize,
266}
267
268/// How a surface looks: its color (and texture), how rough and how metallic
269/// it is, and light it gives off. Made with
270/// [`Ctx::create_material`](crate::Ctx::create_material); a cheap handle.
271///
272/// ```no_run
273/// # use ponko::*;
274/// # fn f(ctx: &mut Ctx) {
275/// let gold = ctx.create_material(MaterialDesc { metallic: 1.0, roughness: 0.3, ..MaterialDesc::color(Color::hex(0xFFC83D)) });
276/// # }
277/// ```
278#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
279#[repr(C)] // shared with C/C++ (`CrabbyMaterial`)
280pub struct Material {
281    pub(crate) id: u32,
282}
283
284/// A material shader written in WGSL: made with
285/// [`Ctx::load_shader`](crate::Ctx::load_shader) or
286/// [`Ctx::create_shader`](crate::Ctx::create_shader); a cheap handle. See
287/// the 3D chapter of the guide for what a shader can do.
288///
289/// ```no_run
290/// # use ponko::*;
291/// # fn f(ctx: &mut Ctx) {
292/// let water = ctx.load_shader("shaders/water.wgsl");
293/// let lake = ctx.create_material(MaterialDesc { shader: Some(water), ..Default::default() });
294/// # }
295/// ```
296#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
297#[repr(C)] // shared with C/C++ (`CrabbyShader`)
298pub struct Shader {
299    pub(crate) id: u32,
300}
301
302impl Shader {
303    /// The engine's id of this shader (for the C API).
304    #[doc(hidden)]
305    #[must_use]
306    pub fn id(self) -> u32 {
307        self.id
308    }
309
310    /// A shader handle from an id (for the C API; check it with `Ctx::has_shader`).
311    #[doc(hidden)]
312    #[must_use]
313    pub fn from_id(id: u32) -> Self {
314        Self { id }
315    }
316}
317
318/// How a material covers what's behind it.
319#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
320#[repr(u32)]
321pub enum Blend {
322    /// Solid, whatever its alpha. The fastest: hidden pixels and shadows
323    /// skip the material's shader.
324    #[default]
325    Opaque = 0,
326    /// See-through by its alpha (glass, water). Drawn after everything
327    /// solid, casts no shadow; overlapping transparent models are not
328    /// sorted, so one may show over another that is in front of it.
329    Transparent = 1,
330    /// Solid, with pixels whose alpha is below 0.5 cut out (leaves,
331    /// fences, grass cards). Their shadows have the same holes.
332    Cutout = 2,
333}
334
335/// What a [`Material`] is made from.
336///
337/// ```
338/// # use ponko::*;
339/// let grass = MaterialDesc { roughness: 0.9, ..MaterialDesc::color(Color::hex(0x4C8A3A)) };
340/// assert_eq!(grass.metallic, 0.0);
341/// ```
342#[derive(Debug, Clone, Copy, PartialEq)]
343pub struct MaterialDesc {
344    /// Base color, multiplied with the texture. Default white.
345    pub color: Color,
346    /// An image on the surface (by the mesh's texture coordinates).
347    pub texture: Option<Texture>,
348    /// 0 is mirror-smooth, 1 is fully rough (matte). Default 0.6.
349    pub roughness: f32,
350    /// 0 for most things, 1 for bare metal. Default 0.
351    pub metallic: f32,
352    /// Light it gives off itself (not lighting anything else). Default black.
353    pub emissive: Color,
354    /// A normal map: surface detail (bumps, grooves) as tangent-space
355    /// normals, OpenGL style (green up), as glTF has them.
356    pub normal_map: Option<Texture>,
357    /// How strong the normal map is. Default 1.
358    pub normal_strength: f32,
359    /// An occlusion-roughness-metallic map, as glTF has it: red is ambient
360    /// occlusion, green multiplies `roughness`, blue multiplies `metallic`.
361    pub orm_map: Option<Texture>,
362    /// Multiplies `emissive` (which must not be black for it to show).
363    pub emissive_map: Option<Texture>,
364    /// How it covers what's behind it. Default [`Blend::Opaque`].
365    pub blend: Blend,
366    /// Draws the back of each triangle too (leaves, cloth, planes seen from
367    /// below). Default false.
368    pub double_sided: bool,
369    /// A custom shader; `None` is the built-in one.
370    pub shader: Option<Shader>,
371    /// Values for a custom shader: `material.custom[0]` to `[3]` there.
372    pub custom: [Vec4; 4],
373    /// Textures for a custom shader: `custom_texture0` and
374    /// `custom_texture1` there.
375    pub custom_textures: [Option<Texture>; 2],
376    /// Texture arrays for a custom shader: `custom_array0` to
377    /// `custom_array2` there (`texture_2d_array<f32>`), each read as its
378    /// [`TextureKind`] says.
379    pub custom_arrays: [Option<TextureArray>; 3],
380}
381
382impl Default for MaterialDesc {
383    fn default() -> Self {
384        Self {
385            color: Color::WHITE,
386            texture: None,
387            roughness: 0.6,
388            metallic: 0.0,
389            emissive: Color::rgb(0.0, 0.0, 0.0),
390            normal_map: None,
391            normal_strength: 1.0,
392            orm_map: None,
393            emissive_map: None,
394            blend: Blend::Opaque,
395            double_sided: false,
396            shader: None,
397            custom: [Vec4::ZERO; 4],
398            custom_textures: [None; 2],
399            custom_arrays: [None; 3],
400        }
401    }
402}
403
404impl MaterialDesc {
405    /// A plain material of one color.
406    ///
407    /// ```
408    /// # use ponko::*;
409    /// let red = MaterialDesc::color(Color::rgb(0.8, 0.1, 0.1));
410    /// # let _ = red;
411    /// ```
412    #[must_use]
413    pub fn color(color: Color) -> Self {
414        Self {
415            color,
416            ..Self::default()
417        }
418    }
419}
420
421/// Draws a [`Mesh`] where the entity's [`Transform3D`] is.
422///
423/// ```no_run
424/// # use ponko::*;
425/// # fn f(ctx: &mut Ctx) {
426/// let white = ctx.default_material();
427/// let cube = ctx.create_mesh(&MeshData::cube(1.0), white);
428/// for i in 0..10 {
429///     let tint = Color::rgb(i as f32 / 10.0, 0.5, 1.0);
430///     ctx.world.spawn((Transform3D::at(vec3(i as f32 * 2.0, 0.0, 0.0)), Model::new(cube).with_color(tint)));
431/// }
432/// # }
433/// ```
434#[derive(Debug, Clone, Copy, PartialEq)]
435#[repr(C)] // shared with C/C++ (`CrabbyModel`)
436pub struct Model {
437    /// What to draw.
438    pub mesh: Mesh,
439    /// Multiplied with the materials' colors, so one mesh can come in many
440    /// colors without more materials. Default white (no change).
441    pub color: Color,
442}
443
444/// Draws a different mesh into the shadow maps than the entity's [`Model`]
445/// shows: a cheap stand-in for a detailed model's shadow (a simplified copy,
446/// a billboard), or `None` for no shadow from this entity. Without it, the
447/// model's own mesh casts the shadow (unless
448/// [`Ctx::set_mesh_shadows`](crate::Ctx::set_mesh_shadows) turned that
449/// mesh's shadows off).
450///
451/// Far from the camera, a shadow is a few texels of the shadow map, so a
452/// mesh with a tenth of the triangles casts the same shadow for a tenth of
453/// the cost. Swap it by distance, as you would the model's own detail.
454///
455/// An entity with a `ShadowMesh` and no [`Model`] casts only the shadow: a
456/// stand-in for the shadow of several models around it that cast none. On
457/// a [`TerrainTile`], `ShadowMesh(None)` turns the ground's shadow off.
458///
459/// In scene files it's `"ShadowMesh": { "mesh": "tree_shadow.glb" }`, or
460/// `{ "mesh": null }` for no shadow.
461///
462/// ```no_run
463/// # use ponko::*;
464/// # fn f(ctx: &mut Ctx) {
465/// let tree = ctx.load_mesh("tree.glb");
466/// let blob = ctx.load_mesh("tree_shadow.glb"); // a few dozen triangles
467/// ctx.world.spawn((Transform3D::default(), Model::new(tree), ShadowMesh(Some(blob))));
468/// # }
469/// ```
470#[derive(Debug, Clone, Copy, PartialEq, Eq)]
471pub struct ShadowMesh(pub Option<Mesh>);
472
473impl Model {
474    /// Draws `mesh` as it is.
475    ///
476    /// ```no_run
477    /// # use ponko::*;
478    /// # fn f(ctx: &mut Ctx) {
479    /// let rock = ctx.load_mesh("rock.glb");
480    /// ctx.world.spawn((Transform3D::default(), Model::new(rock)));
481    /// # }
482    /// ```
483    #[must_use]
484    pub fn new(mesh: Mesh) -> Self {
485        Self {
486            mesh,
487            color: Color::WHITE,
488        }
489    }
490
491    /// The same, tinted with `color`.
492    ///
493    /// ```no_run
494    /// # use ponko::*;
495    /// # fn f(ctx: &mut Ctx, cube: Mesh) {
496    /// let red_cube = Model::new(cube).with_color(Color::rgb(1.0, 0.2, 0.2));
497    /// # }
498    /// ```
499    #[must_use]
500    pub fn with_color(self, color: Color) -> Self {
501        Self { color, ..self }
502    }
503}
504
505/// The 3D camera: where it is, which way it looks, and how wide it sees.
506/// One per game, at [`Ctx::camera3d`](crate::Ctx::camera3d).
507///
508/// ```no_run
509/// # use ponko::*;
510/// # fn f(ctx: &mut Ctx) {
511/// ctx.camera3d.position = vec3(0.0, 4.0, 10.0);
512/// ctx.camera3d.look_at(Vec3::ZERO);
513/// # }
514/// ```
515#[derive(Debug, Clone, Copy, PartialEq)]
516pub struct Camera3D {
517    /// Where the camera is.
518    pub position: Vec3,
519    /// Which way it looks: it sees along its -Z axis.
520    pub rotation: Quat,
521    /// How tall the view is, as an angle in radians. Default 60°.
522    pub fov_y: f32,
523    /// Nothing closer than this is drawn. Default 0.1.
524    pub near: f32,
525    /// Nothing farther than this is drawn. Default 1000.
526    pub far: f32,
527    /// Skip models hidden behind what was drawn a frame or two before.
528    /// Default off.
529    ///
530    /// The renderer keeps the farthest depth of small screen tiles and reads
531    /// it back without waiting for the GPU; a model whose bounds lie behind
532    /// it everywhere they cover isn't drawn (it still casts shadows). A model
533    /// that comes out from behind something can show up a frame or two late,
534    /// mostly when the camera moves fast. Needs compute shaders: on WebGL2
535    /// nothing is culled.
536    pub occlusion_culling: bool,
537    /// Window size in pixels, kept up to date by the engine.
538    viewport: Vec2,
539}
540
541impl Camera3D {
542    pub(crate) fn new(viewport: Vec2) -> Self {
543        Self {
544            position: Vec3::new(0.0, 2.0, 8.0),
545            rotation: look_rotation(Vec3::new(0.0, -2.0, -8.0)),
546            fov_y: 60f32.to_radians(),
547            near: 0.1,
548            far: 1000.0,
549            occlusion_culling: false,
550            viewport,
551        }
552    }
553
554    pub(crate) fn set_viewport(&mut self, viewport: Vec2) {
555        self.viewport = viewport;
556    }
557
558    /// Turns the camera to look at `target`, keeping +Y up.
559    ///
560    /// ```no_run
561    /// # use ponko::*;
562    /// # fn f(ctx: &mut Ctx, player: Vec3) {
563    /// ctx.camera3d.position = player + vec3(0.0, 6.0, 8.0);
564    /// ctx.camera3d.look_at(player);
565    /// # }
566    /// ```
567    pub fn look_at(&mut self, target: Vec3) {
568        self.rotation = look_rotation(target - self.position);
569    }
570
571    /// The direction the camera looks.
572    ///
573    /// ```no_run
574    /// # use ponko::*;
575    /// # fn f(ctx: &mut Ctx) {
576    /// let ahead = ctx.camera3d.position + ctx.camera3d.forward() * 5.0;
577    /// # }
578    /// ```
579    #[must_use]
580    pub fn forward(&self) -> Vec3 {
581        self.rotation * Vec3::NEG_Z
582    }
583
584    /// World to clip space: projection times view.
585    #[must_use]
586    pub(crate) fn view_proj(&self) -> Mat4 {
587        let aspect = (self.viewport.x / self.viewport.y.max(1.0)).max(1e-3);
588        let projection = glam::camera::rh::proj::directx::perspective(self.fov_y, aspect, self.near, self.far);
589        projection * self.view()
590    }
591
592    /// World to the camera's own space.
593    pub(crate) fn view(&self) -> Mat4 {
594        Mat4::from_rotation_translation(self.rotation, self.position).inverse()
595    }
596
597    /// The ray from the camera through window point `screen` (logical
598    /// pixels, as [`Ctx::mouse_pos`](crate::Ctx::mouse_pos)): its start and
599    /// unit direction. For picking things in the world with the mouse.
600    ///
601    /// ```no_run
602    /// # use ponko::*;
603    /// # fn f(ctx: &mut Ctx) {
604    /// let (from, dir) = ctx.camera3d.screen_ray(ctx.mouse_pos());
605    /// // Where the mouse points on the ground (y = 0):
606    /// if dir.y < 0.0 {
607    ///     let ground = from + dir * (-from.y / dir.y);
608    /// }
609    /// # }
610    /// ```
611    #[must_use]
612    pub fn screen_ray(&self, screen: Vec2) -> (Vec3, Vec3) {
613        let ndc = Vec2::new(
614            screen.x / self.viewport.x.max(1.0) * 2.0 - 1.0,
615            1.0 - screen.y / self.viewport.y.max(1.0) * 2.0,
616        );
617        let inverse = self.view_proj().inverse();
618        let far = inverse * Vec4::new(ndc.x, ndc.y, 1.0, 1.0);
619        let far = far.truncate() / far.w;
620        (self.position, (far - self.position).normalize_or(self.forward()))
621    }
622}
623
624/// The light in the 3D world: a sun far away (one direction everywhere)
625/// and ambient light from all around. At [`Ctx::lighting`](crate::Ctx::lighting).
626///
627/// ```no_run
628/// # use ponko::*;
629/// # fn f(ctx: &mut Ctx) {
630/// // Evening: low, orange sun.
631/// ctx.lighting.sun_direction = vec3(-1.0, -0.3, -0.2);
632/// ctx.lighting.sun_color = Color::hex(0xFFB066);
633/// # }
634/// ```
635#[derive(Debug, Clone, Copy, PartialEq)]
636pub struct Lighting {
637    /// The way the sunlight travels (from the sun towards the ground).
638    pub sun_direction: Vec3,
639    /// The sun's color.
640    pub sun_color: Color,
641    /// How bright the sun is. Default 3.
642    pub sun_intensity: f32,
643    /// Light from the sky, on surfaces facing up. Used when
644    /// [`Sky::atmosphere`] is off; with it, the sky's own light is used.
645    pub sky_color: Color,
646    /// Light bounced off the ground, on surfaces facing down. Like
647    /// `sky_color`, only without an atmosphere.
648    pub ground_color: Color,
649    /// Light from the horizon, on surfaces facing sideways: with it, the
650    /// ambient light goes from `ground_color` below through this to
651    /// `sky_color` above (a three-color gradient); without it (`None`, the
652    /// default), straight from ground to sky. Only without an atmosphere.
653    pub equator_color: Option<Color>,
654    /// How bright the sky and ground light is: 1 is as bright as the sky
655    /// makes it. Default 1.
656    pub ambient_intensity: f32,
657    /// Whether the sun casts shadows, and point and spot lights with
658    /// `shadows` set too (off turns every shadow off). Default on.
659    pub shadows: bool,
660    /// How far from the camera shadows reach, in world units. Default 60.
661    pub shadow_distance: f32,
662    /// How many shadow maps (cascades) share that distance, 1 to 4: each
663    /// covers a stretch further out than the one before, so shadows stay
664    /// sharp near the camera however far they reach. Fewer are cheaper.
665    /// Default 4.
666    pub shadow_cascades: u32,
667    /// Width and height of each of the sun's shadow maps, in texels:
668    /// 256 to 4096 (and no more than the GPU allows). Doubling it quadruples
669    /// the memory (`4 * resolution² * cascades` bytes) and sharpens shadows.
670    /// Changing it at run time makes new shadow maps. Default 2048.
671    pub shadow_resolution: u32,
672    /// How often the far cascades (the farther half of them) are drawn
673    /// again, in frames, 1 to 8. At 1 every cascade is drawn every frame. Above
674    /// 1 the far ones take turns: each is drawn into a box a little bigger
675    /// than it needs and kept for up to this many frames, or until the
676    /// camera leaves its box or the sun moves. Far shadows of moving things
677    /// then lag by up to that many frames. Default 2: half the far
678    /// cascades' drawing.
679    pub shadow_refresh: u32,
680    /// How soft the sun's shadow edges are, 1 to 3: how many lookups each
681    /// lit pixel takes in the shadow map. 1 is one smooth lookup (2x2
682    /// texels), the cheapest; 2 is four (3x3 texels); 3 is nine (4x4
683    /// texels), the softest. Default 3. Each step costs about as much as the
684    /// lookups it adds, on every lit pixel.
685    pub shadow_filter: u32,
686    /// How many shadow views point and spot lights may share, 0 to
687    /// [`MAX_LIGHT_SHADOW_VIEWS`]: a shadowed spot light takes one, a
688    /// shadowed point light six. 0 turns point and spot light shadows off.
689    /// Default 12.
690    pub light_shadow_views: u32,
691    /// Width and height of the largest point or spot light shadow view, in
692    /// texels: 128 to 2048, rounded down to a power of two. A light whose
693    /// reach fills the view gets this; one twice as far away half of it,
694    /// down to 64 (see [`PointLight3D::shadow_resolution`] to pick a
695    /// light's own). Default [`LIGHT_SHADOW_SIZE`].
696    pub light_shadow_resolution: u32,
697    /// Width and height of the depth atlas all point and spot light shadow
698    /// views are packed into, in texels: 1024 to 8192, rounded down to a
699    /// power of two (and no more than the GPU allows). It takes
700    /// `4 * light_shadow_atlas²` bytes once a light casts shadows; when
701    /// the views don't fit, the largest shrink. Changing it at run time
702    /// makes a new atlas. Default [`LIGHT_SHADOW_ATLAS_SIZE`].
703    pub light_shadow_atlas: u32,
704    /// Light bounced off the world's surfaces (global illumination). Off
705    /// by default; see [`GlobalIllumination`].
706    pub gi: GlobalIllumination,
707}
708
709impl Default for Lighting {
710    fn default() -> Self {
711        Self {
712            sun_direction: Vec3::new(-0.4, -1.0, -0.3),
713            sun_color: Color::hex(0xFFF4E0),
714            sun_intensity: 3.0,
715            sky_color: Color::hex(0x869BB0),
716            ground_color: Color::hex(0x4D4135),
717            equator_color: None,
718            ambient_intensity: 1.0,
719            shadows: true,
720            shadow_distance: 60.0,
721            shadow_cascades: 4,
722            shadow_resolution: SHADOW_MAP_SIZE,
723            shadow_refresh: 2,
724            shadow_filter: 3,
725            light_shadow_views: 12,
726            light_shadow_resolution: LIGHT_SHADOW_SIZE,
727            light_shadow_atlas: LIGHT_SHADOW_ATLAS_SIZE,
728            gi: GlobalIllumination::default(),
729        }
730    }
731}
732
733impl Lighting {
734    /// The sun's shadow map size, clamped to what the GPU can make (`limit`
735    /// 0: no limit known).
736    pub(crate) fn shadow_size(&self, limit: u32) -> u32 {
737        let limit = if limit == 0 { u32::MAX } else { limit };
738        self.shadow_resolution
739            .clamp(MIN_SHADOW_RESOLUTION, MAX_SHADOW_RESOLUTION)
740            .min(limit)
741    }
742    /// The point and spot lights' shadow view size, clamped likewise.
743    pub(crate) fn light_shadow_size(&self, limit: u32) -> u32 {
744        let limit = if limit == 0 { u32::MAX } else { limit };
745        self.light_shadow_resolution
746            .clamp(MIN_LIGHT_SHADOW_RESOLUTION, MAX_LIGHT_SHADOW_RESOLUTION)
747            .min(limit)
748    }
749    /// The lights' shadow atlas size, clamped likewise.
750    pub(crate) fn light_shadow_atlas_size(&self, limit: u32) -> u32 {
751        let limit = if limit == 0 { u32::MAX } else { limit };
752        let size = self
753            .light_shadow_atlas
754            .clamp(MIN_LIGHT_SHADOW_ATLAS, MAX_LIGHT_SHADOW_ATLAS)
755            .min(limit);
756        shadow_atlas::floor_pow2(size)
757    }
758    /// How many light shadow views are allowed this frame.
759    pub(crate) fn light_shadow_budget(&self) -> usize {
760        (self.light_shadow_views as usize).min(MAX_LIGHT_SHADOW_VIEWS)
761    }
762}
763
764/// This frame's 3D scene, as the renderer gets it.
765pub(crate) struct Scene3D<'a> {
766    pub frame: &'a Frame3D,
767    pub lighting: &'a Lighting,
768    pub sky: &'a Sky,
769    /// The sun and ambient light (from the sky, with an atmosphere).
770    pub light: SkyLight,
771    pub camera: Vec3,
772    pub post: &'a Post,
773    /// Seconds since the game started, and since the last frame.
774    pub time: (f32, f32),
775    /// Whether to make the depth grid occlusion culling reads.
776    pub occlusion: bool,
777}
778
779/// What the engine keeps about each mesh and material on the CPU side: for
780/// culling, and for headless runs, which have no GPU.
781#[derive(Default)]
782pub(crate) struct Assets3D {
783    pub morph_entities: Vec<crate::Entity>,
784    pub morph_bases: std::collections::HashMap<crate::Entity, (u32, MorphWeights, MorphWeights)>,
785    /// Bounding sphere of each mesh, in its own space: center and radius.
786    pub bounds: Vec<(Vec3, f32)>,
787    /// Freed meshes' ids, for the next meshes made.
788    pub free_meshes: Vec<u32>,
789    pub probes: probe::ProbeAssets,
790    /// How each skinned mesh binds to joints, by mesh id; shorter than
791    /// `bounds` when the last ones aren't skinned.
792    pub skins: Vec<Option<std::sync::Arc<MeshSkin>>>,
793    /// Each mesh's morph targets, by mesh id; shorter than `bounds` when
794    /// the last ones have none.
795    pub morphs: Vec<Option<std::sync::Arc<MeshMorph>>>,
796    /// Shared animation clips of skinned glTF files, by resolved path.
797    pub loaded_clips: std::collections::HashMap<std::path::PathBuf, Vec<AnimationClip>>,
798    /// The joints of skinned glTF files, by resolved path.
799    pub loaded_skeletons: std::collections::HashMap<std::path::PathBuf, Skeleton>,
800    /// Meshes set to cast no shadow (`Ctx::set_mesh_shadows`), by id;
801    /// shorter than `bounds` when the last ones cast.
802    pub shadowless: Vec<bool>,
803    pub materials: u32,
804    /// Texture arrays made so far, by id.
805    pub arrays: Vec<TextureArray>,
806    /// Whether each was made as `TextureKind::Data`, by id.
807    pub array_data: Vec<bool>,
808    /// The texture array decals take their images from.
809    pub decal_array: Option<TextureArray>,
810    /// Whether the renderer can draw projected decals (its GPU has a spare
811    /// texture slot); set each frame.
812    pub decals_supported: bool,
813    /// Whether the renderer can draw global illumination (compute shaders
814    /// and a texture slot for the probes); set each frame.
815    pub gi_supported: bool,
816    /// Render targets made so far.
817    pub render_targets: Vec<crate::Texture>,
818    /// Screen effects made so far.
819    pub effects: u32,
820    /// Compute passes made so far, and those loaded from files.
821    pub computes: u32,
822    /// GPU buffers' lengths, by id; 0 for a freed one.
823    pub gpu_buffers: Vec<u32>,
824    pub instance_batches: Vec<Option<instances::Batch>>,
825    /// Freed GPU buffers' ids, for the next buffers made.
826    pub free_gpu_buffers: Vec<u32>,
827    /// Buffer-driven meshes' own bounds (before `set_mesh_gpu_bounds`
828    /// grew them), by mesh id.
829    pub gpu_mesh_bounds: std::collections::HashMap<u32, (Vec3, f32)>,
830    pub loaded_computes: std::collections::HashMap<std::path::PathBuf, Compute>,
831    /// Effects loaded from files, by resolved path.
832    pub loaded_effects: std::collections::HashMap<std::path::PathBuf, Effect>,
833    /// The built-in terrain shader, once a terrain material needs it.
834    pub terrain_shader: Option<Shader>,
835    /// Shaders made so far (the built-in one is not counted).
836    pub shaders: u32,
837    /// Shaders loaded from files, by resolved path.
838    pub loaded_shaders: std::collections::HashMap<std::path::PathBuf, Shader>,
839    /// The material handed out for meshes without one.
840    pub default_material: Option<Material>,
841    /// Meshes loaded from files, by resolved path.
842    pub loaded: std::collections::HashMap<std::path::PathBuf, Mesh>,
843    /// Material handles survive geometry eviction; reloading must preserve
844    /// user shader edits and must not allocate another material/texture set.
845    pub file_materials: std::collections::HashMap<std::path::PathBuf, Vec<Material>>,
846    /// Images that glTF files take from files of their own, by asset name
847    /// (see `gltf::Loaded::image_sources`): many models share them.
848    pub shared_images: std::collections::HashMap<String, crate::Texture>,
849    /// Materials loaded from glTF files, by their name in the file, with
850    /// what they were made from (`Ctx::loaded_materials`).
851    pub loaded_materials: std::collections::HashMap<String, Vec<(Material, MaterialDesc)>>,
852}
853
854impl Assets3D {
855    /// Mesh `id`'s bounds, unless there's no such mesh or it was freed.
856    pub(crate) fn live_bounds(&self, id: u32) -> Option<(Vec3, f32)> {
857        self.bounds.get(id as usize).copied().filter(|&(_, r)| r >= 0.0)
858    }
859
860    /// How mesh `id` binds to joints, if it's skinned.
861    pub(crate) fn skin(&self, id: u32) -> Option<&MeshSkin> {
862        self.skins.get(id as usize)?.as_deref()
863    }
864
865    /// Mesh `id`'s morph targets, if it has any.
866    pub(crate) fn morph(&self, id: u32) -> Option<&MeshMorph> {
867        self.morphs.get(id as usize)?.as_deref()
868    }
869
870    /// Whether mesh `id` casts shadows (the default).
871    pub(crate) fn casts_shadows(&self, id: u32) -> bool {
872        !self.shadowless.get(id as usize).copied().unwrap_or(false)
873    }
874}
875
876/// sRGB color to linear RGBA, for the GPU.
877pub(crate) fn linear(c: Color) -> [f32; 4] {
878    // Most tints are white: skip the three `powf`s (this runs per instance).
879    if c.r == 1.0 && c.g == 1.0 && c.b == 1.0 {
880        return [1.0, 1.0, 1.0, c.a];
881    }
882    let lin = |v: f32| {
883        if v <= 0.04045 {
884            v / 12.92
885        } else {
886            ((v + 0.055) / 1.055).powf(2.4)
887        }
888    };
889    [lin(c.r), lin(c.g), lin(c.b), c.a]
890}
891
892#[cfg(test)]
893mod tests {
894    use super::*;
895
896    #[test]
897    fn looking_at_points_minus_z_at_the_target() {
898        for target in [Vec3::X, Vec3::NEG_X, Vec3::new(1.0, 2.0, -3.0), Vec3::Y, Vec3::NEG_Y] {
899            let t = Transform3D::default().looking_at(target);
900            assert!((t.forward() - target.normalize()).length() < 1e-5, "{target}");
901        }
902    }
903
904    #[test]
905    fn the_screen_center_ray_goes_where_the_camera_looks() {
906        let mut camera = Camera3D::new(Vec2::new(800.0, 600.0));
907        camera.position = Vec3::new(3.0, 4.0, 5.0);
908        camera.look_at(Vec3::ZERO);
909        let (from, dir) = camera.screen_ray(Vec2::new(400.0, 300.0));
910        assert_eq!(from, camera.position);
911        assert!((dir - camera.forward()).length() < 1e-4);
912        // Left of center points left of forward.
913        let (_, left) = camera.screen_ray(Vec2::new(0.0, 300.0));
914        let right_axis = camera.rotation * Vec3::X;
915        assert!(left.dot(right_axis) < 0.0);
916    }
917
918    #[test]
919    fn the_view_projection_puts_the_target_in_the_middle() {
920        let mut camera = Camera3D::new(Vec2::new(800.0, 600.0));
921        camera.position = Vec3::new(0.0, 10.0, 10.0);
922        camera.look_at(Vec3::new(1.0, 0.0, 0.0));
923        let clip = camera.view_proj() * Vec4::new(1.0, 0.0, 0.0, 1.0);
924        let ndc = clip.truncate() / clip.w;
925        assert!(
926            ndc.x.abs() < 1e-4 && ndc.y.abs() < 1e-4 && (0.0..1.0).contains(&ndc.z),
927            "{ndc}"
928        );
929    }
930}