ponko 0.0.1

A fast 2D game engine for simulating millions of things: ECS on all cores, hot reload, an in-game inspector, AI-agent control, and a C++ API.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
//! 3D: meshes with materials, drawn from entities with a [`Transform3D`] and
//! a [`Model`], through a perspective [`Camera3D`], lit by a sun and ambient
//! light ([`Lighting`]). Behind the `3d` feature.
//!
//! The 3D pass is drawn first, with a depth buffer; 2D sprites, text and UI
//! go on top of it. Games without 3D models never run it, and builds without
//! the feature don't have it at all.
//!
//! Coordinates are right-handed with Y up (as in glTF): a camera with no
//! rotation looks down -Z.

mod animator;
mod api;
mod clip;
mod collect;
mod compute;
mod effect;
pub(crate) mod gi;
mod gltf;
mod ik;
pub(crate) mod instances;
pub use instances::{InstanceBatch, InstanceBatchOptions};
mod lod;
pub(crate) mod occlusion;
pub use gi::{GlobalIllumination, MAX_GI_PROBES, MAX_GI_PROBES_PER_FRAME};
pub use ik::{look_at_joint, two_bone_ik};
pub use lod::{LodGroup, LodLevel, LodPick, LodRenderer, LodSettings, LodShadow};
pub(crate) mod lights;
mod mesh;
pub use lights::{LIGHT_SHADOW_ATLAS_SIZE, LIGHT_SHADOW_SIZE, MAX_LIGHT_SHADOW_VIEWS, PointLight3D, SpotLight3D};
pub(crate) use lights::{MAX_LIGHT_SHADOW_RESOLUTION, MIN_LIGHT_SHADOW_RESOLUTION};
pub(crate) mod shadow_atlas;
pub(crate) use shadow_atlas::{MAX_LIGHT_SHADOW_ATLAS, MIN_LIGHT_SHADOW_ATLAS};
pub(crate) mod particles;
pub use particles::{
    ParticleBlend3D, ParticleCurve, ParticleEmitter3D, ParticleGradient, ParticleSettings3D, ParticleShape3D,
    ParticleSpace3D,
};
mod decal;
mod post;
pub(crate) mod projected;
pub use projected::{Decal3D, MAX_DECALS, MAX_DECALS_PER_CLUSTER, ReceivesDecals};
pub(crate) mod probe;
pub use probe::ReflectionProbe3D;
mod quality;
mod skin;
pub(crate) mod strips;
pub use strips::{Line3D, Trail3D};
pub(crate) mod text3d;
pub use text3d::{Text3D, TextAlign3D};
mod far_terrain;
mod sky;
mod target;
mod terrain;
pub use far_terrain::FarTerrain;
mod texture_array;

pub use animator::{
    AnimationCondition, AnimationEvent, AnimationGraph, AnimationLayer, AnimationParameter, AnimationState,
    AnimationTransition, Animator, JointMask, LayerBlend, Motion, MotionChild, RootMotion,
};
pub(crate) use api::GpuMaterialDesc;
#[cfg(feature = "window")]
pub(crate) use api::MaterialTextures;
pub use clip::{AnimationClip, AnimationTrack, Interpolation, JointAnimation, MorphAnimation};
pub(crate) mod motion;
#[cfg(feature = "window")]
pub(crate) use collect::{FADED, Instance3D, MAX_CASCADES, MAX_SHADOW_VIEWS, shadow_views};
pub(crate) use collect::{Frame3D, MAX_SHADOW_RESOLUTION, MIN_SHADOW_RESOLUTION, SHADOW_MAP_SIZE};
pub use compute::{Compute, ComputePass, GpuBuffer};
pub use effect::{Effect, ScreenEffect};
pub use mesh::{MeshData, MorphFrame, MorphTarget};
pub use post::{AmbientOcclusion, AntiAliasing, MAX_RENDER_SCALE, MIN_RENDER_SCALE, Post, Tonemap};
#[cfg(feature = "window")]
pub(crate) use post::{PostUniform, scaled_size};
pub use quality::{GraphicsQuality, QualityLevel};
#[cfg(feature = "window")]
pub(crate) use skin::blend as blend_skin;
pub use skin::{AttachToJoint, Joint, MorphWeights, Skeleton, SkinOf};
pub(crate) use skin::{MeshMorph, MeshSkin, SkinFrame};
#[cfg(all(test, feature = "window"))]
pub(crate) use skin::{SkinDraw, normalized as normalized_weights};
pub use sky::Sky;
pub(crate) use sky::SkyLight;
pub use target::TargetCamera;
pub use terrain::{Heightfield, TERRAIN_LAYERS, TERRAIN_LODS, TerrainMaterialDesc, TerrainTile, TerrainTileData};
pub use texture_array::{TextureArray, TextureKind};

use glam::{Mat3, Mat4, Quat, Vec2, Vec3, Vec4};

use crate::{Color, Texture};

