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}