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(¶graph);
435 if glyphs.is_empty() {
436 continue;
437 }
438 let lettering = crate::text::lettering(¶graph.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}