/// Position, rotation and scale of a 3D entity.
///
/// ```
/// # use ponko::*;
/// let crate_box = Transform3D::at(vec3(0.0, 0.5, -3.0)).rotated(Quat::from_rotation_y(0.3));
/// # let _ = crate_box;
/// ```
#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
#[serde(default)]
#[repr(C, align(16))] // shared with C/C++ (`CrabbyTransform3D`): 48 bytes everywhere
pub struct Transform3D {
    /// Which way it faces.
    pub rotation: Quat,
    /// Where it is, in world units.
    pub position: Vec3,
    /// Size along its own axes. Default `(1, 1, 1)`.
    pub scale: Vec3,
}

impl Default for Transform3D {
    fn default() -> Self {
        Self {
            position: Vec3::ZERO,
            rotation: Quat::IDENTITY,
            scale: Vec3::ONE,
        }
    }
}

impl Transform3D {
    /// At `position`, not rotated, scale 1.
    ///
    /// ```
    /// # use ponko::*;
    /// let t = Transform3D::at(vec3(1.0, 2.0, 3.0));
    /// assert_eq!(t.scale, Vec3::ONE);
    /// ```
    #[must_use]
    pub fn at(position: Vec3) -> Self {
        Self {
            position,
            ..Self::default()
        }
    }

    /// The same, rotated by `rotation`.
    ///
    /// ```
    /// # use ponko::*;
    /// let t = Transform3D::default().rotated(Quat::from_rotation_y(1.0));
    /// # let _ = t;
    /// ```
    #[must_use]
    pub fn rotated(self, rotation: Quat) -> Self {
        Self { rotation, ..self }
    }

    /// The same, scaled by `scale` on every axis.
    ///
    /// ```
    /// # use ponko::*;
    /// assert_eq!(Transform3D::default().scaled(2.0).scale, Vec3::splat(2.0));
    /// ```
    #[must_use]
    pub fn scaled(self, scale: f32) -> Self {
        Self {
            scale: Vec3::splat(scale),
            ..self
        }
    }

    /// The same, turned so its -Z axis points at `target` (with +Y as up).
    ///
    /// ```
    /// # use ponko::*;
    /// let t = Transform3D::at(vec3(0.0, 0.0, 5.0)).looking_at(Vec3::ZERO);
    /// assert!((t.forward() - vec3(0.0, 0.0, -1.0)).length() < 1e-5);
    /// ```
    #[must_use]
    pub fn looking_at(self, target: Vec3) -> Self {
        Self {
            rotation: look_rotation(target - self.position),
            ..self
        }
    }

    /// The direction it faces: its -Z axis.
    ///
    /// ```
    /// # use ponko::*;
    /// assert_eq!(Transform3D::default().forward(), vec3(0.0, 0.0, -1.0));
    /// ```
    #[must_use]
    pub fn forward(&self) -> Vec3 {
        self.rotation * Vec3::NEG_Z
    }

    /// The matrix from its local space to the world.
    ///
    /// ```
    /// # use ponko::*;
    /// let m = Transform3D::at(vec3(1.0, 0.0, 0.0)).matrix();
    /// assert_eq!(m.transform_point3(Vec3::ZERO), vec3(1.0, 0.0, 0.0));
    /// ```
    #[must_use]
    pub fn matrix(&self) -> Mat4 {
        Mat4::from_scale_rotation_translation(self.scale, self.rotation, self.position)
    }
}

/// The rotation that turns -Z towards `direction`, keeping +Y up (or +Z up
/// when looking straight up or down).
fn look_rotation(direction: Vec3) -> Quat {
    let forward = direction.try_normalize().unwrap_or(Vec3::NEG_Z);
    let up = if forward.y.abs() > 0.999 { Vec3::Z } else { Vec3::Y };
    let right = forward.cross(up).normalize();
    let up = right.cross(forward);
    Quat::from_mat3(&Mat3::from_cols(right, up, -forward))
}

/// A mesh on the GPU: triangles, split into parts that each have a
/// [`Material`]. Made with [`Ctx::create_mesh`](crate::Ctx::create_mesh) or
/// loaded with [`Ctx::load_mesh`](crate::Ctx::load_mesh); a cheap handle.
///
/// ```no_run
/// # use ponko::*;
/// # fn f(ctx: &mut Ctx) {
/// let tree: Mesh = ctx.load_mesh("tree.glb");
/// ctx.world.spawn((Transform3D::default(), Model::new(tree)));
/// # }
/// ```
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(C)] // shared with C/C++ (`CrabbyMesh`)
pub struct Mesh {
    pub(crate) id: u32,
}

