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}