Skip to main content

pfx_trace/
detail.rs

1use pfx_materials::{ContentLayer, ContentLook};
2
3use crate::bvh::Triangle;
4use crate::math::{cross, dot, normalize, sub};
5
6#[derive(Clone, Copy, Debug, PartialEq)]
7pub struct TriangleDetail {
8    pub vertices: [[f32; 3]; 3],
9    pub normals: [[f32; 3]; 3],
10    pub uvs: [[f32; 2]; 3],
11    pub material: u32,
12}
13
14impl TriangleDetail {
15    pub fn flat(triangle: Triangle) -> Self {
16        let [a, b, c] = triangle.vertices;
17        let normal = normalize(cross(sub(b, a), sub(c, a)));
18        Self {
19            vertices: triangle.vertices,
20            normals: [normal; 3],
21            uvs: [[0.0; 2]; 3],
22            material: triangle.material,
23        }
24    }
25
26    pub fn interpolate(self, barycentric: [f32; 3]) -> ([f32; 3], [f32; 2]) {
27        let normal = normalize(std::array::from_fn(|axis| {
28            (0..3)
29                .map(|corner| self.normals[corner][axis] * barycentric[corner])
30                .sum()
31        }));
32        let uv = std::array::from_fn(|axis| {
33            (0..3)
34                .map(|corner| self.uvs[corner][axis] * barycentric[corner])
35                .sum()
36        });
37        (normal, uv)
38    }
39
40    pub fn triangle(self) -> Triangle {
41        Triangle {
42            vertices: self.vertices,
43            material: self.material,
44        }
45    }
46}
47
48pub type Transform = [[f32; 4]; 4];
49
50#[derive(Clone, Debug, PartialEq)]
51pub struct Instance {
52    pub triangles: Vec<TriangleDetail>,
53    pub transform: Transform,
54    pub surface: InstanceSurface,
55    pub shadow_only: bool,
56    pub two_sided: bool,
57}
58
59#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
60pub enum ContentFace {
61    #[default]
62    Both,
63    Front,
64    Back,
65}
66
67impl ContentFace {
68    pub fn code(self) -> u32 {
69        match self {
70            Self::Both => 0,
71            Self::Front => 1,
72            Self::Back => 2,
73        }
74    }
75
76    pub fn shows(self, front: bool) -> bool {
77        match self {
78            Self::Both => true,
79            Self::Front => front,
80            Self::Back => !front,
81        }
82    }
83}
84
85#[derive(Clone, Copy, Debug, PartialEq)]
86pub struct InstanceSurface {
87    pub clip: [[f32; 4]; 2],
88    pub uv_offset: [f32; 2],
89    pub uv_scale: [f32; 2],
90    pub crop: [f32; 4],
91    pub face: ContentFace,
92    pub layer: ContentLayer,
93    pub cutout: bool,
94    pub casts_shadow: bool,
95    pub alpha_cutoff: f32,
96}
97
98impl Default for InstanceSurface {
99    fn default() -> Self {
100        Self {
101            clip: [[0.0; 4]; 2],
102            uv_offset: [0.0; 2],
103            uv_scale: [1.0; 2],
104            crop: [0.0, 0.0, 1.0, 1.0],
105            face: ContentFace::Both,
106            layer: ContentLayer::default(),
107            cutout: false,
108            casts_shadow: true,
109            alpha_cutoff: 0.5,
110        }
111    }
112}
113
114impl InstanceSurface {
115    pub fn valid(&self) -> bool {
116        self.clip
117            .iter()
118            .flatten()
119            .chain(self.uv_offset.iter())
120            .chain(self.uv_scale.iter())
121            .chain(self.crop.iter())
122            .chain([
123                &self.layer.ink_roughness,
124                &self.layer.emboss,
125                &self.layer.strength,
126            ])
127            .all(|value| value.is_finite())
128            && (0.0..=1.0).contains(&self.alpha_cutoff)
129    }
130
131    pub fn cuts(&self, point: [f32; 3]) -> bool {
132        self.clip.iter().any(|plane| {
133            plane[..3].iter().any(|&value| value != 0.0)
134                && plane[0] * point[0] + plane[1] * point[1] + plane[2] * point[2] > plane[3]
135        })
136    }
137
138    pub fn content_uv(&self, uv: [f32; 2]) -> Option<[f32; 2]> {
139        pfx_materials::content_uv(uv, self.uv_offset, self.uv_scale, self.crop)
140    }
141}
142
143impl Instance {
144    pub fn transformed(&self) -> Result<Vec<TriangleDetail>, String> {
145        let m = self.transform;
146        if m.into_iter().flatten().any(|value| !value.is_finite()) {
147            return Err("instance transform is not finite".into());
148        }
149        if m[0][3] != 0.0 || m[1][3] != 0.0 || m[2][3] != 0.0 || m[3][3] != 1.0 {
150            return Err("instance transform is not affine".into());
151        }
152        let a = |row: usize, column: usize| m[column][row];
153        let cofactor = |row: usize, column: usize| {
154            let (r0, r1) = ((row + 1) % 3, (row + 2) % 3);
155            let (c0, c1) = ((column + 1) % 3, (column + 2) % 3);
156            a(r0, c0) * a(r1, c1) - a(r0, c1) * a(r1, c0)
157        };
158        let determinant: f32 = (0..3)
159            .map(|column| a(0, column) * cofactor(0, column))
160            .sum();
161        if determinant.abs() < 1e-12 {
162            return Err("instance transform is singular".into());
163        }
164        let normal_matrix: [[f32; 3]; 3] = std::array::from_fn(|row| {
165            std::array::from_fn(|column| cofactor(row, column) * determinant.signum())
166        });
167        let triangles = self
168            .triangles
169            .iter()
170            .map(|triangle| TriangleDetail {
171                vertices: triangle.vertices.map(|point| {
172                    std::array::from_fn(|row| {
173                        m[3][row]
174                            + (0..3)
175                                .map(|column| m[column][row] * point[column])
176                                .sum::<f32>()
177                    })
178                }),
179                normals: triangle
180                    .normals
181                    .map(|normal| normalize(normal_matrix.map(|row| dot(row, normal)))),
182                uvs: triangle.uvs,
183                material: triangle.material,
184            })
185            .collect();
186        Ok(triangles)
187    }
188}
189
190#[derive(Clone, Copy, Debug, PartialEq)]
191pub struct Lens {
192    pub shift: [f32; 2],
193    pub aperture: f32,
194    pub focus_distance: f32,
195}
196
197impl Default for Lens {
198    fn default() -> Self {
199        Self {
200            shift: [0.0; 2],
201            aperture: 0.0,
202            focus_distance: 1.0,
203        }
204    }
205}
206
207impl Lens {
208    pub fn valid(self) -> bool {
209        self.shift.into_iter().all(f32::is_finite)
210            && self.aperture.is_finite()
211            && self.aperture >= 0.0
212            && self.focus_distance.is_finite()
213            && self.focus_distance > 0.0
214    }
215}
216
217#[derive(Clone, Debug, PartialEq)]
218pub struct ContentImage {
219    pub width: u32,
220    pub height: u32,
221    pub texels: Vec<[f32; 4]>,
222}
223
224impl ContentImage {
225    pub fn valid(&self) -> bool {
226        self.width > 0
227            && self.height > 0
228            && (self.width as usize)
229                .checked_mul(self.height as usize)
230                .is_some_and(|count| self.texels.len() == count)
231            && self.texels.iter().flatten().all(|value| value.is_finite())
232    }
233}
234
235#[derive(Clone, Debug, PartialEq)]
236pub struct ContentText {
237    pub width: u32,
238    pub height: u32,
239    pub records: Vec<[f32; 4]>,
240}
241
242impl ContentText {
243    pub fn valid(&self) -> bool {
244        self.width > 0
245            && self.height > 0
246            && self.records.len() > 4
247            && self.records.iter().flatten().all(|value| value.is_finite())
248    }
249}
250
251#[derive(Clone, Debug, Default)]
252pub struct Detail {
253    pub triangles: Vec<TriangleDetail>,
254    pub instances: Vec<Instance>,
255    pub lens: Lens,
256    pub projection: crate::gpu::Projection,
257    pub sun_radius_deg: f32,
258    pub content: Vec<Option<ContentImage>>,
259    pub content_slots: Vec<Option<ContentImage>>,
260    pub text_slots: Vec<Option<ContentText>>,
261    pub leaf: Option<LeafShade>,
262    pub steam: Option<Steam>,
263    pub lights: Vec<crate::lights::LocalLight>,
264    pub content_look: ContentLook,
265    pub bounces: Bounces,
266    pub transmissive_shadows: bool,
267    pub clamp_indirect: f32,
268    pub filter_glossy: f32,
269    pub sampler: crate::sobol::Sampler,
270}
271
272#[derive(Clone, Copy, Debug, PartialEq, Eq)]
273pub struct Bounces {
274    pub total: u32,
275    pub diffuse: u32,
276    pub glossy: u32,
277    pub transmission: u32,
278}
279
280impl Bounces {
281    pub const REFERENCE: Bounces = Bounces {
282        total: 16,
283        diffuse: 4,
284        glossy: 8,
285        transmission: 12,
286    };
287    pub const BLENDER: Bounces = Bounces {
288        total: 12,
289        diffuse: 4,
290        glossy: 4,
291        transmission: 12,
292    };
293
294    pub fn deep(count: u32) -> Bounces {
295        Bounces {
296            total: count,
297            diffuse: count,
298            glossy: count,
299            transmission: count,
300        }
301    }
302
303    pub fn valid(self) -> bool {
304        self.total <= 64 && self.diffuse <= 64 && self.glossy <= 64 && self.transmission <= 64
305    }
306}
307
308impl Default for Bounces {
309    fn default() -> Self {
310        Bounces::REFERENCE
311    }
312}
313
314#[derive(Clone, Copy, Debug, PartialEq)]
315pub struct LeafShade {
316    pub center: [f32; 3],
317    pub strength: f32,
318    pub time: f32,
319}
320
321impl LeafShade {
322    pub fn valid(self) -> bool {
323        self.center.into_iter().all(f32::is_finite)
324            && self.strength.is_finite()
325            && (0.0..=1.0).contains(&self.strength)
326            && self.time.is_finite()
327    }
328}
329
330#[derive(Clone, Copy, Debug, PartialEq)]
331pub struct PlumeLook {
332    pub spread: [f32; 2],
333    pub drift: [[f32; 2]; 2],
334    pub sway: [[f32; 5]; 2],
335    pub fade: [f32; 3],
336    pub grain: [f32; 2],
337    pub lift: f32,
338    pub warp: [f32; 2],
339    pub churn: f32,
340    pub threshold: [f32; 2],
341}
342
343impl Default for PlumeLook {
344    fn default() -> Self {
345        Self {
346            spread: [0.7, 4.0],
347            drift: [[-0.9, -0.12], [0.0, 0.02]],
348            sway: [[17.0, 1.3, 0.012, 0.4, 6.0], [13.0, 0.9, 0.01, 0.4, 5.0]],
349            fade: [0.02, 0.07, 0.2],
350            grain: [70.0, 42.0],
351            lift: 0.05,
352            warp: [2.2, 0.2],
353            churn: 0.7,
354            threshold: [0.42, 0.9],
355        }
356    }
357}
358
359impl PlumeLook {
360    pub const FLOATS: usize = 27;
361    pub const NAMES: [&'static str; Self::FLOATS] = [
362        "STEAM_SPREAD_BASE",
363        "STEAM_SPREAD_GROWTH",
364        "STEAM_DRIFT_X_QUAD",
365        "STEAM_DRIFT_X_LIN",
366        "STEAM_DRIFT_Z_QUAD",
367        "STEAM_DRIFT_Z_LIN",
368        "STEAM_SWAY_X_WAVE",
369        "STEAM_SWAY_X_RATE",
370        "STEAM_SWAY_X_AMP",
371        "STEAM_SWAY_X_BASE",
372        "STEAM_SWAY_X_GROWTH",
373        "STEAM_SWAY_Z_WAVE",
374        "STEAM_SWAY_Z_RATE",
375        "STEAM_SWAY_Z_AMP",
376        "STEAM_SWAY_Z_BASE",
377        "STEAM_SWAY_Z_GROWTH",
378        "STEAM_FADE_IN",
379        "STEAM_FADE_OUT_START",
380        "STEAM_FADE_OUT_END",
381        "STEAM_GRAIN_H",
382        "STEAM_GRAIN_V",
383        "STEAM_LIFT_RATE",
384        "STEAM_WARP_AMP",
385        "STEAM_WARP_RATE",
386        "STEAM_CHURN",
387        "STEAM_THRESHOLD_LOW",
388        "STEAM_THRESHOLD_HIGH",
389    ];
390
391    pub fn floats(self) -> [f32; Self::FLOATS] {
392        let [sx, sz] = self.sway;
393        [
394            self.spread[0],
395            self.spread[1],
396            self.drift[0][0],
397            self.drift[0][1],
398            self.drift[1][0],
399            self.drift[1][1],
400            sx[0],
401            sx[1],
402            sx[2],
403            sx[3],
404            sx[4],
405            sz[0],
406            sz[1],
407            sz[2],
408            sz[3],
409            sz[4],
410            self.fade[0],
411            self.fade[1],
412            self.fade[2],
413            self.grain[0],
414            self.grain[1],
415            self.lift,
416            self.warp[0],
417            self.warp[1],
418            self.churn,
419            self.threshold[0],
420            self.threshold[1],
421        ]
422    }
423
424    pub fn valid(self) -> bool {
425        self.floats().into_iter().all(f32::is_finite)
426            && self.spread[0] > 0.0
427            && self.spread[0] + self.spread[1] * self.fade[2] > 0.0
428            && self.fade[0] > 0.0
429            && self.fade[1] < self.fade[2]
430            && self.fade[2] > 0.0
431            && self.threshold[0] < self.threshold[1]
432    }
433}
434
435#[derive(Clone, Copy, Debug, PartialEq)]
436pub struct Steam {
437    pub source: [f32; 3],
438    pub radius: f32,
439    pub density: f32,
440    pub ambient: f32,
441    pub anisotropy: f32,
442    pub box_lo: [f32; 3],
443    pub box_hi: [f32; 3],
444    pub time: f32,
445    pub look: PlumeLook,
446}
447
448impl Steam {
449    pub fn valid(self) -> bool {
450        self.source
451            .into_iter()
452            .chain(self.box_lo)
453            .chain(self.box_hi)
454            .chain([
455                self.radius,
456                self.density,
457                self.ambient,
458                self.anisotropy,
459                self.time,
460            ])
461            .all(f32::is_finite)
462            && self.radius > 0.0
463            && self.density >= 0.0
464            && self.ambient >= 0.0
465            && (-1.0..1.0).contains(&self.anisotropy)
466            && (0..3).all(|axis| self.box_lo[axis] < self.box_hi[axis])
467            && self.look.valid()
468    }
469}
470
471#[cfg(test)]
472mod tests {
473    use super::*;
474
475    #[test]
476    fn the_default_budget_is_the_reference_budget() {
477        assert_eq!(Detail::default().bounces, Bounces::REFERENCE);
478        assert_eq!(
479            Bounces::REFERENCE,
480            Bounces {
481                total: 16,
482                diffuse: 4,
483                glossy: 8,
484                transmission: 12
485            }
486        );
487        assert!(Bounces::deep(64).valid());
488        assert!(!Bounces::deep(65).valid());
489        assert!(
490            !Bounces {
491                glossy: 65,
492                ..Bounces::BLENDER
493            }
494            .valid()
495        );
496    }
497
498    #[test]
499    fn interpolates_normals_and_uvs() {
500        let triangle = TriangleDetail {
501            vertices: [[0.0; 3]; 3],
502            normals: [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]],
503            uvs: [[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]],
504            material: 0,
505        };
506        let (normal, uv) = triangle.interpolate([0.5, 0.25, 0.25]);
507        assert_eq!(uv, [0.25, 0.25]);
508        assert!((normal[0] - 0.8164966).abs() < 1e-6);
509    }
510
511    #[test]
512    fn transforms_instance_positions_and_normals() {
513        let triangle = TriangleDetail {
514            vertices: [[0.0; 3]; 3],
515            normals: [[1.0, 1.0, 0.0]; 3],
516            uvs: [[0.0; 2]; 3],
517            material: 0,
518        };
519        let instance = Instance {
520            triangles: vec![triangle],
521            surface: InstanceSurface::default(),
522            shadow_only: false,
523            two_sided: true,
524            transform: [
525                [2.0, 0.0, 0.0, 0.0],
526                [0.0, 1.0, 0.0, 0.0],
527                [0.0, 0.0, 1.0, 0.0],
528                [3.0, 4.0, 5.0, 1.0],
529            ],
530        };
531        let transformed = instance.transformed().unwrap();
532        assert_eq!(transformed[0].vertices[0], [3.0, 4.0, 5.0]);
533        assert!((transformed[0].normals[0][0] - 0.4472136).abs() < 1e-6);
534    }
535
536    #[test]
537    fn instance_surface_matches_live_clips_and_uvs() {
538        let surface = InstanceSurface {
539            clip: [[1.0, 0.0, 0.0, 0.2], [0.0, -1.0, 0.0, 0.1]],
540            uv_offset: [0.25, 0.0],
541            uv_scale: [0.5, 1.0],
542            crop: [0.25, 0.1, 0.75, 0.9],
543            ..InstanceSurface::default()
544        };
545        assert!(surface.valid());
546        assert!(
547            InstanceSurface {
548                alpha_cutoff: 0.0,
549                ..surface
550            }
551            .valid()
552        );
553        assert!(
554            InstanceSurface {
555                alpha_cutoff: 1.0,
556                ..surface
557            }
558            .valid()
559        );
560        assert!(
561            !InstanceSurface {
562                alpha_cutoff: 1.1,
563                ..surface
564            }
565            .valid()
566        );
567        assert!(
568            !InstanceSurface {
569                alpha_cutoff: f32::NAN,
570                ..surface
571            }
572            .valid()
573        );
574        assert!(!surface.cuts([0.1, 0.0, 0.0]));
575        assert!(surface.cuts([0.3, 0.0, 0.0]));
576        assert!(surface.cuts([0.0, -0.2, 0.0]));
577        assert_eq!(surface.content_uv([0.5, 0.5]), Some([0.5, 0.5]));
578        assert_eq!(surface.content_uv([0.5, 1.0]), None);
579    }
580}