impl Mesh {
    /// No mesh: a [`Model`] showing it draws nothing and casts no shadow, at
    /// almost no cost. For models that are sometimes hidden, such as
    /// plants beyond their draw distance. Unlike a freed mesh, it never
    /// becomes another mesh, since no mesh is ever made with its id.
    ///
    /// ```
    /// # use ponko::*;
    /// Game::new("t", 1, 1).run_headless(0, |ctx| {
    ///     let material = ctx.default_material();
    ///     let bush = ctx.create_mesh(&MeshData::cube(1.0), material);
    ///     ctx.world.spawn((Transform3D::default(), Model::new(Mesh::NONE)));
    ///     assert!(!ctx.is_mesh_live(Mesh::NONE));
    ///     assert_ne!(bush, Mesh::NONE);
    /// });
    /// ```
    pub const NONE: Mesh = Mesh { id: u32::MAX };
}

/// What one mesh cost to draw in the last frame, from
/// [`Ctx::mesh_costs`](crate::Ctx::mesh_costs).
#[derive(Debug, Clone, PartialEq)]
pub struct MeshCost {
    /// The mesh.
    pub mesh: Mesh,
    /// The file it was loaded from, if it came from one (glTF meshes are
    /// `file.glb#index`-style paths as they were loaded).
    pub name: Option<String>,
    /// Models the camera drew with it.
    pub models: usize,
    /// Triangles the camera's passes drew with it.
    pub triangles: usize,
    /// Models drawn with it into the shadow maps, all cascades together.
    pub shadow_models: usize,
    /// Triangles drawn with it into the shadow maps, all cascades together.
    pub shadow_triangles: usize,
}

/// How a surface looks: its color (and texture), how rough and how metallic
/// it is, and light it gives off. Made with
/// [`Ctx::create_material`](crate::Ctx::create_material); a cheap handle.
///
/// ```no_run
/// # use ponko::*;
/// # fn f(ctx: &mut Ctx) {
/// let gold = ctx.create_material(MaterialDesc { metallic: 1.0, roughness: 0.3, ..MaterialDesc::color(Color::hex(0xFFC83D)) });
/// # }
/// ```
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(C)] // shared with C/C++ (`CrabbyMaterial`)
pub struct Material {
    pub(crate) id: u32,
}

/// A material shader written in WGSL: made with
/// [`Ctx::load_shader`](crate::Ctx::load_shader) or
/// [`Ctx::create_shader`](crate::Ctx::create_shader); a cheap handle. See
/// the 3D chapter of the guide for what a shader can do.
///
/// ```no_run
/// # use ponko::*;
/// # fn f(ctx: &mut Ctx) {
/// let water = ctx.load_shader("shaders/water.wgsl");
/// let lake = ctx.create_material(MaterialDesc { shader: Some(water), ..Default::default() });
/// # }
/// ```
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(C)] // shared with C/C++ (`CrabbyShader`)
pub struct Shader {
    pub(crate) id: u32,
}

impl Shader {
    /// The engine's id of this shader (for the C API).
    #[doc(hidden)]
    #[must_use]
    pub fn id(self) -> u32 {
        self.id
    }

    /// A shader handle from an id (for the C API; check it with `Ctx::has_shader`).
    #[doc(hidden)]
    #[must_use]
    pub fn from_id(id: u32) -> Self {
        Self { id }
    }
}

/// How a material covers what's behind it.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[repr(u32)]
pub enum Blend {
    /// Solid, whatever its alpha. The fastest: hidden pixels and shadows
    /// skip the material's shader.
    #[default]
    Opaque = 0,
    /// See-through by its alpha (glass, water). Drawn after everything
    /// solid, casts no shadow; overlapping transparent models are not
    /// sorted, so one may show over another that is in front of it.
    Transparent = 1,
    /// Solid, with pixels whose alpha is below 0.5 cut out (leaves,
    /// fences, grass cards). Their shadows have the same holes.
    Cutout = 2,
}

/// What a [`Material`] is made from.
///
/// ```
/// # use ponko::*;
/// let grass = MaterialDesc { roughness: 0.9, ..MaterialDesc::color(Color::hex(0x4C8A3A)) };
/// assert_eq!(grass.metallic, 0.0);
/// ```
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct MaterialDesc {
    /// Base color, multiplied with the texture. Default white.
    pub color: Color,
    /// An image on the surface (by the mesh's texture coordinates).
    pub texture: Option<Texture>,
    /// 0 is mirror-smooth, 1 is fully rough (matte). Default 0.6.
    pub roughness: f32,
    /// 0 for most things, 1 for bare metal. Default 0.
    pub metallic: f32,
    /// Light it gives off itself (not lighting anything else). Default black.
    pub emissive: Color,
    /// A normal map: surface detail (bumps, grooves) as tangent-space
    /// normals, OpenGL style (green up), as glTF has them.
    pub normal_map: Option<Texture>,
    /// How strong the normal map is. Default 1.
    pub normal_strength: f32,
    /// An occlusion-roughness-metallic map, as glTF has it: red is ambient
    /// occlusion, green multiplies `roughness`, blue multiplies `metallic`.
    pub orm_map: Option<Texture>,
    /// Multiplies `emissive` (which must not be black for it to show).
    pub emissive_map: Option<Texture>,
    /// How it covers what's behind it. Default [`Blend::Opaque`].
    pub blend: Blend,
    /// Draws the back of each triangle too (leaves, cloth, planes seen from
    /// below). Default false.
    pub double_sided: bool,
    /// A custom shader; `None` is the built-in one.
    pub shader: Option<Shader>,
    /// Values for a custom shader: `material.custom[0]` to `[3]` there.
    pub custom: [Vec4; 4],
    /// Textures for a custom shader: `custom_texture0` and
    /// `custom_texture1` there.
    pub custom_textures: [Option<Texture>; 2],
    /// Texture arrays for a custom shader: `custom_array0` to
    /// `custom_array2` there (`texture_2d_array<f32>`), each read as its
    /// [`TextureKind`] says.
    pub custom_arrays: [Option<TextureArray>; 3],
}

