Skip to main content

primase_core/shader/
program_frame.rs

1//! The coordinate frame a source's programs compute in ([`ProgramFrame`]), and how each value a program reads or writes
2//! moves between it and the world's frame ([`SpatialKind`]): one table over every role, so a consumer that runs the
3//! source's programs on the world converts each value by its kind at the one place it binds them, and a new role
4//! states its kind where it is added.
5
6use serde_json::{Value, json};
7
8use super::TextureSource;
9use super::encoding::NormalSpace;
10use super::lights::{LightListPart, LightValue};
11use super::roles::{
12    Attribute, BufferSource, BufferTable, ConstantRole, FrameParameter, FrameTexture, InstanceValue,
13    ItemParameter, ObjectMatrix, ObjectParameter, OutputRole, ViewParameter,
14};
15use crate::frame::SourceFrame;
16use crate::json::JsonError;
17use crate::world::{Resource, World};
18
19/// `core.program_frame`: the coordinate frame the source's game programs compute in, stated by the reader that keeps
20/// them. The world is in core's frame ([`crate::frame`]); a source's programs compute in the frame their game's data is
21/// in, which its reader converted the world out of. A consumer that runs them on the world converts every value they
22/// read into their frame by its kind ([`SpatialKind`]) and reads what they write in it, so the programs compute as they
23/// do in the game. There is no default: a reader states one form or the other, an identity included
24/// (`Source(SourceFrame::WORLD)` for programs that compute in core's frame).
25#[derive(Clone, Debug, PartialEq)]
26pub enum ProgramFrame {
27    /// The programs compute in this frame: `world = frame(program)`, as the reader converted the source's data.
28    Source(SourceFrame),
29    /// The frame the programs compute in is not established for this source, or a consumer's conversion into it is
30    /// not checked: a consumer feeds them world values unconverted and shows `why` with every view of them. `why` names
31    /// the world, what is known of its frame and on what evidence, and what is missing.
32    NotConverted {
33        /// The world, what is known of its programs' frame (with its evidence), and what is not.
34        why: String,
35    },
36}
37
38impl ProgramFrame {
39    /// The frame values are converted into, where one is stated.
40    pub fn frame(&self) -> Option<&SourceFrame> {
41        match self {
42            ProgramFrame::Source(frame) => Some(frame),
43            ProgramFrame::NotConverted { .. } => None,
44        }
45    }
46
47    /// State this as the world's program frame, from a reader or pass that keeps the source's programs: inserted where
48    /// the world has none, kept where it has the same, refused where it has another (two producers of one world's
49    /// programs disagreeing on the frame they compute in).
50    pub fn state(self, world: &mut World) -> Result<(), String> {
51        match world.resource::<ProgramFrame>() {
52            None => {
53                world.insert_resource(self);
54                Ok(())
55            }
56            Some(stated) if *stated == self => Ok(()),
57            Some(stated) => Err(format!(
58                "the world's programs are already stated to compute in {stated:?}, not {self:?}"
59            )),
60        }
61    }
62}
63
64impl Resource for ProgramFrame {
65    const NAME: &'static str = "core.program_frame";
66    fn to_json(&self) -> Result<Option<Value>, JsonError> {
67        Ok(Some(match self {
68            ProgramFrame::Source(frame) => json!({
69                "source": {
70                    "axes": frame.axes().map(|a| format!("{a:?}")),
71                    "metres_per_unit": frame.metres_per_unit(),
72                }
73            }),
74            ProgramFrame::NotConverted { why } => json!({ "not_converted": why }),
75        }))
76    }
77    /// A frame that is not converted says why.
78    fn verify(&self, _w: &World) -> Result<(), String> {
79        match self {
80            ProgramFrame::Source(_) => Ok(()),
81            ProgramFrame::NotConverted { why } if why.trim().is_empty() => {
82                Err("a program frame that is not converted must say why".into())
83            }
84            ProgramFrame::NotConverted { .. } => Ok(()),
85        }
86    }
87}
88
89/// How a value moves from the world's frame into a program frame `C` (`world = C(program)`, a signed axis permutation
90/// times a unit scale): the methods of [`SourceFrame`] that convert each kind are named with it. View space and clip
91/// space are not frames of the world: a world-to-view matrix converted as [`SpatialKind::FromWorld`] takes the program's
92/// positions to the same view space, so values in view or clip space, and lengths measured there, keep theirs.
93#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
94pub enum SpatialKind {
95    /// No coordinates of the world's space (a colour, a count, a texture coordinate, a view-space value): fed as it is.
96    None,
97    /// A position, or an offset between positions, in the world's or an object's space:
98    /// [`SourceFrame::point_to_source`].
99    Point,
100    /// A direction or a normal: [`SourceFrame::direction_to_source`].
101    Direction,
102    /// A tangent, xyz a direction and w the bitangent's sign: [`SourceFrame::tangent_to_source`].
103    Tangent,
104    /// A plane `(n, d)` of the world's space: [`SourceFrame::plane_to_source`].
105    Plane,
106    /// A distance measured in the world's space: [`SourceFrame::length_to_source`].
107    Length,
108    /// One over a distance measured in the world's space: [`SourceFrame::inverse_length_to_source`].
109    InverseLength,
110    /// A matrix that takes world (or object) positions to a space that is not the world's (the view, the clip):
111    /// [`SourceFrame::from_world_matrix_to_source`].
112    FromWorld,
113    /// A matrix that gives world (or object) positions from a space that is not the world's:
114    /// [`SourceFrame::to_world_matrix_to_source`].
115    ToWorld,
116    /// A matrix from one space of the world's frame to another (an object's to the world, a joint's to its model's):
117    /// [`SourceFrame::world_matrix_to_source`].
118    WithinWorld,
119    /// One column of a [`SpatialKind::WithinWorld`] rotation (an instance's axis): the column of the converted matrix,
120    /// which for a permuting frame is another axis's column converted, so a consumer converts the whole rotation.
121    WithinWorldColumn,
122    /// A record of a light table: each field by its own kind ([`LightValue::spatial_kind`]).
123    LightRecord,
124    /// A cube image a program samples by a direction (a reflection probe): its faces must be the program frame's.
125    CubeByDirection,
126    /// A volume image a program samples at a position (light probes): its placement must be the program frame's.
127    VolumeByPosition,
128    /// An image whose texels hold directions (a decal normal buffer): written and read in the program frame.
129    ImageOfDirections,
130}
131
132impl Attribute {
133    /// How the stream moves into a program frame. The reader converted the mesh's positions, normals, tangents and
134    /// points into the world's frame; the other streams hold no coordinates of it.
135    pub fn spatial_kind(self) -> SpatialKind {
136        match self {
137            Attribute::Position | Attribute::Point(_) => SpatialKind::Point,
138            Attribute::Normal => SpatialKind::Direction,
139            Attribute::Tangent(_) => SpatialKind::Tangent,
140            Attribute::TexCoord(_)
141            | Attribute::Color(_)
142            | Attribute::Joints(_)
143            | Attribute::Weights(_)
144            | Attribute::ShaderInput(_) => SpatialKind::None,
145        }
146    }
147}
148
149impl InstanceValue {
150    /// How the instance's value moves into a program frame.
151    pub fn spatial_kind(self) -> SpatialKind {
152        match self {
153            InstanceValue::Translation => SpatialKind::Point,
154            InstanceValue::Axis(_) => SpatialKind::WithinWorldColumn,
155            InstanceValue::Scale | InstanceValue::LodBlend => SpatialKind::None,
156        }
157    }
158}
159
160impl ViewParameter {
161    /// How the view's value moves into a program frame. View and clip space keep theirs.
162    pub fn spatial_kind(self) -> SpatialKind {
163        match self {
164            ViewParameter::CameraPosition => SpatialKind::Point,
165            ViewParameter::View
166            | ViewParameter::ViewProjection
167            | ViewParameter::CameraRelativeViewProjection
168            | ViewParameter::PreviousViewProjection => SpatialKind::FromWorld,
169            ViewParameter::InverseView | ViewParameter::InverseViewProjection => SpatialKind::ToWorld,
170            ViewParameter::Projection
171            | ViewParameter::InverseProjection
172            | ViewParameter::ViewportSize
173            | ViewParameter::InverseViewportSize
174            | ViewParameter::NearPlane
175            | ViewParameter::FarPlane
176            | ViewParameter::InverseFarPlane
177            | ViewParameter::ProjectionFlip
178            | ViewParameter::ResolutionScale
179            | ViewParameter::Perspective
180            | ViewParameter::DepthRange
181            | ViewParameter::DeviceDepthOffset
182            | ViewParameter::DeviceDepthScale => SpatialKind::None,
183        }
184    }
185}
186
187impl ObjectMatrix {
188    /// How the object's transform moves into a program frame: the object's space is converted as the world's is.
189    pub fn spatial_kind(self) -> SpatialKind {
190        match self {
191            ObjectMatrix::World => SpatialKind::WithinWorld,
192            ObjectMatrix::WorldView | ObjectMatrix::WorldViewProjection => SpatialKind::FromWorld,
193        }
194    }
195}
196
197impl ObjectParameter {
198    /// How the object's value moves into a program frame.
199    pub fn spatial_kind(self) -> SpatialKind {
200        match self {
201            ObjectParameter::World
202            | ObjectParameter::InverseWorld
203            | ObjectParameter::BonePalette
204            | ObjectParameter::ClothCancelPalette => SpatialKind::WithinWorld,
205            ObjectParameter::WorldViewProjection | ObjectParameter::WorldView => SpatialKind::FromWorld,
206            ObjectParameter::ViewToObject => SpatialKind::ToWorld,
207            ObjectParameter::PreviousFrame(m)
208            | ObjectParameter::CameraRelative(m)
209            | ObjectParameter::CameraRelativePreviousFrame(m) => m.spatial_kind(),
210            ObjectParameter::ClipPlane => SpatialKind::Plane,
211            ObjectParameter::CameraFadeInverseRange => SpatialKind::InverseLength,
212            ObjectParameter::InstanceColour
213            | ObjectParameter::TextureScroll(_)
214            | ObjectParameter::EnvironmentData
215            | ObjectParameter::MatrixBase
216            | ObjectParameter::InstanceBase
217            | ObjectParameter::ClothSimulation
218            | ObjectParameter::TextureTransformRow(_)
219            | ObjectParameter::PaletteCoordinate
220            | ObjectParameter::CameraFadeEnabled
221            | ObjectParameter::CameraFadeEnd
222            | ObjectParameter::DitherAlphaEnabled
223            | ObjectParameter::DitherEnabled
224            | ObjectParameter::Fade
225            | ObjectParameter::WindOff => SpatialKind::None,
226        }
227    }
228}
229
230impl FrameParameter {
231    /// How the frame's value moves into a program frame.
232    pub fn spatial_kind(self) -> SpatialKind {
233        match self {
234            FrameParameter::LightDirection | FrameParameter::DirectionToLight => SpatialKind::Direction,
235            FrameParameter::Time
236            | FrameParameter::TimeOfDay
237            | FrameParameter::PreExposure
238            | FrameParameter::InversePreExposure
239            | FrameParameter::Exposure
240            | FrameParameter::InverseExposure
241            | FrameParameter::ColourSlot
242            | FrameParameter::LightColour
243            | FrameParameter::LightSpecularColour
244            | FrameParameter::DirectionalAmbientColour
245            | FrameParameter::LightCount(_)
246            | FrameParameter::LightTileSize
247            | FrameParameter::LightTilesPerRow
248            | FrameParameter::LocalShadowAtlasSize
249            | FrameParameter::InverseLocalShadowAtlasSize => SpatialKind::None,
250        }
251    }
252}
253
254impl ItemParameter {
255    /// How the item's value moves into a program frame.
256    pub fn spatial_kind(self) -> SpatialKind {
257        match self {
258            ItemParameter::LightRecord => SpatialKind::LightRecord,
259        }
260    }
261}
262
263impl ConstantRole {
264    /// How the constant moves into a program frame. A material's own words, and a source's defaults, are the source's
265    /// data, already in its programs' frame.
266    pub fn spatial_kind(&self) -> SpatialKind {
267        match self {
268            ConstantRole::View(v) => v.spatial_kind(),
269            ConstantRole::Object(o) => o.spatial_kind(),
270            ConstantRole::Frame(f) => f.spatial_kind(),
271            ConstantRole::Item(i) => i.spatial_kind(),
272            ConstantRole::Material(_)
273            | ConstantRole::Default { .. }
274            | ConstantRole::AlphaReference { .. }
275            | ConstantRole::Unknown { .. } => SpatialKind::None,
276        }
277    }
278}
279
280impl LightValue {
281    /// How the field of a light record moves into a program frame. The records are in view space, which keeps its own,
282    /// but for the point a shadow is rendered from.
283    pub fn spatial_kind(&self) -> SpatialKind {
284        match self {
285            LightValue::ShadowCentre => SpatialKind::Point,
286            LightValue::ViewPosition
287            | LightValue::Range
288            | LightValue::InverseRangeSquared
289            | LightValue::InverseFalloffDistance
290            | LightValue::TranslucencyStrength
291            | LightValue::TranslucencyExponentScale
292            | LightValue::Colour
293            | LightValue::SpecularColour
294            | LightValue::ViewToCone
295            | LightValue::ViewBoundingSphere
296            | LightValue::VerticalConeSphere { .. }
297            | LightValue::ViewDepthMin
298            | LightValue::ViewDepthMax
299            | LightValue::Shadowed
300            | LightValue::ShadowProjection { .. }
301            | LightValue::ShadowFaces { .. }
302            | LightValue::Constant { .. }
303            | LightValue::Unknown { .. } => SpatialKind::None,
304        }
305    }
306}
307
308impl BufferTable {
309    /// How the table's contents move into a program frame: its matrices from one space of the world's frame to
310    /// another (a header word, and an instance record's indices, are none).
311    pub fn spatial_kind(&self) -> SpatialKind {
312        match self {
313            BufferTable::ObjectMatrices { .. }
314            | BufferTable::HeaderedMatrices { .. }
315            | BufferTable::SkinnedMatrices { .. } => SpatialKind::WithinWorld,
316            BufferTable::Instances { .. } => SpatialKind::None,
317        }
318    }
319}
320
321impl BufferSource {
322    /// How the buffer's contents move into a program frame. A graph's own buffers are written and read by the source's
323    /// programs, in their frame.
324    pub fn spatial_kind(&self) -> SpatialKind {
325        match self {
326            BufferSource::Table(t) => t.spatial_kind(),
327            BufferSource::Lights(_) => SpatialKind::LightRecord,
328            BufferSource::LightLists(LightListPart::Lookup(_) | LightListPart::Indices(_))
329            | BufferSource::Graph(_)
330            | BufferSource::Unknown { .. } => SpatialKind::None,
331        }
332    }
333}
334
335impl FrameTexture {
336    /// How the frame's image moves into a program frame.
337    pub fn spatial_kind(self) -> SpatialKind {
338        match self {
339            FrameTexture::ReflectionProbe => SpatialKind::CubeByDirection,
340            FrameTexture::LightProbeVolume => SpatialKind::VolumeByPosition,
341            FrameTexture::DecalBuffer(1) => SpatialKind::ImageOfDirections,
342            FrameTexture::DecalBuffer(_)
343            | FrameTexture::SceneDepth
344            | FrameTexture::SceneColour
345            | FrameTexture::ShadowMap
346            | FrameTexture::ScreenSpaceShadows
347            | FrameTexture::AmbientOcclusion => SpatialKind::None,
348        }
349    }
350}
351
352impl TextureSource {
353    /// How the bound image moves into a program frame. A source's own textures and a graph's own targets are in its
354    /// programs' frame already.
355    pub fn spatial_kind(&self) -> SpatialKind {
356        match self {
357            TextureSource::Frame(t) => t.spatial_kind(),
358            TextureSource::Texture { .. }
359            | TextureSource::Default { .. }
360            | TextureSource::Missing { .. }
361            | TextureSource::Target { .. } => SpatialKind::None,
362        }
363    }
364}
365
366impl OutputRole {
367    /// How what the program writes moves out of its frame: a world-space normal is the program frame's.
368    pub fn spatial_kind(&self) -> SpatialKind {
369        match self {
370            OutputRole::Normal {
371                space: NormalSpace::World,
372                ..
373            } => SpatialKind::Direction,
374            OutputRole::Normal {
375                space: NormalSpace::View | NormalSpace::Tangent,
376                ..
377            }
378            | OutputRole::Albedo
379            | OutputRole::Gloss { .. }
380            | OutputRole::Roughness
381            | OutputRole::Metallic
382            | OutputRole::Occlusion { .. }
383            | OutputRole::SpecularOcclusion
384            | OutputRole::Translucency
385            | OutputRole::Reflectance { .. }
386            | OutputRole::SpecularIntensity { .. }
387            | OutputRole::SpecularExponent { .. }
388            | OutputRole::Fresnel
389            | OutputRole::MotionVector
390            | OutputRole::Emission { .. }
391            | OutputRole::Colour
392            | OutputRole::Coverage
393            | OutputRole::Opacity
394            | OutputRole::Unknown { .. } => SpatialKind::None,
395        }
396    }
397}
398
399#[cfg(test)]
400mod tests {
401    use super::*;
402    use crate::frame::Axis::{NegZ, PosX, PosY};
403
404    const MIRROR: SourceFrame = SourceFrame::new([PosX, PosY, NegZ], 1.0);
405
406    #[test]
407    fn a_frame_is_stated_and_one_not_converted_says_why() {
408        let w = World::new();
409        assert_eq!(ProgramFrame::Source(MIRROR).frame(), Some(&MIRROR));
410        assert_eq!(ProgramFrame::Source(MIRROR).verify(&w), Ok(()));
411        let none = ProgramFrame::NotConverted { why: " ".into() };
412        assert_eq!(none.frame(), None);
413        assert!(none.verify(&w).unwrap_err().contains("why"));
414        let json = ProgramFrame::Source(MIRROR).to_json().unwrap().unwrap();
415        assert_eq!(json["source"]["axes"], json!(["PosX", "PosY", "NegZ"]));
416    }
417
418    /// Every producer of a world's programs states the frame; a second statement agrees or is refused.
419    #[test]
420    fn a_world_states_one_frame() {
421        let mut w = World::new();
422        ProgramFrame::Source(MIRROR).state(&mut w).unwrap();
423        ProgramFrame::Source(MIRROR).state(&mut w).unwrap();
424        assert_eq!(w.resource::<ProgramFrame>(), Some(&ProgramFrame::Source(MIRROR)));
425        let other = ProgramFrame::NotConverted {
426            why: "a world".into(),
427        };
428        assert!(other.state(&mut w).unwrap_err().contains("already stated"));
429        assert_eq!(w.resource::<ProgramFrame>(), Some(&ProgramFrame::Source(MIRROR)));
430    }
431
432    /// The table on the roles whose kind decides where a drawn surface lands and faces: positions and their
433    /// matrices, the light's direction, the camera, and what is left alone.
434    #[test]
435    fn each_role_states_how_it_moves() {
436        assert_eq!(Attribute::Position.spatial_kind(), SpatialKind::Point);
437        assert_eq!(Attribute::Point(3).spatial_kind(), SpatialKind::Point);
438        assert_eq!(Attribute::Normal.spatial_kind(), SpatialKind::Direction);
439        assert_eq!(Attribute::Tangent(0).spatial_kind(), SpatialKind::Tangent);
440        assert_eq!(Attribute::Color(3).spatial_kind(), SpatialKind::None);
441        let constant = |r: ConstantRole| r.spatial_kind();
442        assert_eq!(
443            constant(ConstantRole::View(ViewParameter::CameraPosition)),
444            SpatialKind::Point
445        );
446        assert_eq!(
447            constant(ConstantRole::View(ViewParameter::ViewProjection)),
448            SpatialKind::FromWorld
449        );
450        assert_eq!(
451            constant(ConstantRole::View(ViewParameter::InverseView)),
452            SpatialKind::ToWorld
453        );
454        assert_eq!(
455            constant(ConstantRole::View(ViewParameter::Projection)),
456            SpatialKind::None
457        );
458        assert_eq!(
459            constant(ConstantRole::Object(ObjectParameter::World)),
460            SpatialKind::WithinWorld
461        );
462        assert_eq!(
463            constant(ConstantRole::Object(ObjectParameter::CameraRelative(
464                ObjectMatrix::WorldViewProjection
465            ))),
466            SpatialKind::FromWorld
467        );
468        assert_eq!(
469            constant(ConstantRole::Object(ObjectParameter::ClipPlane)),
470            SpatialKind::Plane
471        );
472        assert_eq!(
473            constant(ConstantRole::Object(ObjectParameter::CameraFadeInverseRange)),
474            SpatialKind::InverseLength
475        );
476        assert_eq!(
477            constant(ConstantRole::Object(ObjectParameter::CameraFadeEnd)),
478            SpatialKind::None
479        );
480        assert_eq!(
481            constant(ConstantRole::Frame(FrameParameter::DirectionToLight)),
482            SpatialKind::Direction
483        );
484        assert_eq!(
485            constant(ConstantRole::Material(vec![0, 0, 0x3f80_0000])),
486            SpatialKind::None
487        );
488        assert_eq!(
489            InstanceValue::Axis(2).spatial_kind(),
490            SpatialKind::WithinWorldColumn
491        );
492        assert_eq!(LightValue::ShadowCentre.spatial_kind(), SpatialKind::Point);
493        assert_eq!(LightValue::ViewPosition.spatial_kind(), SpatialKind::None);
494        assert_eq!(
495            FrameTexture::ReflectionProbe.spatial_kind(),
496            SpatialKind::CubeByDirection
497        );
498    }
499}