Skip to main content

pfx_load/
scene.rs

1mod anim;
2mod bind;
3mod build;
4mod diff;
5mod draws;
6mod edit;
7mod mix;
8mod plates;
9mod play;
10mod pose;
11mod rewrite;
12pub mod tiles;
13mod watch;
14
15#[cfg(test)]
16mod play_tests;
17#[cfg(test)]
18mod tests;
19
20use std::collections::BTreeMap;
21use std::path::{Path, PathBuf};
22use std::sync::Arc;
23
24use pfx_core::anim::Rig;
25use pfx_core::camera::Preset;
26use pfx_core::daylight::Daylight;
27use pfx_core::sky::AnalyticSky;
28use pfx_materials::Library;
29use pfx_post::Chain;
30use pfx_text::{Anchor, Face, Representation, RichParagraph, Span, TextEngine};
31
32use crate::Sky;
33use crate::vec3::{cross, dot, sub};
34
35pub use anim::{Animation, Posed, SPRITE_CLIP, Sprite, content_key, surface};
36pub use bind::{bind, bound, filled};
37pub use build::project_root;
38pub use diff::{Changes, SceneDiff};
39pub use draws::{Draw, Draws};
40pub use edit::{EditError, Kind, Patch, PatchGroup, SceneEdit, Target, Value};
41pub use pfx_scene::Id;
42pub use pfx_scene::ProjectFile;
43pub use pfx_scene::types::{
44    Tunable as DeclaredTunable, TunableKind as DeclaredKind, TunableValue as DeclaredValue,
45};
46pub use plates::{PlateCasters, Plates, Proxies, Proxy, ProxyShape};
47pub use play::{
48    Body, BodyKind, BodyShape, Character, CharacterPlane, Cue, Physics, Sound, Trigger,
49};
50pub use pose::billboard;
51pub use rewrite::{fix, migrate};
52pub use watch::{DEBOUNCE, Reload, SceneWatch};
53
54pub type Matrix = [[f32; 4]; 4];
55
56pub const IDENTITY: Matrix = [
57    [1.0, 0.0, 0.0, 0.0],
58    [0.0, 1.0, 0.0, 0.0],
59    [0.0, 0.0, 1.0, 0.0],
60    [0.0, 0.0, 0.0, 1.0],
61];
62
63pub const DEFAULT_MATERIAL: &str = "(default)";
64
65#[derive(Clone, Debug, PartialEq, Eq)]
66pub struct Location {
67    pub file: PathBuf,
68    pub line: usize,
69    pub column: usize,
70}
71
72#[derive(Clone, Debug, PartialEq, Eq)]
73pub struct SceneError {
74    pub file: PathBuf,
75    pub line: Option<usize>,
76    pub column: Option<usize>,
77    pub message: String,
78    pub related: Vec<Location>,
79}
80
81impl SceneError {
82    pub fn new(file: &Path, line: Option<usize>, message: impl Into<String>) -> Self {
83        Self {
84            file: file.to_path_buf(),
85            line,
86            column: None,
87            message: message.into(),
88            related: Vec::new(),
89        }
90    }
91}
92
93impl std::fmt::Display for SceneError {
94    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95        match (self.line, self.column) {
96            (Some(line), Some(column)) => write!(
97                f,
98                "{}:{line}:{column}: {}",
99                self.file.display(),
100                self.message
101            )?,
102            (Some(line), None) => write!(f, "{}:{line}: {}", self.file.display(), self.message)?,
103            _ => write!(f, "{}: {}", self.file.display(), self.message)?,
104        }
105        for place in &self.related {
106            write!(
107                f,
108                "\n  {}:{}:{}: see here",
109                place.file.display(),
110                place.line,
111                place.column
112            )?;
113        }
114        Ok(())
115    }
116}
117
118impl std::error::Error for SceneError {}
119
120impl From<SceneError> for String {
121    fn from(error: SceneError) -> Self {
122        error.to_string()
123    }
124}
125
126#[derive(Clone, Copy, Debug, PartialEq, Eq)]
127pub enum Shadow {
128    Cast,
129    Only,
130    None,
131}
132
133impl Shadow {
134    pub fn parse(text: &str) -> Option<Self> {
135        match text {
136            "cast" => Some(Self::Cast),
137            "only" => Some(Self::Only),
138            "none" => Some(Self::None),
139            _ => None,
140        }
141    }
142
143    pub fn casts(self) -> bool {
144        self != Self::None
145    }
146
147    pub fn drawn(self) -> bool {
148        self != Self::Only
149    }
150}
151
152#[derive(Clone, Debug, PartialEq)]
153pub struct Geometry {
154    pub positions: Vec<[f32; 3]>,
155    pub normals: Vec<[f32; 3]>,
156    pub tangents: Vec<[f32; 4]>,
157    pub uvs: Vec<[f32; 2]>,
158    pub uvs1: Option<Vec<[f32; 2]>>,
159    pub indices: Vec<u32>,
160    pub skin: Option<Arc<GeometrySkin>>,
161    pub hash: [u8; 32],
162}
163
164#[derive(Clone, Debug, PartialEq)]
165pub struct GeometrySkin {
166    pub joints: Vec<[u16; 4]>,
167    pub weights: Vec<[f32; 4]>,
168}
169
170impl GeometrySkin {
171    pub fn palette(&self) -> usize {
172        self.joints
173            .iter()
174            .zip(&self.weights)
175            .flat_map(|(joints, weights)| {
176                joints
177                    .iter()
178                    .zip(weights)
179                    .filter(|(_, weight)| **weight != 0.0)
180                    .map(|(joint, _)| *joint as usize + 1)
181            })
182            .max()
183            .unwrap_or(1)
184    }
185}
186
187#[derive(Clone, Debug, Default, PartialEq)]
188pub struct NodeOverride {
189    pub material: Option<String>,
190    pub hidden: bool,
191    pub shadow: Option<Shadow>,
192    pub two_sided: Option<bool>,
193}
194
195#[derive(Clone, Debug, PartialEq)]
196pub struct Part {
197    pub node: String,
198    pub joint: Option<usize>,
199    pub transform: Matrix,
200    pub geometry: Arc<Geometry>,
201    pub material: Option<String>,
202    pub overrides: NodeOverride,
203}
204
205#[derive(Clone, Debug, PartialEq)]
206pub struct SceneMesh {
207    pub file: PathBuf,
208    pub node: Option<String>,
209    pub parts: Vec<Part>,
210    pub rig: Option<Arc<Rig>>,
211    pub hash: [u8; 32],
212}
213
214#[derive(Clone, Debug, PartialEq)]
215pub struct Object {
216    pub name: String,
217    pub mesh: String,
218    pub at: [f32; 3],
219    pub rotate: [f32; 3],
220    pub scale: [f32; 3],
221    pub parent: Option<String>,
222    pub model: Matrix,
223    pub material: Option<String>,
224    pub materials: BTreeMap<String, String>,
225    pub shadow: Shadow,
226    pub two_sided: bool,
227    pub hidden: bool,
228    pub clip: Vec<[f32; 4]>,
229    pub content: Option<String>,
230    pub id: u32,
231    pub alpha_cutoff: f32,
232    pub face_camera: bool,
233    pub dynamic: Option<bool>,
234}
235
236pub const ALPHA_CUTOFF: f32 = 0.5;
237
238#[derive(Clone, Copy, Debug, PartialEq)]
239pub struct Sun {
240    pub direction: [f32; 3],
241    pub color: [f32; 3],
242    pub intensity: f32,
243    pub hour: f32,
244    pub daylight: Option<Daylight>,
245    pub reference_hour: f64,
246    pub radius: f32,
247}
248
249impl Sun {
250    pub fn dark() -> Self {
251        Self {
252            direction: [0.0, 1.0, 0.0],
253            color: [1.0; 3],
254            intensity: 0.0,
255            hour: 12.0,
256            daylight: None,
257            reference_hour: pfx_core::daylight::REFERENCE_HOUR,
258            radius: 0.0,
259        }
260    }
261}
262
263#[derive(Clone, Debug)]
264pub enum Environment {
265    Analytic(AnalyticSky),
266    Hdr(Arc<Sky>),
267}
268
269impl Environment {
270    pub fn black() -> Self {
271        Self::Hdr(Arc::new(Sky {
272            width: 1,
273            height: 1,
274            texels: vec![[0.0, 0.0, 0.0, 1.0]],
275        }))
276    }
277}
278
279#[derive(Clone, Debug)]
280pub struct SceneSky {
281    pub kind: String,
282    pub environment: Environment,
283    pub hash: [u8; 32],
284}
285
286impl PartialEq for SceneSky {
287    fn eq(&self, other: &Self) -> bool {
288        self.hash == other.hash
289    }
290}
291
292#[derive(Clone, Copy, Debug, PartialEq)]
293pub struct Light {
294    pub position: [f32; 3],
295    pub color: [f32; 3],
296    pub intensity: f32,
297    pub radius: f32,
298    pub range: f32,
299    pub shadow: bool,
300}
301
302#[derive(Clone, Copy, Debug, PartialEq)]
303pub struct Haze {
304    pub lo: [f32; 3],
305    pub hi: [f32; 3],
306    pub fog: f32,
307    pub smoke: f32,
308    pub mist: f32,
309    pub floor: f32,
310    pub phase: f32,
311    pub back: f32,
312    pub gold: [f32; 3],
313    pub ambient: [f32; 3],
314    pub reach: f32,
315    pub unmapped: f32,
316    pub seed: u32,
317}
318
319#[derive(Clone, Copy, Debug, PartialEq, Eq)]
320pub enum MoverKind {
321    Turn,
322    Slide,
323}
324
325#[derive(Clone, Copy, Debug, PartialEq, Eq)]
326pub enum Motion {
327    Swing,
328    Loop,
329    Once,
330}
331
332impl Motion {
333    pub fn at(self, u: f32) -> f32 {
334        match self {
335            Self::Swing => (1.0 - (std::f32::consts::TAU * u).cos()) * 0.5,
336            Self::Loop => u - u.floor(),
337            Self::Once => u.clamp(0.0, 1.0),
338        }
339    }
340}
341
342#[derive(Clone, Debug, PartialEq)]
343pub struct Mover {
344    pub objects: Vec<String>,
345    pub kind: MoverKind,
346    pub pivot: [f32; 3],
347    pub axis: [f32; 3],
348    pub travel: [f32; 2],
349    pub period: f32,
350    pub motion: Motion,
351    pub offset: f32,
352    pub clip: Vec<[f32; 4]>,
353}
354
355impl Mover {
356    pub fn value(&self, time: f32) -> f32 {
357        let u = (time + self.offset) / self.period;
358        let [from, to] = self.travel;
359        from + (to - from) * self.motion.at(u)
360    }
361
362    pub fn transform(&self, time: f32) -> Matrix {
363        let value = self.value(time);
364        match self.kind {
365            MoverKind::Slide => {
366                let mut out = IDENTITY;
367                out[3] = [
368                    self.axis[0] * value,
369                    self.axis[1] * value,
370                    self.axis[2] * value,
371                    1.0,
372                ];
373                out
374            }
375            MoverKind::Turn => {
376                let [x, y, z] = self.axis;
377                let (s, c) = value.to_radians().sin_cos();
378                let t = 1.0 - c;
379                let r: Matrix = [
380                    [t * x * x + c, t * x * y + s * z, t * x * z - s * y, 0.0],
381                    [t * x * y - s * z, t * y * y + c, t * y * z + s * x, 0.0],
382                    [t * x * z + s * y, t * y * z - s * x, t * z * z + c, 0.0],
383                    [0.0, 0.0, 0.0, 1.0],
384                ];
385                let mut to = IDENTITY;
386                to[3] = [self.pivot[0], self.pivot[1], self.pivot[2], 1.0];
387                let mut back = IDENTITY;
388                back[3] = [-self.pivot[0], -self.pivot[1], -self.pivot[2], 1.0];
389                multiply(to, multiply(r, back))
390            }
391        }
392    }
393}
394
395#[derive(Clone, Copy, Debug, PartialEq)]
396pub struct Emitter {
397    pub position: [f32; 3],
398    pub radius: f32,
399    pub color: [f32; 3],
400    pub intensity: f32,
401}
402
403#[derive(Clone, Copy, Debug, PartialEq)]
404pub enum Projection {
405    Perspective { fov: f32 },
406    Orthographic { height: f32 },
407}
408
409#[derive(Clone, Copy, Debug, PartialEq)]
410pub struct DepthOfField {
411    pub fstop: f32,
412    pub distance: f32,
413    pub focal: f32,
414}
415
416impl DepthOfField {
417    pub fn aperture(&self) -> f32 {
418        self.focal / self.fstop
419    }
420}
421
422#[derive(Clone, Copy, Debug, PartialEq)]
423pub struct Camera {
424    pub projection: Projection,
425    pub at: [f32; 3],
426    pub look_at: [f32; 3],
427    pub up: [f32; 3],
428    pub near: f32,
429    pub far: f32,
430    pub shift: [f32; 2],
431    pub depth_of_field: Option<DepthOfField>,
432    pub preset: Preset,
433}
434
435impl Default for Camera {
436    fn default() -> Self {
437        Self {
438            projection: Projection::Perspective { fov: 40.0 },
439            at: [0.0, 0.0, 5.0],
440            look_at: [0.0; 3],
441            up: [0.0, 1.0, 0.0],
442            near: 0.05,
443            far: 100.0,
444            shift: [0.0; 2],
445            depth_of_field: None,
446            preset: Preset::default(),
447        }
448    }
449}
450
451impl Camera {
452    pub fn axes(&self) -> ([f32; 3], [f32; 3], [f32; 3]) {
453        let forward = unit(sub(self.look_at, self.at));
454        let right = unit(cross(forward, self.up));
455        let up = cross(right, forward);
456        (forward, right, up)
457    }
458
459    pub fn view(&self) -> Matrix {
460        let (forward, right, up) = self.axes();
461        let eye = self.at;
462        [
463            [right[0], up[0], -forward[0], 0.0],
464            [right[1], up[1], -forward[1], 0.0],
465            [right[2], up[2], -forward[2], 0.0],
466            [-dot(right, eye), -dot(up, eye), dot(forward, eye), 1.0],
467        ]
468    }
469
470    pub fn projection(&self, aspect: f32) -> Matrix {
471        let (near, far) = (self.near, self.far);
472        let [sx, sy] = self.shift;
473        match self.projection {
474            Projection::Perspective { fov } => {
475                let tan = (fov.to_radians() * 0.5).tan();
476                [
477                    [1.0 / (tan * aspect), 0.0, 0.0, 0.0],
478                    [0.0, 1.0 / tan, 0.0, 0.0],
479                    [sx, sy, far / (near - far), -1.0],
480                    [0.0, 0.0, far * near / (near - far), 0.0],
481                ]
482            }
483            Projection::Orthographic { height } => {
484                let width = height * aspect;
485                [
486                    [2.0 / width, 0.0, 0.0, 0.0],
487                    [0.0, 2.0 / height, 0.0, 0.0],
488                    [0.0, 0.0, 1.0 / (near - far), 0.0],
489                    [-sx, -sy, near / (near - far), 1.0],
490                ]
491            }
492        }
493    }
494
495    pub fn tan_half_height(&self) -> Option<f32> {
496        match self.projection {
497            Projection::Perspective { fov } => Some((fov.to_radians() * 0.5).tan()),
498            Projection::Orthographic { .. } => None,
499        }
500    }
501}
502
503#[derive(Clone, Debug)]
504pub struct Finish {
505    pub chain: Chain,
506    pub hash: [u8; 32],
507}
508
509impl PartialEq for Finish {
510    fn eq(&self, other: &Self) -> bool {
511        self.hash == other.hash
512    }
513}
514
515#[derive(Clone, Copy, Debug, Default, PartialEq)]
516pub struct TraceSettings {
517    pub transmissive_shadows: bool,
518    pub clamp_indirect: f32,
519    pub filter_glossy: f32,
520}
521
522impl TraceSettings {
523    pub const CLAMP_INDIRECT_MAX: f32 = 10_000.0;
524    pub const FILTER_GLOSSY_MAX: f32 = 1.0;
525
526    pub fn check(&self) -> Result<(), String> {
527        if !(0.0..=Self::CLAMP_INDIRECT_MAX).contains(&self.clamp_indirect) {
528            return Err(format!(
529                "clamp_indirect = {} is outside 0 to {}",
530                self.clamp_indirect,
531                Self::CLAMP_INDIRECT_MAX
532            ));
533        }
534        if !(0.0..=Self::FILTER_GLOSSY_MAX).contains(&self.filter_glossy) {
535            return Err(format!(
536                "filter_glossy = {} is outside 0 to {}",
537                self.filter_glossy,
538                Self::FILTER_GLOSSY_MAX
539            ));
540        }
541        Ok(())
542    }
543}
544
545#[derive(Clone, Debug, PartialEq)]
546pub struct Content {
547    pub image: PathBuf,
548    pub width: u32,
549    pub height: u32,
550    pub srgb: bool,
551    pub rgba: Arc<Vec<u8>>,
552    pub hash: [u8; 32],
553}
554
555#[derive(Clone, Debug, PartialEq)]
556pub struct Text {
557    pub text: String,
558    pub font: PathBuf,
559    pub font_bytes: Arc<Vec<u8>>,
560    pub family: String,
561    pub lit: bool,
562    pub size: f32,
563    pub color: [f32; 4],
564    pub at: [f32; 3],
565    pub rotate: [f32; 3],
566    pub place: Matrix,
567    pub dynamic: bool,
568}
569
570pub const TEXT_NOMINAL: f32 = 1638.4;
571pub const TEXT_WEIGHT: u16 = 400;
572const OPEN: [f32; 4] = [-1e9, -1e9, 1e9, 1e9];
573const FLIP: Matrix = [
574    [1.0, 0.0, 0.0, 0.0],
575    [0.0, -1.0, 0.0, 0.0],
576    [0.0, 0.0, 1.0, 0.0],
577    [0.0, 0.0, 0.0, 1.0],
578];
579
580impl Text {
581    pub fn paragraph(&self) -> Result<RichParagraph, String> {
582        let mut engine = TextEngine::new(&self.font_bytes)
583            .map_err(|error| format!("{}: {error:?}", self.font.display()))?;
584        let spans = [Span {
585            text: self.text.clone(),
586            face: Face {
587                family: self.family.clone(),
588                size: self.size,
589                line: self.size * 1.25,
590                weight: TEXT_WEIGHT,
591                italic: false,
592                spacing: 0.0,
593            },
594            color: self.color,
595        }];
596        let scale = TEXT_NOMINAL / self.size;
597        let block = engine
598            .layout_spans(&spans, None, scale, Anchor::Start)
599            .map_err(|error| format!("{}: {error:?}", self.font.display()))?;
600        let origin = [-block.width * 0.5, -block.height * 0.5];
601        engine
602            .render_spans(
603                &spans,
604                None,
605                scale,
606                Anchor::Start,
607                Representation::Msdf,
608                origin,
609                1.0,
610                OPEN,
611                [0.0; 3],
612            )
613            .map_err(|error| format!("{}: {error:?}", self.font.display()))
614    }
615
616    pub fn model(&self) -> Matrix {
617        multiply(
618            self.place,
619            multiply(model(self.at, self.rotate, [1.0; 3]), FLIP),
620        )
621    }
622}
623
624#[derive(Clone, Debug, Default, PartialEq)]
625pub struct Scene {
626    pub path: PathBuf,
627    pub files: BTreeMap<PathBuf, [u8; 32]>,
628    pub library: Library,
629    pub fallback: Option<String>,
630    pub meshes: BTreeMap<String, SceneMesh>,
631    pub objects: Vec<Object>,
632    pub sun: Option<Sun>,
633    pub sky: Option<SceneSky>,
634    pub haze: Option<Haze>,
635    pub movers: BTreeMap<String, Mover>,
636    pub lights: BTreeMap<String, Light>,
637    pub emitters: BTreeMap<String, Emitter>,
638    pub camera: Option<Camera>,
639    pub finish: Option<Finish>,
640    pub trace: TraceSettings,
641    pub contents: BTreeMap<String, Content>,
642    pub texts: BTreeMap<String, Text>,
643    pub plates: Option<Plates>,
644    pub physics: Option<Physics>,
645    pub bodies: BTreeMap<String, Body>,
646    pub body_layers: BTreeMap<String, String>,
647    pub characters: BTreeMap<String, Character>,
648    pub animations: BTreeMap<String, Animation>,
649    pub triggers: BTreeMap<String, Trigger>,
650    pub layers: BTreeMap<String, Vec<String>>,
651    pub sounds: BTreeMap<String, Sound>,
652    pub warnings: Vec<SceneError>,
653}
654
655impl Scene {
656    pub fn empty() -> Self {
657        Self::default()
658    }
659
660    pub fn open(path: impl AsRef<Path>) -> Result<Self, SceneError> {
661        build::open(path.as_ref(), None)
662    }
663
664    pub fn open_at(path: impl AsRef<Path>, hour: f64) -> Result<Self, SceneError> {
665        build::open(path.as_ref(), Some(hour))
666    }
667
668    pub fn sun_or_dark(&self) -> Sun {
669        self.sun.unwrap_or_else(Sun::dark)
670    }
671
672    pub fn physics_or_default(&self) -> Physics {
673        self.physics.unwrap_or_default()
674    }
675
676    pub fn camera_or_default(&self) -> Camera {
677        self.camera.unwrap_or_default()
678    }
679
680    pub fn environment(&self) -> Environment {
681        self.sky
682            .as_ref()
683            .map(|sky| sky.environment.clone())
684            .unwrap_or_else(Environment::black)
685    }
686
687    pub fn object(&self, name: &str) -> Option<&Object> {
688        self.objects.iter().find(|object| object.name == name)
689    }
690
691    pub fn draws(&self) -> Draws {
692        draws::draws(self)
693    }
694
695    pub fn diff(&self, newer: &Scene) -> SceneDiff {
696        diff::diff(self, newer)
697    }
698
699    pub fn animated(&self) -> bool {
700        !self.movers.is_empty()
701            || self
702                .objects
703                .iter()
704                .any(|object| object.face_camera && !object.hidden)
705    }
706
707    pub fn posed(&self, camera: &Camera, time: f32) -> Vec<Matrix> {
708        pose::objects(self, camera, time)
709    }
710}
711
712pub fn model(at: [f32; 3], rotate: [f32; 3], scale: [f32; 3]) -> Matrix {
713    let [x, y, z] = rotate.map(f32::to_radians);
714    let (sx, cx) = x.sin_cos();
715    let (sy, cy) = y.sin_cos();
716    let (sz, cz) = z.sin_cos();
717    let rx: Matrix = [
718        [1.0, 0.0, 0.0, 0.0],
719        [0.0, cx, sx, 0.0],
720        [0.0, -sx, cx, 0.0],
721        [0.0, 0.0, 0.0, 1.0],
722    ];
723    let ry: Matrix = [
724        [cy, 0.0, -sy, 0.0],
725        [0.0, 1.0, 0.0, 0.0],
726        [sy, 0.0, cy, 0.0],
727        [0.0, 0.0, 0.0, 1.0],
728    ];
729    let rz: Matrix = [
730        [cz, sz, 0.0, 0.0],
731        [-sz, cz, 0.0, 0.0],
732        [0.0, 0.0, 1.0, 0.0],
733        [0.0, 0.0, 0.0, 1.0],
734    ];
735    let s: Matrix = [
736        [scale[0], 0.0, 0.0, 0.0],
737        [0.0, scale[1], 0.0, 0.0],
738        [0.0, 0.0, scale[2], 0.0],
739        [0.0, 0.0, 0.0, 1.0],
740    ];
741    let mut t = IDENTITY;
742    t[3] = [at[0], at[1], at[2], 1.0];
743    multiply(t, multiply(rz, multiply(ry, multiply(rx, s))))
744}
745
746pub fn multiply(a: Matrix, b: Matrix) -> Matrix {
747    let mut out = [[0.0; 4]; 4];
748    for column in 0..4 {
749        for row in 0..4 {
750            out[column][row] = (0..4).map(|k| a[k][row] * b[column][k]).sum();
751        }
752    }
753    out
754}
755
756fn unit(v: [f32; 3]) -> [f32; 3] {
757    let length = dot(v, v).sqrt();
758    if length > 0.0 {
759        v.map(|x| x / length)
760    } else {
761        v
762    }
763}