impl Default for MaterialDesc {
    fn default() -> Self {
        Self {
            color: Color::WHITE,
            texture: None,
            roughness: 0.6,
            metallic: 0.0,
            emissive: Color::rgb(0.0, 0.0, 0.0),
            normal_map: None,
            normal_strength: 1.0,
            orm_map: None,
            emissive_map: None,
            blend: Blend::Opaque,
            double_sided: false,
            shader: None,
            custom: [Vec4::ZERO; 4],
            custom_textures: [None; 2],
            custom_arrays: [None; 3],
        }
    }
}

impl MaterialDesc {
    /// A plain material of one color.
    ///
    /// ```
    /// # use ponko::*;
    /// let red = MaterialDesc::color(Color::rgb(0.8, 0.1, 0.1));
    /// # let _ = red;
    /// ```
    #[must_use]
    pub fn color(color: Color) -> Self {
        Self {
            color,
            ..Self::default()
        }
    }
}

/// Draws a [`Mesh`] where the entity's [`Transform3D`] is.
///
/// ```no_run
/// # use ponko::*;
/// # fn f(ctx: &mut Ctx) {
/// let white = ctx.default_material();
/// let cube = ctx.create_mesh(&MeshData::cube(1.0), white);
/// for i in 0..10 {
///     let tint = Color::rgb(i as f32 / 10.0, 0.5, 1.0);
///     ctx.world.spawn((Transform3D::at(vec3(i as f32 * 2.0, 0.0, 0.0)), Model::new(cube).with_color(tint)));
/// }
/// # }
/// ```
#[derive(Debug, Clone, Copy, PartialEq)]
#[repr(C)] // shared with C/C++ (`CrabbyModel`)
pub struct Model {
    /// What to draw.
    pub mesh: Mesh,
    /// Multiplied with the materials' colors, so one mesh can come in many
    /// colors without more materials. Default white (no change).
    pub color: Color,
}

/// Draws a different mesh into the shadow maps than the entity's [`Model`]
/// shows: a cheap stand-in for a detailed model's shadow (a simplified copy,
/// a billboard), or `None` for no shadow from this entity. Without it, the
/// model's own mesh casts the shadow (unless
/// [`Ctx::set_mesh_shadows`](crate::Ctx::set_mesh_shadows) turned that
/// mesh's shadows off).
///
/// Far from the camera, a shadow is a few texels of the shadow map, so a
/// mesh with a tenth of the triangles casts the same shadow for a tenth of
/// the cost. Swap it by distance, as you would the model's own detail.
///
/// An entity with a `ShadowMesh` and no [`Model`] casts only the shadow: a
/// stand-in for the shadow of several models around it that cast none. On
/// a [`TerrainTile`], `ShadowMesh(None)` turns the ground's shadow off.
///
/// In scene files it's `"ShadowMesh": { "mesh": "tree_shadow.glb" }`, or
/// `{ "mesh": null }` for no shadow.
///
/// ```no_run
/// # use ponko::*;
/// # fn f(ctx: &mut Ctx) {
/// let tree = ctx.load_mesh("tree.glb");
/// let blob = ctx.load_mesh("tree_shadow.glb"); // a few dozen triangles
/// ctx.world.spawn((Transform3D::default(), Model::new(tree), ShadowMesh(Some(blob))));
/// # }
/// ```
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ShadowMesh(pub Option<Mesh>);

impl Model {
    /// Draws `mesh` as it is.
    ///
    /// ```no_run
    /// # use ponko::*;
    /// # fn f(ctx: &mut Ctx) {
    /// let rock = ctx.load_mesh("rock.glb");
    /// ctx.world.spawn((Transform3D::default(), Model::new(rock)));
    /// # }
    /// ```
    #[must_use]
    pub fn new(mesh: Mesh) -> Self {
        Self {
            mesh,
            color: Color::WHITE,
        }
    }

    /// The same, tinted with `color`.
    ///
    /// ```no_run
    /// # use ponko::*;
    /// # fn f(ctx: &mut Ctx, cube: Mesh) {
    /// let red_cube = Model::new(cube).with_color(Color::rgb(1.0, 0.2, 0.2));
    /// # }
    /// ```
    #[must_use]
    pub fn with_color(self, color: Color) -> Self {
        Self { color, ..self }
    }
}

