Skip to main content

pfx_trace/
stage.rs

1use std::borrow::Cow;
2use std::collections::BTreeMap;
3use std::collections::btree_map::Entry;
4
5use pfx_core::anim::{Skinned, skin as skin_vertices};
6use pfx_gpu::Gpu;
7use pfx_load::Sky;
8use pfx_load::scene::{
9    Camera as SceneCamera, Environment as SceneEnvironment, Geometry, Posed,
10    Projection as SceneProjection, Scene as LoadedScene, surface as sprite_surface,
11};
12use pfx_materials::{ContentLayer, Material};
13
14use crate::detail::{
15    ContentFace, ContentImage, Detail, Instance, InstanceSurface, Lens, Transform, TriangleDetail,
16};
17use crate::gpu::{Camera, Projection, Scene, Sun, Trace, TraceError};
18use crate::lights::LocalLight;
19use crate::sky::Environment;
20use crate::text::TextContent;
21
22pub const ALPHA_CUTOFF: f32 = 0.5;
23
24#[derive(Clone, Copy, Debug)]
25pub struct Mesh<'a> {
26    pub positions: &'a [[f32; 3]],
27    pub normals: &'a [[f32; 3]],
28    pub tangents: &'a [[f32; 4]],
29    pub uvs: &'a [[f32; 2]],
30    pub alpha: Option<&'a [f32]>,
31    pub indices: &'a [u32],
32}
33
34#[derive(Clone, Copy, Debug, PartialEq)]
35pub struct Image<'a> {
36    pub width: u32,
37    pub height: u32,
38    pub texels: &'a [[u8; 4]],
39    pub srgb: bool,
40}
41
42impl Image<'_> {
43    pub fn same(&self, other: &Image<'_>) -> bool {
44        self.width == other.width
45            && self.height == other.height
46            && self.srgb == other.srgb
47            && (std::ptr::eq(self.texels, other.texels) || self.texels == other.texels)
48    }
49
50    pub fn valid(&self) -> bool {
51        self.width > 0
52            && self.height > 0
53            && (self.width as usize)
54                .checked_mul(self.height as usize)
55                .is_some_and(|count| self.texels.len() == count)
56    }
57
58    pub fn linear(&self) -> ContentImage {
59        let decode = |byte: u8| {
60            let value = f32::from(byte) / 255.0;
61            if !self.srgb {
62                value
63            } else {
64                pfx_materials::linear_channel(value)
65            }
66        };
67        ContentImage {
68            width: self.width,
69            height: self.height,
70            texels: self
71                .texels
72                .iter()
73                .map(|&[r, g, b, a]| [decode(r), decode(g), decode(b), f32::from(a) / 255.0])
74                .collect(),
75        }
76    }
77}
78
79#[derive(Clone, Copy, Debug, PartialEq)]
80pub enum ContentKind<'a> {
81    Image(Image<'a>),
82    Text(TextContent<'a>),
83}
84
85impl ContentKind<'_> {
86    pub fn same(&self, other: &ContentKind<'_>) -> bool {
87        match (self, other) {
88            (Self::Image(a), ContentKind::Image(b)) => a.same(b),
89            (Self::Text(a), ContentKind::Text(b)) => a.same(b),
90            _ => false,
91        }
92    }
93
94    pub fn valid(&self) -> bool {
95        match self {
96            Self::Image(image) => image.valid(),
97            Self::Text(text) => text.valid(),
98        }
99    }
100
101    pub fn image(&self) -> Option<Image<'_>> {
102        match self {
103            Self::Image(image) => Some(*image),
104            Self::Text(_) => None,
105        }
106    }
107}
108
109#[derive(Clone, Copy, Debug, PartialEq)]
110pub struct PlacementContent<'a> {
111    pub kind: ContentKind<'a>,
112    pub uv_offset: [f32; 2],
113    pub uv_scale: [f32; 2],
114    pub crop: [f32; 4],
115    pub face: ContentFace,
116    pub cutout: bool,
117}
118
119impl<'a> PlacementContent<'a> {
120    pub fn new(image: Image<'a>) -> Self {
121        Self::of(ContentKind::Image(image))
122    }
123
124    pub fn text(text: TextContent<'a>) -> Self {
125        Self::of(ContentKind::Text(text))
126    }
127
128    pub fn of(kind: ContentKind<'a>) -> Self {
129        Self {
130            kind,
131            uv_offset: [0.0; 2],
132            uv_scale: [1.0; 2],
133            crop: [0.0, 0.0, 1.0, 1.0],
134            face: ContentFace::Both,
135            cutout: false,
136        }
137    }
138}
139
140#[derive(Clone, Copy, Debug, PartialEq)]
141pub struct Placement<'a> {
142    pub mesh: u32,
143    pub model: Transform,
144    pub material: u32,
145    pub casts_shadow: bool,
146    pub content: Option<PlacementContent<'a>>,
147    pub clip: [[f32; 4]; 2],
148    pub shadow_only: bool,
149    pub alpha_cutoff: f32,
150    pub two_sided: bool,
151}
152
153impl Placement<'_> {
154    pub fn new(mesh: u32, model: Transform, material: u32) -> Self {
155        Self {
156            mesh,
157            model,
158            material,
159            casts_shadow: true,
160            content: None,
161            clip: [[0.0; 4]; 2],
162            shadow_only: false,
163            alpha_cutoff: ALPHA_CUTOFF,
164            two_sided: true,
165        }
166    }
167}
168
169pub struct Stage<'a, E = Sky> {
170    pub meshes: &'a [Mesh<'a>],
171    pub instances: &'a [Placement<'a>],
172    pub materials: &'a [Material],
173    pub sky: E,
174    pub sun: Sun,
175    pub camera: Camera,
176    pub projection: Projection,
177    pub lens: Lens,
178}
179
180pub struct Staged<E = Sky> {
181    pub scene: Scene<E>,
182    pub detail: Detail,
183}
184
185impl<E> Stage<'_, E> {
186    pub fn build(self) -> Result<Staged<E>, String> {
187        if self.materials.is_empty() {
188            return Err("staged scene needs at least one material".into());
189        }
190        if !self.projection.valid() || !self.lens.valid() {
191            return Err("staged camera has an invalid projection or lens".into());
192        }
193        for mesh in self.meshes {
194            check_mesh(mesh)?;
195        }
196        let first_slot = self
197            .materials
198            .iter()
199            .filter(|material| material.content_layer.active())
200            .map(|material| material.content_layer.slot as usize + 1)
201            .max()
202            .unwrap_or(0);
203        let mut sources: Vec<ContentKind<'_>> = Vec::new();
204        let mut cuts: Vec<(u32, u32, Vec<TriangleDetail>)> = Vec::new();
205        let mut instances = Vec::with_capacity(self.instances.len());
206        for placement in self.instances {
207            if !(0.0..=1.0).contains(&placement.alpha_cutoff) {
208                return Err("staged alpha cutoff must be a fraction from 0 to 1".into());
209            }
210            if !placement
211                .clip
212                .iter()
213                .flatten()
214                .all(|value| value.is_finite())
215            {
216                return Err("staged clip plane is not finite".into());
217            }
218            let mesh = self
219                .meshes
220                .get(placement.mesh as usize)
221                .ok_or("staged instance names a missing mesh")?;
222            if placement.material as usize >= self.materials.len() {
223                return Err("staged instance names a missing material".into());
224            }
225            let mut surface = InstanceSurface {
226                casts_shadow: placement.casts_shadow,
227                clip: placement.clip,
228                alpha_cutoff: placement.alpha_cutoff,
229                ..InstanceSurface::default()
230            };
231            if let Some(content) = placement.content {
232                if !content.kind.valid() {
233                    return Err("staged content image has the wrong texel count".into());
234                }
235                let index = match sources.iter().position(|source| source.same(&content.kind)) {
236                    Some(index) => index,
237                    None => {
238                        sources.push(content.kind);
239                        sources.len() - 1
240                    }
241                };
242                let material = &self.materials[placement.material as usize];
243                surface.uv_offset = content.uv_offset;
244                surface.uv_scale = content.uv_scale;
245                surface.crop = content.crop;
246                surface.face = content.face;
247                surface.cutout = content.cutout;
248                surface.layer = ContentLayer {
249                    slot: (first_slot + index) as i32,
250                    ..material.content_layer
251                };
252                if !surface.valid() {
253                    return Err("staged content placement is not finite".into());
254                }
255            }
256            let key = placement.alpha_cutoff.to_bits();
257            let mut kept = if let Some((_, _, ready)) = cuts
258                .iter()
259                .find(|(index, bits, _)| *index == placement.mesh && *bits == key)
260            {
261                ready.clone()
262            } else {
263                let ready = triangles(mesh, placement.alpha_cutoff)?;
264                cuts.push((placement.mesh, key, ready.clone()));
265                ready
266            };
267            for triangle in &mut kept {
268                triangle.material = placement.material;
269            }
270            instances.push(Instance {
271                triangles: kept,
272                transform: placement.model,
273                surface,
274                shadow_only: placement.shadow_only,
275                two_sided: placement.two_sided,
276            });
277        }
278        let content_slots = if sources.is_empty() {
279            Vec::new()
280        } else {
281            std::iter::repeat_n(None, first_slot)
282                .chain(
283                    sources
284                        .iter()
285                        .map(|source| source.image().map(|image| image.linear())),
286                )
287                .collect()
288        };
289        let text_slots = if sources
290            .iter()
291            .any(|source| matches!(source, ContentKind::Text(_)))
292        {
293            let mut slots = vec![None; first_slot];
294            for source in &sources {
295                slots.push(match source {
296                    ContentKind::Text(text) => Some(text.records()?),
297                    ContentKind::Image(_) => None,
298                });
299            }
300            slots
301        } else {
302            Vec::new()
303        };
304        Ok(Staged {
305            scene: Scene {
306                triangles: Vec::new(),
307                shapes: Vec::new(),
308                materials: self.materials.to_vec(),
309                sky: self.sky,
310                camera: self.camera,
311                sun: self.sun,
312            },
313            detail: Detail {
314                instances,
315                lens: self.lens,
316                projection: self.projection,
317                content_slots,
318                text_slots,
319                ..Detail::default()
320            },
321        })
322    }
323}
324
325impl<E: Clone + Into<Environment>> Staged<E> {
326    pub fn trace(&self, gpu: &Gpu, width: u32, height: u32) -> Result<Trace, TraceError> {
327        Trace::new_detailed(gpu, &self.scene, &self.detail, width, height)
328    }
329}
330
331pub fn scene_camera(camera: &SceneCamera, aspect: f32) -> (Camera, Projection) {
332    let (forward, right, up) = camera.axes();
333    match camera.projection {
334        SceneProjection::Perspective { fov } => {
335            let tan = (fov.to_radians() * 0.5).tan();
336            (
337                Camera {
338                    origin: camera.at,
339                    forward,
340                    right: right.map(|v| v * tan * aspect),
341                    up: up.map(|v| v * tan),
342                },
343                Projection::Perspective,
344            )
345        }
346        SceneProjection::Orthographic { height } => (
347            Camera {
348                origin: camera.at,
349                forward,
350                right,
351                up,
352            },
353            Projection::Orthographic {
354                width: height * aspect,
355                height,
356            },
357        ),
358    }
359}
360
361pub fn scene(scene: &LoadedScene, aspect: f32) -> Result<Staged<Environment>, String> {
362    scene_at(scene, aspect, 0.0)
363}
364
365pub fn scene_lens(camera: &SceneCamera) -> Lens {
366    match camera.depth_of_field {
367        Some(depth) => Lens {
368            shift: camera.shift,
369            aperture: depth.aperture(),
370            focus_distance: depth.distance,
371        },
372        None => Lens {
373            shift: camera.shift,
374            ..Lens::default()
375        },
376    }
377}
378
379pub fn scene_at(
380    scene: &LoadedScene,
381    aspect: f32,
382    time: f32,
383) -> Result<Staged<Environment>, String> {
384    scene_posed(scene, aspect, &Posed::at(time))
385}
386
387pub fn scene_posed(
388    scene: &LoadedScene,
389    aspect: f32,
390    posed: &Posed,
391) -> Result<Staged<Environment>, String> {
392    let draws = scene.draws();
393    let viewpoint = scene.camera_or_default();
394    let computed;
395    let objects = if posed.models.is_empty() {
396        computed = scene.posed(&viewpoint, posed.time);
397        &computed
398    } else {
399        &posed.models
400    };
401    let models = draws.models_posed(objects, &posed.palettes);
402    let mut baked: Vec<Option<Skinned>> = Vec::with_capacity(draws.items.len());
403    for draw in &draws.items {
404        let (Some(skin), Some(palette)) = (
405            &draw.geometry.skin,
406            draw.owner.and_then(|owner| posed.palettes.get(&owner)),
407        ) else {
408            baked.push(None);
409            continue;
410        };
411        let geometry = &draw.geometry;
412        let skinned = skin_vertices(
413            &geometry.positions,
414            &geometry.normals,
415            &geometry.tangents,
416            &skin.joints,
417            &skin.weights,
418            palette,
419        )
420        .map_err(|error| format!("object {}: {error}", draw.object))?;
421        baked.push(Some(skinned));
422    }
423    let mut geometries: Vec<&Geometry> = Vec::new();
424    let mut slots = BTreeMap::new();
425    for draw in &draws.items {
426        if let Entry::Vacant(slot) = slots.entry(draw.geometry.hash) {
427            slot.insert(geometries.len() as u32);
428            geometries.push(&draw.geometry);
429        }
430    }
431    let mut letterings = Vec::new();
432    for text in scene.texts.values() {
433        let paragraph = text.paragraph()?;
434        let glyphs = crate::text::glyphs(&paragraph);
435        if glyphs.is_empty() {
436            continue;
437        }
438        let lettering = crate::text::lettering(&paragraph.atlas, &glyphs)?;
439        letterings.push((lettering, text.model(), text.lit));
440    }
441    let carriers: Vec<crate::text::Carrier> = letterings
442        .iter()
443        .map(|(lettering, _, _)| lettering.carrier())
444        .collect();
445    let mut meshes: Vec<Mesh<'_>> = geometries
446        .iter()
447        .map(|geometry| Mesh {
448            positions: &geometry.positions,
449            normals: &geometry.normals,
450            tangents: &geometry.tangents,
451            uvs: &geometry.uvs,
452            alpha: None,
453            indices: &geometry.indices,
454        })
455        .collect();
456    let mut placements = Vec::with_capacity(draws.items.len() + letterings.len());
457    for ((draw, model), skinned) in draws.items.iter().zip(&models).zip(&baked) {
458        let mesh = match skinned {
459            Some(skinned) => {
460                let geometry = &draw.geometry;
461                meshes.push(Mesh {
462                    positions: &skinned.positions,
463                    normals: &skinned.normals,
464                    tangents: &skinned.tangents,
465                    uvs: &geometry.uvs,
466                    alpha: None,
467                    indices: &geometry.indices,
468                });
469                (meshes.len() - 1) as u32
470            }
471            None => *slots
472                .get(&draw.geometry.hash)
473                .ok_or("a staged draw lost its mesh")?,
474        };
475        let material = &draws.materials[draw.material as usize];
476        let content = match &draw.content {
477            Some(name) if material.content_layer.active() => {
478                let content = scene
479                    .contents
480                    .get(name)
481                    .ok_or_else(|| format!("content {name} is missing"))?;
482                let mut placed = PlacementContent::new(Image {
483                    width: content.width,
484                    height: content.height,
485                    texels: bytemuck::cast_slice(&content.rgba),
486                    srgb: content.srgb,
487                });
488                let frame = draw.owner.and_then(|owner| {
489                    posed.frames.get(&owner).copied().or_else(|| {
490                        scene
491                            .animations
492                            .get(&scene.objects.get(owner)?.name)?
493                            .still_frame()
494                    })
495                });
496                if let Some((offset, scale, crop)) = frame.map(sprite_surface) {
497                    placed.uv_offset = offset;
498                    placed.uv_scale = scale;
499                    placed.crop = crop;
500                }
501                Some(placed)
502            }
503            _ => None,
504        };
505        placements.push(Placement {
506            casts_shadow: draw.shadow.casts(),
507            shadow_only: !draw.shadow.drawn(),
508            two_sided: draw.two_sided,
509            clip: draw.clip,
510            content,
511            alpha_cutoff: draw.alpha_cutoff,
512            ..Placement::new(mesh, *model, draw.material)
513        });
514    }
515    let mut materials = Cow::Borrowed(draws.materials.as_slice());
516    let text_material = materials.len() as u32;
517    if !letterings.is_empty() {
518        materials
519            .to_mut()
520            .extend([crate::text::material(false), crate::text::material(true)]);
521    }
522    for ((lettering, model, lit), carrier) in letterings.iter().zip(&carriers) {
523        let mesh = meshes.len() as u32;
524        meshes.push(carrier.mesh());
525        placements.push(Placement {
526            casts_shadow: false,
527            content: Some(lettering.content()),
528            ..Placement::new(mesh, *model, text_material + u32::from(*lit))
529        });
530    }
531    let sun = scene.sun_or_dark();
532    let (camera, projection) = scene_camera(&viewpoint, aspect);
533    let sky = match scene.environment() {
534        SceneEnvironment::Analytic(sky) => Environment::Analytic(sky),
535        SceneEnvironment::Hdr(sky) => Environment::Hdr(std::sync::Arc::unwrap_or_clone(sky)),
536    };
537    let mut staged = Stage {
538        meshes: &meshes,
539        instances: &placements,
540        materials: &materials,
541        sky,
542        sun: Sun {
543            direction: sun.direction,
544            color: sun.color,
545            intensity: sun.intensity,
546        },
547        camera,
548        projection,
549        lens: scene_lens(&viewpoint),
550    }
551    .build()?;
552    staged.detail.sun_radius_deg = sun.radius;
553    staged.detail.lights = scene
554        .lights
555        .values()
556        .map(|light| LocalLight {
557            radius: light.radius,
558            shadow: light.shadow,
559            ..LocalLight::point(light.position, light.color, light.intensity, light.range)
560        })
561        .collect();
562    staged.detail.transmissive_shadows = scene.trace.transmissive_shadows;
563    staged.detail.clamp_indirect = scene.trace.clamp_indirect;
564    staged.detail.filter_glossy = scene.trace.filter_glossy;
565    Ok(staged)
566}
567
568#[derive(Clone, Copy)]
569struct Corner {
570    position: [f32; 3],
571    normal: [f32; 3],
572    uv: [f32; 2],
573    alpha: f32,
574}
575
576fn lerp<const N: usize>(a: [f32; N], b: [f32; N], t: f32) -> [f32; N] {
577    std::array::from_fn(|k| a[k] + (b[k] - a[k]) * t)
578}
579
580fn check_mesh(mesh: &Mesh<'_>) -> Result<(), String> {
581    let count = mesh.positions.len();
582    if count == 0 || mesh.indices.is_empty() || !mesh.indices.len().is_multiple_of(3) {
583        return Err("mesh needs vertices and triangle indices".into());
584    }
585    if mesh.normals.len() != count
586        || mesh.tangents.len() != count
587        || mesh.uvs.len() != count
588        || mesh.alpha.is_some_and(|alpha| alpha.len() != count)
589    {
590        return Err("mesh attributes have different vertex counts".into());
591    }
592    if mesh.indices.iter().any(|&index| index as usize >= count) {
593        return Err("mesh index is outside the vertex buffer".into());
594    }
595    Ok(())
596}
597
598fn triangles(mesh: &Mesh<'_>, cutoff: f32) -> Result<Vec<TriangleDetail>, String> {
599    check_mesh(mesh)?;
600    let corner = |index: u32| {
601        let index = index as usize;
602        Corner {
603            position: mesh.positions[index],
604            normal: mesh.normals[index],
605            uv: mesh.uvs[index],
606            alpha: mesh.alpha.map_or(1.0, |alpha| alpha[index]),
607        }
608    };
609    let mut out = Vec::with_capacity(mesh.indices.len() / 3);
610    for face in mesh.indices.chunks_exact(3) {
611        let corners = [corner(face[0]), corner(face[1]), corner(face[2])];
612        let kept = if cutoff.to_bits() == ALPHA_CUTOFF.to_bits() {
613            cut(corners)
614        } else {
615            cut_at(corners, cutoff)
616        };
617        for k in 1..kept.len().saturating_sub(1) {
618            let [a, b, c] = [kept[0], kept[k], kept[k + 1]];
619            out.push(TriangleDetail {
620                vertices: [a.position, b.position, c.position],
621                normals: [a.normal, b.normal, c.normal],
622                uvs: [a.uv, b.uv, c.uv],
623                material: 0,
624            });
625        }
626    }
627    Ok(out)
628}
629
630fn cut(corners: [Corner; 3]) -> Vec<Corner> {
631    if corners.iter().all(|c| c.alpha >= ALPHA_CUTOFF) {
632        return corners.to_vec();
633    }
634    let mut kept = Vec::with_capacity(4);
635    for k in 0..3 {
636        let a = corners[k];
637        let b = corners[(k + 1) % 3];
638        let inside_a = a.alpha >= ALPHA_CUTOFF;
639        let inside_b = b.alpha >= ALPHA_CUTOFF;
640        if inside_a {
641            kept.push(a);
642        }
643        if inside_a != inside_b {
644            let t = (ALPHA_CUTOFF - a.alpha) / (b.alpha - a.alpha);
645            kept.push(Corner {
646                position: lerp(a.position, b.position, t),
647                normal: lerp(a.normal, b.normal, t),
648                uv: lerp(a.uv, b.uv, t),
649                alpha: ALPHA_CUTOFF,
650            });
651        }
652    }
653    kept
654}
655
656fn cut_at(corners: [Corner; 3], cutoff: f32) -> Vec<Corner> {
657    if corners.iter().all(|c| c.alpha >= cutoff) {
658        return corners.to_vec();
659    }
660    let mut kept = Vec::with_capacity(4);
661    for k in 0..3 {
662        let a = corners[k];
663        let b = corners[(k + 1) % 3];
664        let inside_a = a.alpha >= cutoff;
665        let inside_b = b.alpha >= cutoff;
666        if inside_a {
667            kept.push(a);
668        }
669        if inside_a != inside_b {
670            let t = (cutoff - a.alpha) / (b.alpha - a.alpha);
671            kept.push(Corner {
672                position: lerp(a.position, b.position, t),
673                normal: lerp(a.normal, b.normal, t),
674                uv: lerp(a.uv, b.uv, t),
675                alpha: cutoff,
676            });
677        }
678    }
679    kept
680}
681
682#[cfg(test)]
683mod tests {
684    use super::*;
685
686    const IDENTITY: Transform = [
687        [1.0, 0.0, 0.0, 0.0],
688        [0.0, 1.0, 0.0, 0.0],
689        [0.0, 0.0, 1.0, 0.0],
690        [0.0, 0.0, 0.0, 1.0],
691    ];
692
693    fn stage<'a>(
694        meshes: &'a [Mesh<'a>],
695        instances: &'a [Placement<'a>],
696        materials: &'a [Material],
697    ) -> Stage<'a> {
698        Stage {
699            meshes,
700            instances,
701            materials,
702            sky: Sky {
703                width: 1,
704                height: 1,
705                texels: vec![[0.0; 4]],
706            },
707            sun: Sun {
708                direction: [0.0, 1.0, 0.0],
709                color: [1.0; 3],
710                intensity: 0.0,
711            },
712            camera: Camera {
713                origin: [0.0; 3],
714                forward: [0.0, 0.0, -1.0],
715                right: [1.0, 0.0, 0.0],
716                up: [0.0, 1.0, 0.0],
717            },
718            projection: Projection::Perspective,
719            lens: Lens::default(),
720        }
721    }
722
723    const POSITIONS: [[f32; 3]; 3] = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
724    const NORMALS: [[f32; 3]; 3] = [[0.0, 0.0, 1.0]; 3];
725    const TANGENTS: [[f32; 4]; 3] = [[1.0, 0.0, 0.0, 1.0]; 3];
726    const UVS: [[f32; 2]; 3] = [[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]];
727
728    fn triangle(alpha: Option<&[f32]>) -> Mesh<'_> {
729        Mesh {
730            positions: &POSITIONS,
731            normals: &NORMALS,
732            tangents: &TANGENTS,
733            uvs: &UVS,
734            alpha,
735            indices: &[0, 1, 2],
736        }
737    }
738
739    fn area(triangle: &TriangleDetail) -> f32 {
740        let [a, b, c] = triangle.vertices;
741        let e1 = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
742        let e2 = [c[0] - a[0], c[1] - a[1], c[2] - a[2]];
743        0.5 * (e1[0] * e2[1] - e1[1] * e2[0]).abs()
744    }
745
746    #[test]
747    fn instances_share_a_mesh_with_their_own_material_and_transform() {
748        let meshes = [triangle(None)];
749        let mut moved = IDENTITY;
750        moved[3] = [2.0, 0.0, -1.0, 1.0];
751        let instances = [Placement::new(0, IDENTITY, 1), Placement::new(0, moved, 0)];
752        let materials = [Material::default(), Material::default()];
753        let staged = stage(&meshes, &instances, &materials).build().unwrap();
754        assert!(staged.scene.triangles.is_empty());
755        assert_eq!(staged.scene.materials.len(), 2);
756        let placed = &staged.detail.instances;
757        assert_eq!(placed.len(), 2);
758        assert_eq!(placed[0].triangles[0].material, 1);
759        assert_eq!(placed[1].triangles[0].material, 0);
760        assert_eq!(placed[0].triangles[0].uvs, UVS);
761        assert_eq!(
762            placed[1].transformed().unwrap()[0].vertices[1],
763            [3.0, 0.0, -1.0]
764        );
765    }
766
767    #[test]
768    fn alpha_below_the_cutoff_is_cut_where_the_live_renderer_discards() {
769        let materials = [Material::default()];
770        let instances = [Placement::new(0, IDENTITY, 0)];
771        let cases: [(&[f32], usize, f32); 4] = [
772            (&[1.0, 1.0, 1.0], 1, 0.5),
773            (&[0.0, 0.0, 0.0], 0, 0.0),
774            (&[1.0, 0.0, 0.0], 1, 0.125),
775            (&[1.0, 1.0, 0.0], 2, 0.375),
776        ];
777        for (alpha, count, expected) in cases {
778            let meshes = [triangle(Some(alpha))];
779            let staged = stage(&meshes, &instances, &materials).build().unwrap();
780            let kept = &staged.detail.instances[0].triangles;
781            assert_eq!(kept.len(), count, "{alpha:?}");
782            let total: f32 = kept.iter().map(area).sum();
783            assert!((total - expected).abs() < 1e-6, "{alpha:?}: {total}");
784            for triangle in kept {
785                for (vertex, uv) in triangle.vertices.iter().zip(triangle.uvs) {
786                    assert_eq!([vertex[0], vertex[1]], uv);
787                }
788            }
789        }
790    }
791
792    #[test]
793    fn broken_input_is_refused() {
794        let materials = [Material::default()];
795        let good = [Placement::new(0, IDENTITY, 0)];
796        let short = Mesh {
797            uvs: &UVS[..2],
798            ..triangle(None)
799        };
800        let outside = Mesh {
801            indices: &[0, 1, 3],
802            ..triangle(None)
803        };
804        for mesh in [short, outside] {
805            assert!(stage(&[mesh], &good, &materials).build().is_err());
806        }
807        let meshes = [triangle(None)];
808        let missing_mesh = [Placement { mesh: 1, ..good[0] }];
809        let missing_material = [Placement {
810            material: 1,
811            ..good[0]
812        }];
813        assert!(stage(&meshes, &missing_mesh, &materials).build().is_err());
814        assert!(
815            stage(&meshes, &missing_material, &materials)
816                .build()
817                .is_err()
818        );
819        assert!(stage(&meshes, &good, &[]).build().is_err());
820        let mut flat = stage(&meshes, &good, &materials);
821        flat.projection = Projection::Orthographic {
822            width: 0.0,
823            height: 1.0,
824        };
825        assert!(flat.build().is_err());
826    }
827
828    #[test]
829    fn placements_carry_their_shadow_flag_and_share_content_slots_by_image() {
830        let meshes = [triangle(None)];
831        let red = [[255, 0, 0, 255]; 4];
832        let red_copy = red;
833        let grey = [[188, 188, 188, 128]; 4];
834        fn image(texels: &[[u8; 4]], srgb: bool) -> Image<'_> {
835            Image {
836                width: 2,
837                height: 2,
838                texels,
839                srgb,
840            }
841        }
842        let shown = PlacementContent {
843            uv_offset: [0.5, 0.0],
844            uv_scale: [0.5, 1.0],
845            crop: [0.5, 0.0, 1.0, 1.0],
846            face: ContentFace::Front,
847            cutout: true,
848            ..PlacementContent::new(image(&red, true))
849        };
850        let instances = [
851            Placement {
852                casts_shadow: false,
853                ..Placement::new(0, IDENTITY, 0)
854            },
855            Placement {
856                content: Some(shown),
857                ..Placement::new(0, IDENTITY, 1)
858            },
859            Placement {
860                content: Some(PlacementContent::new(image(&red_copy, true))),
861                ..Placement::new(0, IDENTITY, 1)
862            },
863            Placement {
864                content: Some(PlacementContent::new(image(&grey, false))),
865                ..Placement::new(0, IDENTITY, 0)
866            },
867            Placement {
868                content: Some(PlacementContent::new(image(&grey, true))),
869                ..Placement::new(0, IDENTITY, 0)
870            },
871        ];
872        let screen = Material {
873            content_layer: ContentLayer::from_kind(
874                pfx_materials::Content::Screen,
875                1.0,
876                1,
877                &pfx_materials::ContentLook {
878                    screen_gain: 1.5,
879                    ..Default::default()
880                },
881            ),
882            ..Material::default()
883        };
884        let materials = [Material::default(), screen];
885        let staged = stage(&meshes, &instances, &materials).build().unwrap();
886        let surfaces: Vec<InstanceSurface> = staged
887            .detail
888            .instances
889            .iter()
890            .map(|instance| instance.surface)
891            .collect();
892        assert!(!surfaces[0].casts_shadow);
893        assert!(!surfaces[0].layer.active());
894        assert!(surfaces[1..].iter().all(|surface| surface.casts_shadow));
895        assert_eq!(
896            surfaces.iter().map(|s| s.layer.slot).collect::<Vec<_>>(),
897            [-1, 2, 2, 3, 4]
898        );
899        assert_eq!(surfaces[1].layer.blend, screen.content_layer.blend);
900        assert_eq!(surfaces[1].layer.strength, screen.content_layer.strength);
901        assert_eq!(
902            surfaces[3].layer,
903            ContentLayer {
904                slot: 3,
905                ..ContentLayer::default()
906            }
907        );
908        assert_eq!(surfaces[1].uv_offset, [0.5, 0.0]);
909        assert_eq!(surfaces[1].crop, [0.5, 0.0, 1.0, 1.0]);
910        assert_eq!(surfaces[1].face, ContentFace::Front);
911        assert!(surfaces[1].cutout && !surfaces[2].cutout);
912        let slots = &staged.detail.content_slots;
913        assert_eq!(slots.len(), 5);
914        assert!(slots[..2].iter().all(Option::is_none));
915        assert_eq!(slots[2].as_ref().unwrap().texels[0], [1.0, 0.0, 0.0, 1.0]);
916        let linear = slots[3].as_ref().unwrap().texels[0];
917        assert!((linear[0] - 188.0 / 255.0).abs() < 1e-6);
918        assert!((linear[3] - 128.0 / 255.0).abs() < 1e-6);
919        let decoded = slots[4].as_ref().unwrap().texels[0];
920        assert!((decoded[0] - 0.5029).abs() < 1e-3, "{decoded:?}");
921        assert_eq!(decoded[3], linear[3]);
922    }
923
924    #[test]
925    fn placements_without_content_leave_the_slots_empty() {
926        let meshes = [triangle(None)];
927        let instances = [Placement::new(0, IDENTITY, 0)];
928        let materials = [Material::default()];
929        let staged = stage(&meshes, &instances, &materials).build().unwrap();
930        assert!(staged.detail.content_slots.is_empty());
931        assert_eq!(
932            staged.detail.instances[0].surface,
933            InstanceSurface::default()
934        );
935    }
936
937    #[test]
938    fn broken_content_is_refused() {
939        let meshes = [triangle(None)];
940        let materials = [Material::default()];
941        let texels = [[0_u8; 4]; 3];
942        let short = Image {
943            width: 2,
944            height: 2,
945            texels: &texels,
946            srgb: true,
947        };
948        let instances = [Placement {
949            content: Some(PlacementContent::new(short)),
950            ..Placement::new(0, IDENTITY, 0)
951        }];
952        assert!(stage(&meshes, &instances, &materials).build().is_err());
953        let fine = Image {
954            width: 3,
955            height: 1,
956            ..short
957        };
958        let nan = [Placement {
959            content: Some(PlacementContent {
960                uv_scale: [f32::NAN, 1.0],
961                ..PlacementContent::new(fine)
962            }),
963            ..Placement::new(0, IDENTITY, 0)
964        }];
965        assert!(stage(&meshes, &nan, &materials).build().is_err());
966    }
967
968    fn details_match(left: &Detail, right: &Detail) {
969        assert_eq!(left.triangles, right.triangles);
970        assert_eq!(left.instances, right.instances);
971        assert_eq!(left.lens, right.lens);
972        assert_eq!(left.projection, right.projection);
973        assert_eq!(left.sun_radius_deg, right.sun_radius_deg);
974        assert_eq!(left.content, right.content);
975        assert_eq!(left.content_slots, right.content_slots);
976        assert_eq!(left.leaf, right.leaf);
977        assert_eq!(left.steam, right.steam);
978        assert_eq!(left.lights, right.lights);
979    }
980
981    #[test]
982    fn a_placement_carries_clip_and_shadow_only_into_the_detail() {
983        let meshes = [triangle(None)];
984        let materials = [Material::default()];
985        let clip = [[0.0, 1.0, 0.0, -0.5], [1.0, 0.0, 0.0, 0.2]];
986        let placed = [Placement {
987            clip,
988            shadow_only: true,
989            ..Placement::new(0, IDENTITY, 0)
990        }];
991        let built = stage(&meshes, &placed, &materials).build().unwrap();
992        let mut patched = stage(&meshes, &[Placement::new(0, IDENTITY, 0)], &materials)
993            .build()
994            .unwrap();
995        patched.detail.instances[0].shadow_only = true;
996        patched.detail.instances[0].surface.clip = clip;
997        details_match(&built.detail, &patched.detail);
998        assert!(built.detail.instances[0].surface.cuts([0.0, 0.0, 0.0]));
999        assert!(!built.detail.instances[0].surface.cuts([0.0, -1.0, 0.0]));
1000    }
1001
1002    #[test]
1003    fn a_placement_is_two_sided_unless_it_says_otherwise() {
1004        let meshes = [triangle(None)];
1005        let materials = [Material::default()];
1006        let open = stage(&meshes, &[Placement::new(0, IDENTITY, 0)], &materials)
1007            .build()
1008            .unwrap();
1009        assert!(open.detail.instances[0].two_sided);
1010        let closed = [Placement {
1011            two_sided: false,
1012            ..Placement::new(0, IDENTITY, 0)
1013        }];
1014        let built = stage(&meshes, &closed, &materials).build().unwrap();
1015        assert!(!built.detail.instances[0].two_sided);
1016    }
1017
1018    #[test]
1019    fn one_mesh_keeps_a_polygon_for_each_cutoff() {
1020        let alpha = [1.0_f32, 0.0, 0.0];
1021        let meshes = [triangle(Some(&alpha))];
1022        let materials = [Material::default()];
1023        let instances = [
1024            Placement {
1025                alpha_cutoff: 0.25,
1026                ..Placement::new(0, IDENTITY, 0)
1027            },
1028            Placement {
1029                alpha_cutoff: 0.9,
1030                ..Placement::new(0, IDENTITY, 0)
1031            },
1032            Placement::new(0, IDENTITY, 0),
1033        ];
1034        let staged = stage(&meshes, &instances, &materials).build().unwrap();
1035        let areas: Vec<f32> = staged
1036            .detail
1037            .instances
1038            .iter()
1039            .map(|instance| instance.triangles.iter().map(area).sum())
1040            .collect();
1041        assert!((areas[0] - 0.28125).abs() < 1e-6, "{}", areas[0]);
1042        assert!((areas[1] - 0.005).abs() < 1e-6, "{}", areas[1]);
1043        assert!((areas[2] - 0.125).abs() < 1e-6, "{}", areas[2]);
1044        assert_eq!(staged.detail.instances[0].surface.alpha_cutoff, 0.25);
1045        assert_eq!(
1046            staged.detail.instances[2].surface.alpha_cutoff,
1047            ALPHA_CUTOFF
1048        );
1049    }
1050
1051    #[test]
1052    fn a_cutoff_outside_zero_to_one_or_a_non_finite_clip_is_refused() {
1053        let meshes = [triangle(None)];
1054        let materials = [Material::default()];
1055        let bad = [
1056            Placement {
1057                alpha_cutoff: 1.1,
1058                ..Placement::new(0, IDENTITY, 0)
1059            },
1060            Placement {
1061                alpha_cutoff: -0.01,
1062                ..Placement::new(0, IDENTITY, 0)
1063            },
1064            Placement {
1065                alpha_cutoff: f32::NAN,
1066                ..Placement::new(0, IDENTITY, 0)
1067            },
1068            Placement {
1069                alpha_cutoff: f32::INFINITY,
1070                ..Placement::new(0, IDENTITY, 0)
1071            },
1072            Placement {
1073                clip: [[f32::NAN, 0.0, 0.0, 0.0], [0.0; 4]],
1074                ..Placement::new(0, IDENTITY, 0)
1075            },
1076        ];
1077        for placement in bad {
1078            assert!(stage(&meshes, &[placement], &materials).build().is_err());
1079        }
1080        for cutoff in [0.0, 1.0] {
1081            let placement = [Placement {
1082                alpha_cutoff: cutoff,
1083                ..Placement::new(0, IDENTITY, 0)
1084            }];
1085            assert!(stage(&meshes, &placement, &materials).build().is_ok());
1086        }
1087        let spare = Mesh {
1088            indices: &[0, 1, 3],
1089            ..triangle(None)
1090        };
1091        assert!(
1092            stage(
1093                &[triangle(None), spare],
1094                &[Placement::new(0, IDENTITY, 0)],
1095                &materials
1096            )
1097            .build()
1098            .is_err()
1099        );
1100    }
1101
1102    #[test]
1103    fn a_loaded_scene_stages_its_lens_sun_radius_cutoff_movers_and_text() {
1104        let manifest = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
1105        let root = manifest.join("../../tmp/trace-scene-tests/keys");
1106        let _ = std::fs::remove_dir_all(&root);
1107        std::fs::create_dir_all(&root).unwrap();
1108        for entry in std::fs::read_dir(manifest.join("../load/tests/scenes")).unwrap() {
1109            let entry = entry.unwrap();
1110            std::fs::copy(entry.path(), root.join(entry.file_name())).unwrap();
1111        }
1112        std::fs::copy(
1113            manifest.join("../text/fonts/EBGaramond[wght].ttf"),
1114            root.join("serif.ttf"),
1115        )
1116        .unwrap();
1117        let path = root.join("keys.scene.toml");
1118        std::fs::write(
1119            &path,
1120            "materials = [\"materials.toml\"]\n\n[mesh.block]\nfile = \"block.gltf\"\n\n[[object]]\nname = \"lid\"\nmesh = \"block\"\nalpha_cutoff = 0.3\n\n[[mover]]\nname = \"slide\"\nobjects = [\"lid\"]\nkind = \"slide\"\naxis = [1.0, 0.0, 0.0]\ntravel = [0.0, 2.0]\nperiod = 2.0\n\n[sun]\nmodel = \"authored\"\ntoward = [0.0, 1.0, 0.0]\nirradiance = 1.0\nradius = 1.5\n\n[camera]\nat = [0.0, 0.0, 5.0]\nfocal = 50.0\nfstop = 2.0\nshift = [0.1, 0.2]\n\n[text.sign]\ntext = \"Hi\"\nfont = \"serif.ttf\"\nsize = 0.5\nat = [0.0, 1.0, 0.0]\n",
1121        )
1122        .unwrap();
1123        let loaded = LoadedScene::open(&path).unwrap();
1124        let rest = scene(&loaded, 1.5).unwrap();
1125        let moved = scene_at(&loaded, 1.5, 1.0).unwrap();
1126        let _ = std::fs::remove_dir_all(&root);
1127        assert_eq!(rest.detail.sun_radius_deg, 1.5);
1128        assert_eq!(rest.detail.lens.shift, [0.1, 0.2]);
1129        assert!((rest.detail.lens.aperture - 0.025).abs() < 1e-6);
1130        assert!((rest.detail.lens.focus_distance - 5.0).abs() < 1e-5);
1131        assert_eq!(rest.detail.instances.len(), 2);
1132        assert_eq!(rest.detail.instances[0].surface.alpha_cutoff, 0.3);
1133        let travelled =
1134            moved.detail.instances[0].transform[3][0] - rest.detail.instances[0].transform[3][0];
1135        assert!((travelled - 2.0).abs() < 1e-5, "{travelled}");
1136        let text = &rest.detail.instances[1];
1137        assert!(!text.surface.casts_shadow);
1138        assert!((text.transform[3][1] - 1.0).abs() < 1e-6);
1139        assert_eq!(text.transform[1][1], -1.0);
1140        assert!(rest.detail.text_slots.iter().any(Option::is_some));
1141        assert_eq!(
1142            rest.scene.materials.len(),
1143            loaded.draws().materials.len() + 2
1144        );
1145    }
1146
1147    #[test]
1148    fn a_scene_trace_table_turns_on_transmissive_shadows() {
1149        let folder = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tmp/stage-trace");
1150        std::fs::create_dir_all(&folder).unwrap();
1151        let mut staged = Vec::new();
1152        for (name, text) in [
1153            ("plain.scene.toml", ""),
1154            ("on.scene.toml", "[trace]\ntransmissive_shadows = true\n"),
1155        ] {
1156            let path = folder.join(name);
1157            let glow = "[[emitter]]\nname = \"glow\"\nposition = [0.0, 1.0, 0.0]\nradius = 0.2\nintensity = 1.0\n\n";
1158            std::fs::write(&path, format!("{glow}{text}")).unwrap();
1159            let scene = LoadedScene::open(&path).unwrap();
1160            staged.push(
1161                super::scene(&scene, 1.0)
1162                    .unwrap()
1163                    .detail
1164                    .transmissive_shadows,
1165            );
1166        }
1167        let _ = std::fs::remove_dir_all(&folder);
1168        assert_eq!(staged, [false, true]);
1169    }
1170
1171    #[test]
1172    fn a_scene_trace_table_sets_the_firefly_clamp_and_the_glossy_filter() {
1173        let folder =
1174            std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tmp/stage-fireflies");
1175        std::fs::create_dir_all(&folder).unwrap();
1176        let mut staged = Vec::new();
1177        for (name, text) in [
1178            ("plain.scene.toml", ""),
1179            (
1180                "on.scene.toml",
1181                "[trace]\nclamp_indirect = 12.0\nfilter_glossy = 0.2\n",
1182            ),
1183        ] {
1184            let path = folder.join(name);
1185            let glow = "[[emitter]]\nname = \"glow\"\nposition = [0.0, 1.0, 0.0]\nradius = 0.2\nintensity = 1.0\n\n";
1186            std::fs::write(&path, format!("{glow}{text}")).unwrap();
1187            let scene = LoadedScene::open(&path).unwrap();
1188            let detail = super::scene(&scene, 1.0).unwrap().detail;
1189            staged.push((detail.clamp_indirect, detail.filter_glossy));
1190        }
1191        let _ = std::fs::remove_dir_all(&folder);
1192        assert_eq!(staged, [(0.0, 0.0), (12.0, 0.2)]);
1193    }
1194}