/// The 3D camera: where it is, which way it looks, and how wide it sees.
/// One per game, at [`Ctx::camera3d`](crate::Ctx::camera3d).
///
/// ```no_run
/// # use ponko::*;
/// # fn f(ctx: &mut Ctx) {
/// ctx.camera3d.position = vec3(0.0, 4.0, 10.0);
/// ctx.camera3d.look_at(Vec3::ZERO);
/// # }
/// ```
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Camera3D {
    /// Where the camera is.
    pub position: Vec3,
    /// Which way it looks: it sees along its -Z axis.
    pub rotation: Quat,
    /// How tall the view is, as an angle in radians. Default 60°.
    pub fov_y: f32,
    /// Nothing closer than this is drawn. Default 0.1.
    pub near: f32,
    /// Nothing farther than this is drawn. Default 1000.
    pub far: f32,
    /// Skip models hidden behind what was drawn a frame or two before.
    /// Default off.
    ///
    /// The renderer keeps the farthest depth of small screen tiles and reads
    /// it back without waiting for the GPU; a model whose bounds lie behind
    /// it everywhere they cover isn't drawn (it still casts shadows). A model
    /// that comes out from behind something can show up a frame or two late,
    /// mostly when the camera moves fast. Needs compute shaders: on WebGL2
    /// nothing is culled.
    pub occlusion_culling: bool,
    /// Window size in pixels, kept up to date by the engine.
    viewport: Vec2,
}

impl Camera3D {
    pub(crate) fn new(viewport: Vec2) -> Self {
        Self {
            position: Vec3::new(0.0, 2.0, 8.0),
            rotation: look_rotation(Vec3::new(0.0, -2.0, -8.0)),
            fov_y: 60f32.to_radians(),
            near: 0.1,
            far: 1000.0,
            occlusion_culling: false,
            viewport,
        }
    }

    pub(crate) fn set_viewport(&mut self, viewport: Vec2) {
        self.viewport = viewport;
    }

    /// Turns the camera to look at `target`, keeping +Y up.
    ///
    /// ```no_run
    /// # use ponko::*;
    /// # fn f(ctx: &mut Ctx, player: Vec3) {
    /// ctx.camera3d.position = player + vec3(0.0, 6.0, 8.0);
    /// ctx.camera3d.look_at(player);
    /// # }
    /// ```
    pub fn look_at(&mut self, target: Vec3) {
        self.rotation = look_rotation(target - self.position);
    }

    /// The direction the camera looks.
    ///
    /// ```no_run
    /// # use ponko::*;
    /// # fn f(ctx: &mut Ctx) {
    /// let ahead = ctx.camera3d.position + ctx.camera3d.forward() * 5.0;
    /// # }
    /// ```
    #[must_use]
    pub fn forward(&self) -> Vec3 {
        self.rotation * Vec3::NEG_Z
    }

    /// World to clip space: projection times view.
    #[must_use]
    pub(crate) fn view_proj(&self) -> Mat4 {
        let aspect = (self.viewport.x / self.viewport.y.max(1.0)).max(1e-3);
        let projection = glam::camera::rh::proj::directx::perspective(self.fov_y, aspect, self.near, self.far);
        projection * self.view()
    }

    /// World to the camera's own space.
    pub(crate) fn view(&self) -> Mat4 {
        Mat4::from_rotation_translation(self.rotation, self.position).inverse()
    }

    /// The ray from the camera through window point `screen` (logical
    /// pixels, as [`Ctx::mouse_pos`](crate::Ctx::mouse_pos)): its start and
    /// unit direction. For picking things in the world with the mouse.
    ///
    /// ```no_run
    /// # use ponko::*;
    /// # fn f(ctx: &mut Ctx) {
    /// let (from, dir) = ctx.camera3d.screen_ray(ctx.mouse_pos());
    /// // Where the mouse points on the ground (y = 0):
    /// if dir.y < 0.0 {
    ///     let ground = from + dir * (-from.y / dir.y);
    /// }
    /// # }
    /// ```
    #[must_use]
    pub fn screen_ray(&self, screen: Vec2) -> (Vec3, Vec3) {
        let ndc = Vec2::new(
            screen.x / self.viewport.x.max(1.0) * 2.0 - 1.0,
            1.0 - screen.y / self.viewport.y.max(1.0) * 2.0,
        );
        let inverse = self.view_proj().inverse();
        let far = inverse * Vec4::new(ndc.x, ndc.y, 1.0, 1.0);
        let far = far.truncate() / far.w;
        (self.position, (far - self.position).normalize_or(self.forward()))
    }
}

/// The light in the 3D world: a sun far away (one direction everywhere)
/// and ambient light from all around. At [`Ctx::lighting`](crate::Ctx::lighting).
///
/// ```no_run
/// # use ponko::*;
/// # fn f(ctx: &mut Ctx) {
/// // Evening: low, orange sun.
/// ctx.lighting.sun_direction = vec3(-1.0, -0.3, -0.2);
/// ctx.lighting.sun_color = Color::hex(0xFFB066);
/// # }
/// ```
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Lighting {
    /// The way the sunlight travels (from the sun towards the ground).
    pub sun_direction: Vec3,
    /// The sun's color.
    pub sun_color: Color,
    /// How bright the sun is. Default 3.
    pub sun_intensity: f32,
    /// Light from the sky, on surfaces facing up. Used when
    /// [`Sky::atmosphere`] is off; with it, the sky's own light is used.
    pub sky_color: Color,
    /// Light bounced off the ground, on surfaces facing down. Like
    /// `sky_color`, only without an atmosphere.
    pub ground_color: Color,
    /// Light from the horizon, on surfaces facing sideways: with it, the
    /// ambient light goes from `ground_color` below through this to
    /// `sky_color` above (a three-color gradient); without it (`None`, the
    /// default), straight from ground to sky. Only without an atmosphere.
    pub equator_color: Option<Color>,
    /// How bright the sky and ground light is: 1 is as bright as the sky
    /// makes it. Default 1.
    pub ambient_intensity: f32,
    /// Whether the sun casts shadows, and point and spot lights with
    /// `shadows` set too (off turns every shadow off). Default on.
    pub shadows: bool,
    /// How far from the camera shadows reach, in world units. Default 60.
    pub shadow_distance: f32,
    /// How many shadow maps (cascades) share that distance, 1 to 4: each
    /// covers a stretch further out than the one before, so shadows stay
    /// sharp near the camera however far they reach. Fewer are cheaper.
    /// Default 4.
    pub shadow_cascades: u32,
    /// Width and height of each of the sun's shadow maps, in texels:
    /// 256 to 4096 (and no more than the GPU allows). Doubling it quadruples
    /// the memory (`4 * resolution² * cascades` bytes) and sharpens shadows.
    /// Changing it at run time makes new shadow maps. Default 2048.
    pub shadow_resolution: u32,
    /// How often the far cascades (the farther half of them) are drawn
    /// again, in frames, 1 to 8. At 1 every cascade is drawn every frame. Above
    /// 1 the far ones take turns: each is drawn into a box a little bigger
    /// than it needs and kept for up to this many frames, or until the
    /// camera leaves its box or the sun moves. Far shadows of moving things
    /// then lag by up to that many frames. Default 2: half the far
    /// cascades' drawing.
    pub shadow_refresh: u32,
    /// How soft the sun's shadow edges are, 1 to 3: how many lookups each
    /// lit pixel takes in the shadow map. 1 is one smooth lookup (2x2
    /// texels), the cheapest; 2 is four (3x3 texels); 3 is nine (4x4
    /// texels), the softest. Default 3. Each step costs about as much as the
    /// lookups it adds, on every lit pixel.
    pub shadow_filter: u32,
    /// How many shadow views point and spot lights may share, 0 to
    /// [`MAX_LIGHT_SHADOW_VIEWS`]: a shadowed spot light takes one, a
    /// shadowed point light six. 0 turns point and spot light shadows off.
    /// Default 12.
    pub light_shadow_views: u32,
    /// Width and height of the largest point or spot light shadow view, in
    /// texels: 128 to 2048, rounded down to a power of two. A light whose
    /// reach fills the view gets this; one twice as far away half of it,
    /// down to 64 (see [`PointLight3D::shadow_resolution`] to pick a
    /// light's own). Default [`LIGHT_SHADOW_SIZE`].
    pub light_shadow_resolution: u32,
    /// Width and height of the depth atlas all point and spot light shadow
    /// views are packed into, in texels: 1024 to 8192, rounded down to a
    /// power of two (and no more than the GPU allows). It takes
    /// `4 * light_shadow_atlas²` bytes once a light casts shadows; when
    /// the views don't fit, the largest shrink. Changing it at run time
    /// makes a new atlas. Default [`LIGHT_SHADOW_ATLAS_SIZE`].
    pub light_shadow_atlas: u32,
    /// Light bounced off the world's surfaces (global illumination). Off
    /// by default; see [`GlobalIllumination`].
    pub gi: GlobalIllumination,
}

impl Default for Lighting {
    fn default() -> Self {
        Self {
            sun_direction: Vec3::new(-0.4, -1.0, -0.3),
            sun_color: Color::hex(0xFFF4E0),
            sun_intensity: 3.0,
            sky_color: Color::hex(0x869BB0),
            ground_color: Color::hex(0x4D4135),
            equator_color: None,
            ambient_intensity: 1.0,
            shadows: true,
            shadow_distance: 60.0,
            shadow_cascades: 4,
            shadow_resolution: SHADOW_MAP_SIZE,
            shadow_refresh: 2,
            shadow_filter: 3,
            light_shadow_views: 12,
            light_shadow_resolution: LIGHT_SHADOW_SIZE,
            light_shadow_atlas: LIGHT_SHADOW_ATLAS_SIZE,
            gi: GlobalIllumination::default(),
        }
    }
}

impl Lighting {
    /// The sun's shadow map size, clamped to what the GPU can make (`limit`
    /// 0: no limit known).
    pub(crate) fn shadow_size(&self, limit: u32) -> u32 {
        let limit = if limit == 0 { u32::MAX } else { limit };
        self.shadow_resolution
            .clamp(MIN_SHADOW_RESOLUTION, MAX_SHADOW_RESOLUTION)
            .min(limit)
    }
    /// The point and spot lights' shadow view size, clamped likewise.
    pub(crate) fn light_shadow_size(&self, limit: u32) -> u32 {
        let limit = if limit == 0 { u32::MAX } else { limit };
        self.light_shadow_resolution
            .clamp(MIN_LIGHT_SHADOW_RESOLUTION, MAX_LIGHT_SHADOW_RESOLUTION)
            .min(limit)
    }
    /// The lights' shadow atlas size, clamped likewise.
    pub(crate) fn light_shadow_atlas_size(&self, limit: u32) -> u32 {
        let limit = if limit == 0 { u32::MAX } else { limit };
        let size = self
            .light_shadow_atlas
            .clamp(MIN_LIGHT_SHADOW_ATLAS, MAX_LIGHT_SHADOW_ATLAS)
            .min(limit);
        shadow_atlas::floor_pow2(size)
    }
    /// How many light shadow views are allowed this frame.
    pub(crate) fn light_shadow_budget(&self) -> usize {
        (self.light_shadow_views as usize).min(MAX_LIGHT_SHADOW_VIEWS)
    }
}

/// This frame's 3D scene, as the renderer gets it.
pub(crate) struct Scene3D<'a> {
    pub frame: &'a Frame3D,
    pub lighting: &'a Lighting,
    pub sky: &'a Sky,
    /// The sun and ambient light (from the sky, with an atmosphere).
    pub light: SkyLight,
    pub camera: Vec3,
    pub post: &'a Post,
    /// Seconds since the game started, and since the last frame.
    pub time: (f32, f32),
    /// Whether to make the depth grid occlusion culling reads.
    pub occlusion: bool,
}

/// What the engine keeps about each mesh and material on the CPU side: for
/// culling, and for headless runs, which have no GPU.
#[derive(Default)]
pub(crate) struct Assets3D {
    pub morph_entities: Vec<crate::Entity>,
    pub morph_bases: std::collections::HashMap<crate::Entity, (u32, MorphWeights, MorphWeights)>,
    /// Bounding sphere of each mesh, in its own space: center and radius.
    pub bounds: Vec<(Vec3, f32)>,
    /// Freed meshes' ids, for the next meshes made.
    pub free_meshes: Vec<u32>,
    pub probes: probe::ProbeAssets,
    /// How each skinned mesh binds to joints, by mesh id; shorter than
    /// `bounds` when the last ones aren't skinned.
    pub skins: Vec<Option<std::sync::Arc<MeshSkin>>>,
    /// Each mesh's morph targets, by mesh id; shorter than `bounds` when
    /// the last ones have none.
    pub morphs: Vec<Option<std::sync::Arc<MeshMorph>>>,
    /// Shared animation clips of skinned glTF files, by resolved path.
    pub loaded_clips: std::collections::HashMap<std::path::PathBuf, Vec<AnimationClip>>,
    /// The joints of skinned glTF files, by resolved path.
    pub loaded_skeletons: std::collections::HashMap<std::path::PathBuf, Skeleton>,
    /// Meshes set to cast no shadow (`Ctx::set_mesh_shadows`), by id;
    /// shorter than `bounds` when the last ones cast.
    pub shadowless: Vec<bool>,
    pub materials: u32,
    /// Texture arrays made so far, by id.
    pub arrays: Vec<TextureArray>,
    /// Whether each was made as `TextureKind::Data`, by id.
    pub array_data: Vec<bool>,
    /// The texture array decals take their images from.
    pub decal_array: Option<TextureArray>,
    /// Whether the renderer can draw projected decals (its GPU has a spare
    /// texture slot); set each frame.
    pub decals_supported: bool,
    /// Whether the renderer can draw global illumination (compute shaders
    /// and a texture slot for the probes); set each frame.
    pub gi_supported: bool,
    /// Render targets made so far.
    pub render_targets: Vec<crate::Texture>,
    /// Screen effects made so far.
    pub effects: u32,
    /// Compute passes made so far, and those loaded from files.
    pub computes: u32,
    /// GPU buffers' lengths, by id; 0 for a freed one.
    pub gpu_buffers: Vec<u32>,
    pub instance_batches: Vec<Option<instances::Batch>>,
    /// Freed GPU buffers' ids, for the next buffers made.
    pub free_gpu_buffers: Vec<u32>,
    /// Buffer-driven meshes' own bounds (before `set_mesh_gpu_bounds`
    /// grew them), by mesh id.
    pub gpu_mesh_bounds: std::collections::HashMap<u32, (Vec3, f32)>,
    pub loaded_computes: std::collections::HashMap<std::path::PathBuf, Compute>,
    /// Effects loaded from files, by resolved path.
    pub loaded_effects: std::collections::HashMap<std::path::PathBuf, Effect>,
    /// The built-in terrain shader, once a terrain material needs it.
    pub terrain_shader: Option<Shader>,
    /// Shaders made so far (the built-in one is not counted).
    pub shaders: u32,
    /// Shaders loaded from files, by resolved path.
    pub loaded_shaders: std::collections::HashMap<std::path::PathBuf, Shader>,
    /// The material handed out for meshes without one.
    pub default_material: Option<Material>,
    /// Meshes loaded from files, by resolved path.
    pub loaded: std::collections::HashMap<std::path::PathBuf, Mesh>,
    /// Material handles survive geometry eviction; reloading must preserve
    /// user shader edits and must not allocate another material/texture set.
    pub file_materials: std::collections::HashMap<std::path::PathBuf, Vec<Material>>,
    /// Images that glTF files take from files of their own, by asset name
    /// (see `gltf::Loaded::image_sources`): many models share them.
    pub shared_images: std::collections::HashMap<String, crate::Texture>,
    /// Materials loaded from glTF files, by their name in the file, with
    /// what they were made from (`Ctx::loaded_materials`).
    pub loaded_materials: std::collections::HashMap<String, Vec<(Material, MaterialDesc)>>,
}

impl Assets3D {
    /// Mesh `id`'s bounds, unless there's no such mesh or it was freed.
    pub(crate) fn live_bounds(&self, id: u32) -> Option<(Vec3, f32)> {
        self.bounds.get(id as usize).copied().filter(|&(_, r)| r >= 0.0)
    }

    /// How mesh `id` binds to joints, if it's skinned.
    pub(crate) fn skin(&self, id: u32) -> Option<&MeshSkin> {
        self.skins.get(id as usize)?.as_deref()
    }

    /// Mesh `id`'s morph targets, if it has any.
    pub(crate) fn morph(&self, id: u32) -> Option<&MeshMorph> {
        self.morphs.get(id as usize)?.as_deref()
    }

    /// Whether mesh `id` casts shadows (the default).
    pub(crate) fn casts_shadows(&self, id: u32) -> bool {
        !self.shadowless.get(id as usize).copied().unwrap_or(false)
    }
}

/// sRGB color to linear RGBA, for the GPU.
pub(crate) fn linear(c: Color) -> [f32; 4] {
    // Most tints are white: skip the three `powf`s (this runs per instance).
    if c.r == 1.0 && c.g == 1.0 && c.b == 1.0 {
        return [1.0, 1.0, 1.0, c.a];
    }
    let lin = |v: f32| {
        if v <= 0.04045 {
            v / 12.92
        } else {
            ((v + 0.055) / 1.055).powf(2.4)
        }
    };
    [lin(c.r), lin(c.g), lin(c.b), c.a]
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn looking_at_points_minus_z_at_the_target() {
        for target in [Vec3::X, Vec3::NEG_X, Vec3::new(1.0, 2.0, -3.0), Vec3::Y, Vec3::NEG_Y] {
            let t = Transform3D::default().looking_at(target);
            assert!((t.forward() - target.normalize()).length() < 1e-5, "{target}");
        }
    }

    #[test]
    fn the_screen_center_ray_goes_where_the_camera_looks() {
        let mut camera = Camera3D::new(Vec2::new(800.0, 600.0));
        camera.position = Vec3::new(3.0, 4.0, 5.0);
        camera.look_at(Vec3::ZERO);
        let (from, dir) = camera.screen_ray(Vec2::new(400.0, 300.0));
        assert_eq!(from, camera.position);
        assert!((dir - camera.forward()).length() < 1e-4);
        // Left of center points left of forward.
        let (_, left) = camera.screen_ray(Vec2::new(0.0, 300.0));
        let right_axis = camera.rotation * Vec3::X;
        assert!(left.dot(right_axis) < 0.0);
    }

    #[test]
    fn the_view_projection_puts_the_target_in_the_middle() {
        let mut camera = Camera3D::new(Vec2::new(800.0, 600.0));
        camera.position = Vec3::new(0.0, 10.0, 10.0);
        camera.look_at(Vec3::new(1.0, 0.0, 0.0));
        let clip = camera.view_proj() * Vec4::new(1.0, 0.0, 0.0, 1.0);
        let ndc = clip.truncate() / clip.w;
        assert!(
            ndc.x.abs() < 1e-4 && ndc.y.abs() < 1e-4 && (0.0..1.0).contains(&ndc.z),
            "{ndc}"
        );
    }
}