BlueGit

golden-age paperdoll-renderer

2D preview: map modes show stored atlas pixels as-is (raw RGB depth, 16-bit at display precision)

timfa Grok Bot · pushed by timfa

Authored 2026-11-07 · 6 days ago

Hash
80dff63c79fa8ada744c70f51113123ac2fe915c
Parent
99178c7c
6 files changed +182 −4
README.md +1 −1
@@ -51,7 +51,7 @@ Vanuit **Body** of de bodyweergave opent **2D-preview** de gerenderde compositor
51 51 - Kies **Man / Vrouw**. Alleen exportsets met dezelfde project-/rig-/exportcontract (`CompatKey`) zijn combineerbaar; equipment verschijnt als multiselect wanneer die contracten overeenkomen.
52 52 - Kies een clip uit de body-exportinventaris. **Speel / Pauze**, snelheid en export-fps sturen een looping frame-tijd (elapsed × fps); ontbrekende maps/frames tonen een diagnose in plaats van een stille vervanging.
53 53 - **Yaw − / Yaw +** wrappen over 0…330° (stap 30). **Pitch − / Pitch +** kiezen −60 / −30 / 0 / +30 / +60 met klemmen (start op 0). Hoekwijzigingen behouden de animatietijd.
54 - Kies weergavestand **Resultaat** (standaard; belichte compositie), **Alleen diffuse**, **Alleen normal map**, **Alleen depth map** of **Alleen specular map**. Mapstanden tonen de diepte-gecompositeerde winnende laag onbelicht (depth als grijswaarde, normal als opgeslagen RGB). Playback, hoek en equipment werken in elke stand; licht alleen in Resultaat.
54 - Kies weergavestand **Resultaat** (standaard; belichte compositie), **Alleen diffuse**, **Alleen normal map**, **Alleen depth map** of **Alleen specular map**. Mapstanden tonen per pixel de winnende laag van de dieptecompositie, onbelicht en precies zoals in de atlas opgeslagen, zonder decodering: diffuse, normal en specular als hun RGBA, depth als zijn opgeslagen kanalen (`gray` als grijs, `rgb_bands` en `rgb_packed` als ruwe RGB). 16-bit depth wordt op gewone weergaveprecisie (8 bit per kanaal, afgerond) getoond. Alleen Resultaat decodeert de diepte voor compositie en belichting. Playback, hoek en equipment werken in elke stand; licht alleen in Resultaat.
55 55 - Houd **Azimut / Elevatie** ingedrukt voor continue lichtrotatie in °/s (delta-time); azimut wrapt over 360° zonder klem, elevatie blijft −80…+80. De knoppen volgen de echte muisknopstatus, dus ze blijven draaien zolang je vasthoudt (ook langer dan een seconde). Loslaten, van de knop af bewegen, focusverlies of de pointer die het venster verlaat stopt de beweging. Belichting is alleen voor Resultaat; opgeslagen diffuse blijft unlit albedo.
56 56 - Playback loopt over exact `frames / fps` seconden, zodat ieder geëxporteerd frame (inclusief het laatste) even lang duurt (`1/fps`).
57 57 - Het samengestelde frame gebruikt nearest-neighbor weergave met uniforme schaal en gecentreerde letterboxing, gelijk aan de productie-passpreviews.
@@ -51,7 +51,7 @@ Vanuit **Body** of de bodyweergave opent **2D-preview** de gerenderde compositor
51- Kies **Man / Vrouw**. Alleen exportsets met dezelfde project-/rig-/exportcontract (`CompatKey`) zijn combineerbaar; equipment verschijnt als multiselect wanneer die contracten overeenkomen.
51- Kies **Man / Vrouw**. Alleen exportsets met dezelfde project-/rig-/exportcontract (`CompatKey`) zijn combineerbaar; equipment verschijnt als multiselect wanneer die contracten overeenkomen.
52- Kies een clip uit de body-exportinventaris. **Speel / Pauze**, snelheid en export-fps sturen een looping frame-tijd (elapsed × fps); ontbrekende maps/frames tonen een diagnose in plaats van een stille vervanging.
52- Kies een clip uit de body-exportinventaris. **Speel / Pauze**, snelheid en export-fps sturen een looping frame-tijd (elapsed × fps); ontbrekende maps/frames tonen een diagnose in plaats van een stille vervanging.
53- **Yaw − / Yaw +** wrappen over 0…330° (stap 30). **Pitch − / Pitch +** kiezen −60 / −30 / 0 / +30 / +60 met klemmen (start op 0). Hoekwijzigingen behouden de animatietijd.
53- **Yaw − / Yaw +** wrappen over 0…330° (stap 30). **Pitch − / Pitch +** kiezen −60 / −30 / 0 / +30 / +60 met klemmen (start op 0). Hoekwijzigingen behouden de animatietijd.
54- Kies weergavestand **Resultaat** (standaard; belichte compositie), **Alleen diffuse**, **Alleen normal map**, **Alleen depth map** of **Alleen specular map**. Mapstanden tonen de diepte-gecompositeerde winnende laag onbelicht (depth als grijswaarde, normal als opgeslagen RGB). Playback, hoek en equipment werken in elke stand; licht alleen in Resultaat.
54- Kies weergavestand **Resultaat** (standaard; belichte compositie), **Alleen diffuse**, **Alleen normal map**, **Alleen depth map** of **Alleen specular map**. Mapstanden tonen per pixel de winnende laag van de dieptecompositie, onbelicht en precies zoals in de atlas opgeslagen, zonder decodering: diffuse, normal en specular als hun RGBA, depth als zijn opgeslagen kanalen (`gray` als grijs, `rgb_bands` en `rgb_packed` als ruwe RGB). 16-bit depth wordt op gewone weergaveprecisie (8 bit per kanaal, afgerond) getoond. Alleen Resultaat decodeert de diepte voor compositie en belichting. Playback, hoek en equipment werken in elke stand; licht alleen in Resultaat.
55- Houd **Azimut / Elevatie** ingedrukt voor continue lichtrotatie in °/s (delta-time); azimut wrapt over 360° zonder klem, elevatie blijft −80…+80. De knoppen volgen de echte muisknopstatus, dus ze blijven draaien zolang je vasthoudt (ook langer dan een seconde). Loslaten, van de knop af bewegen, focusverlies of de pointer die het venster verlaat stopt de beweging. Belichting is alleen voor Resultaat; opgeslagen diffuse blijft unlit albedo.
55- Houd **Azimut / Elevatie** ingedrukt voor continue lichtrotatie in °/s (delta-time); azimut wrapt over 360° zonder klem, elevatie blijft −80…+80. De knoppen volgen de echte muisknopstatus, dus ze blijven draaien zolang je vasthoudt (ook langer dan een seconde). Loslaten, van de knop af bewegen, focusverlies of de pointer die het venster verlaat stopt de beweging. Belichting is alleen voor Resultaat; opgeslagen diffuse blijft unlit albedo.
56- Playback loopt over exact `frames / fps` seconden, zodat ieder geëxporteerd frame (inclusief het laatste) even lang duurt (`1/fps`).
56- Playback loopt over exact `frames / fps` seconden, zodat ieder geëxporteerd frame (inclusief het laatste) even lang duurt (`1/fps`).
57- Het samengestelde frame gebruikt nearest-neighbor weergave met uniforme schaal en gecentreerde letterboxing, gelijk aan de productie-passpreviews.
57- Het samengestelde frame gebruikt nearest-neighbor weergave met uniforme schaal en gecentreerde letterboxing, gelijk aan de productie-passpreviews.
src/atlas.rs +35 −0
@@ -850,8 +850,20 @@ impl AtlasSet {
850 850 )));
851 851 }
852 852 let mut maps = BTreeMap::new();
853 let mut depth_display = Vec::new();
853 854 for kind in ATLAS_MAPS {
854 855 let bytes = self.read_map(kind)?;
856 if kind == MapKind::Depth {
857 let fmt = self.ppda.depth_format()?;
858 let native = export_set::decode_depth_native(
859 &bytes,
860 self.layout.width,
861 self.layout.height,
862 MAX_ATLAS_DIM,
863 fmt,
864 )?;
865 depth_display = export_set::depth_display_rgba(&native, fmt);
866 }
855 867 let image = export_set::decode_map_sized(
856 868 &bytes,
857 869 kind,
@@ -865,6 +877,7 @@ impl AtlasSet {
865 877 Ok(LoadedAtlas {
866 878 set: self.clone(),
867 879 maps,
880 depth_display,
868 881 })
869 882 }
870 883 }
@@ -874,6 +887,8 @@ impl AtlasSet {
874 887 pub struct LoadedAtlas {
875 888 pub set: AtlasSet,
876 889 maps: BTreeMap<&'static str, MapImage>,
890 /// Depth atlas as stored, RGBA8 display form (no decoding; Tim t992u).
891 depth_display: Vec<u8>,
877 892 }
878 893
879 894 impl LoadedAtlas {
@@ -907,6 +922,26 @@ impl LoadedAtlas {
907 922 .ok_or_else(|| ExportError::Incomplete(format!("hoek p{pitch} y{yaw} niet in .ppda")))
908 923 }
909 924
925 /// Crop the depth cell as stored, in RGBA8 display form (rgb_bands /
926 /// rgb_packed keep their raw RGB; 16-bit at display precision).
927 pub fn depth_display_cell(
928 &self,
929 clip_token: &str,
930 frame: u32,
931 pitch: i32,
932 yaw: u32,
933 ) -> Result<Vec<u8>, ExportError> {
934 let v = self.view(clip_token, frame, pitch, yaw)?;
935 let atlas_w = self.set.layout.width as usize;
936 let (x, y, w, h) = (v.x as usize, v.y as usize, v.w as usize, v.h as usize);
937 let mut out = Vec::with_capacity(w * h * 4);
938 for row in y..y + h {
939 let start = (row * atlas_w + x) * 4;
940 out.extend_from_slice(&self.depth_display[start..start + w * 4]);
941 }
942 Ok(out)
943 }
944
910 945 /// Crop one map cell exactly as stored.
911 946 pub fn cell(
912 947 &self,
@@ -850,8 +850,20 @@ impl AtlasSet {
850 )));
850 )));
851 }
851 }
852 let mut maps = BTreeMap::new();
852 let mut maps = BTreeMap::new();
853 let mut depth_display = Vec::new();
853 for kind in ATLAS_MAPS {
854 for kind in ATLAS_MAPS {
854 let bytes = self.read_map(kind)?;
855 let bytes = self.read_map(kind)?;
856 if kind == MapKind::Depth {
857 let fmt = self.ppda.depth_format()?;
858 let native = export_set::decode_depth_native(
859 &bytes,
860 self.layout.width,
861 self.layout.height,
862 MAX_ATLAS_DIM,
863 fmt,
864 )?;
865 depth_display = export_set::depth_display_rgba(&native, fmt);
866 }
855 let image = export_set::decode_map_sized(
867 let image = export_set::decode_map_sized(
856 &bytes,
868 &bytes,
857 kind,
869 kind,
@@ -865,6 +877,7 @@ impl AtlasSet {
865 Ok(LoadedAtlas {
877 Ok(LoadedAtlas {
866 set: self.clone(),
878 set: self.clone(),
867 maps,
879 maps,
880 depth_display,
868 })
881 })
869 }
882 }
870}
883}
@@ -874,6 +887,8 @@ impl AtlasSet {
874pub struct LoadedAtlas {
887pub struct LoadedAtlas {
875 pub set: AtlasSet,
888 pub set: AtlasSet,
876 maps: BTreeMap<&'static str, MapImage>,
889 maps: BTreeMap<&'static str, MapImage>,
890 /// Depth atlas as stored, RGBA8 display form (no decoding; Tim t992u).
891 depth_display: Vec<u8>,
877}
892}
878
893
879impl LoadedAtlas {
894impl LoadedAtlas {
@@ -907,6 +922,26 @@ impl LoadedAtlas {
907 .ok_or_else(|| ExportError::Incomplete(format!("hoek p{pitch} y{yaw} niet in .ppda")))
922 .ok_or_else(|| ExportError::Incomplete(format!("hoek p{pitch} y{yaw} niet in .ppda")))
908 }
923 }
909
924
925 /// Crop the depth cell as stored, in RGBA8 display form (rgb_bands /
926 /// rgb_packed keep their raw RGB; 16-bit at display precision).
927 pub fn depth_display_cell(
928 &self,
929 clip_token: &str,
930 frame: u32,
931 pitch: i32,
932 yaw: u32,
933 ) -> Result<Vec<u8>, ExportError> {
934 let v = self.view(clip_token, frame, pitch, yaw)?;
935 let atlas_w = self.set.layout.width as usize;
936 let (x, y, w, h) = (v.x as usize, v.y as usize, v.w as usize, v.h as usize);
937 let mut out = Vec::with_capacity(w * h * 4);
938 for row in y..y + h {
939 let start = (row * atlas_w + x) * 4;
940 out.extend_from_slice(&self.depth_display[start..start + w * 4]);
941 }
942 Ok(out)
943 }
944
910 /// Crop one map cell exactly as stored.
945 /// Crop one map cell exactly as stored.
911 pub fn cell(
946 pub fn cell(
912 &self,
947 &self,
src/compositor.rs +28 −1
@@ -94,6 +94,9 @@ pub struct LayerMaps {
94 94 pub specular: Vec<u8>,
95 95 /// Interleaved gray, alpha (`width*height*2`), same layout as `MapImage::Depth16`.
96 96 pub depth: Vec<u16>,
97 /// Depth pixels as stored, RGBA8 display form (`width*height*4`), when the
98 /// source provides them (atlas reader). Only for map inspection (t992u).
99 pub depth_display: Option<Vec<u8>>,
97 100 }
98 101
99 102 impl LayerMaps {
@@ -133,6 +136,7 @@ impl LayerMaps {
133 136 normal: nrgba,
134 137 specular: srgba,
135 138 depth: samples,
139 depth_display: None,
136 140 })
137 141 }
138 142
@@ -165,7 +169,15 @@ impl LayerMaps {
165 169 let normal = atlas.cell(clip_token, frame, pitch, yaw, MapKind::Normal)?;
166 170 let specular = atlas.cell(clip_token, frame, pitch, yaw, MapKind::Specular)?;
167 171 let depth = atlas.cell(clip_token, frame, pitch, yaw, MapKind::Depth)?;
168 Self::from_maps(label, &diffuse, &normal, &specular, &depth)
172 let mut layer = Self::from_maps(label, &diffuse, &normal, &specular, &depth)?;
173 let display = atlas.depth_display_cell(clip_token, frame, pitch, yaw)?;
174 if display.len() != layer.depth.len() * 2 {
175 return Err(ComposeError::Map(
176 "depth-weergave past niet bij de cel".into(),
177 ));
178 }
179 layer.depth_display = Some(display);
180 Ok(layer)
169 181 }
170 182 }
171 183
@@ -313,6 +325,9 @@ pub struct ComposedMaps {
313 325 pub normal: Vec<u8>,
314 326 pub specular: Vec<u8>,
315 327 pub depth: Vec<u16>,
328 /// Winning layer's stored depth pixel (RGBA8 display form) when every layer
329 /// carries one; `None` otherwise.
330 pub depth_display: Option<Vec<u8>>,
316 331 pub winner: Vec<u8>,
317 332 }
318 333
@@ -358,6 +373,12 @@ pub fn compose(layers: &[LayerMaps]) -> Result<ComposedMaps, ComposeError> {
358 373 let mut specular = vec![0u8; pixels * 4];
359 374 let mut depth = vec![0u16; pixels * 2];
360 375 let mut winner = vec![u8::MAX; pixels];
376 let all_display = layers.iter().all(|l| {
377 l.depth_display
378 .as_ref()
379 .is_some_and(|d| d.len() == pixels * 4)
380 });
381 let mut depth_display = all_display.then(|| vec![0u8; pixels * 4]);
361 382
362 383 for i in 0..pixels {
363 384 let mut best_depth: Option<u16> = None;
@@ -395,6 +416,9 @@ pub fn compose(layers: &[LayerMaps]) -> Result<ComposedMaps, ComposeError> {
395 416 specular[o..o + 4].copy_from_slice(&layer.specular[o..o + 4]);
396 417 depth[i * 2] = layer.depth[i * 2];
397 418 depth[i * 2 + 1] = layer.depth[i * 2 + 1];
419 if let (Some(out), Some(src)) = (depth_display.as_mut(), layer.depth_display.as_ref()) {
420 out[o..o + 4].copy_from_slice(&src[o..o + 4]);
421 }
398 422 winner[i] = layer_idx as u8;
399 423 }
400 424 }
@@ -406,6 +430,7 @@ pub fn compose(layers: &[LayerMaps]) -> Result<ComposedMaps, ComposeError> {
406 430 normal,
407 431 specular,
408 432 depth,
433 depth_display,
409 434 winner,
410 435 })
411 436 }
@@ -609,6 +634,7 @@ mod tests {
609 634 .cycle()
610 635 .take(pixels * 2)
611 636 .collect(),
637 depth_display: None,
612 638 }
613 639 }
614 640
@@ -639,6 +665,7 @@ mod tests {
639 665 normal,
640 666 specular,
641 667 depth,
668 depth_display: None,
642 669 }
643 670 }
644 671
@@ -94,6 +94,9 @@ pub struct LayerMaps {
94 pub specular: Vec<u8>,
94 pub specular: Vec<u8>,
95 /// Interleaved gray, alpha (`width*height*2`), same layout as `MapImage::Depth16`.
95 /// Interleaved gray, alpha (`width*height*2`), same layout as `MapImage::Depth16`.
96 pub depth: Vec<u16>,
96 pub depth: Vec<u16>,
97 /// Depth pixels as stored, RGBA8 display form (`width*height*4`), when the
98 /// source provides them (atlas reader). Only for map inspection (t992u).
99 pub depth_display: Option<Vec<u8>>,
97}
100}
98
101
99impl LayerMaps {
102impl LayerMaps {
@@ -133,6 +136,7 @@ impl LayerMaps {
133 normal: nrgba,
136 normal: nrgba,
134 specular: srgba,
137 specular: srgba,
135 depth: samples,
138 depth: samples,
139 depth_display: None,
136 })
140 })
137 }
141 }
138
142
@@ -165,7 +169,15 @@ impl LayerMaps {
165 let normal = atlas.cell(clip_token, frame, pitch, yaw, MapKind::Normal)?;
169 let normal = atlas.cell(clip_token, frame, pitch, yaw, MapKind::Normal)?;
166 let specular = atlas.cell(clip_token, frame, pitch, yaw, MapKind::Specular)?;
170 let specular = atlas.cell(clip_token, frame, pitch, yaw, MapKind::Specular)?;
167 let depth = atlas.cell(clip_token, frame, pitch, yaw, MapKind::Depth)?;
171 let depth = atlas.cell(clip_token, frame, pitch, yaw, MapKind::Depth)?;
168 Self::from_maps(label, &diffuse, &normal, &specular, &depth)
172 let mut layer = Self::from_maps(label, &diffuse, &normal, &specular, &depth)?;
173 let display = atlas.depth_display_cell(clip_token, frame, pitch, yaw)?;
174 if display.len() != layer.depth.len() * 2 {
175 return Err(ComposeError::Map(
176 "depth-weergave past niet bij de cel".into(),
177 ));
178 }
179 layer.depth_display = Some(display);
180 Ok(layer)
169 }
181 }
170}
182}
171
183
@@ -313,6 +325,9 @@ pub struct ComposedMaps {
313 pub normal: Vec<u8>,
325 pub normal: Vec<u8>,
314 pub specular: Vec<u8>,
326 pub specular: Vec<u8>,
315 pub depth: Vec<u16>,
327 pub depth: Vec<u16>,
328 /// Winning layer's stored depth pixel (RGBA8 display form) when every layer
329 /// carries one; `None` otherwise.
330 pub depth_display: Option<Vec<u8>>,
316 pub winner: Vec<u8>,
331 pub winner: Vec<u8>,
317}
332}
318
333
@@ -358,6 +373,12 @@ pub fn compose(layers: &[LayerMaps]) -> Result<ComposedMaps, ComposeError> {
358 let mut specular = vec![0u8; pixels * 4];
373 let mut specular = vec![0u8; pixels * 4];
359 let mut depth = vec![0u16; pixels * 2];
374 let mut depth = vec![0u16; pixels * 2];
360 let mut winner = vec![u8::MAX; pixels];
375 let mut winner = vec![u8::MAX; pixels];
376 let all_display = layers.iter().all(|l| {
377 l.depth_display
378 .as_ref()
379 .is_some_and(|d| d.len() == pixels * 4)
380 });
381 let mut depth_display = all_display.then(|| vec![0u8; pixels * 4]);
361
382
362 for i in 0..pixels {
383 for i in 0..pixels {
363 let mut best_depth: Option<u16> = None;
384 let mut best_depth: Option<u16> = None;
@@ -395,6 +416,9 @@ pub fn compose(layers: &[LayerMaps]) -> Result<ComposedMaps, ComposeError> {
395 specular[o..o + 4].copy_from_slice(&layer.specular[o..o + 4]);
416 specular[o..o + 4].copy_from_slice(&layer.specular[o..o + 4]);
396 depth[i * 2] = layer.depth[i * 2];
417 depth[i * 2] = layer.depth[i * 2];
397 depth[i * 2 + 1] = layer.depth[i * 2 + 1];
418 depth[i * 2 + 1] = layer.depth[i * 2 + 1];
419 if let (Some(out), Some(src)) = (depth_display.as_mut(), layer.depth_display.as_ref()) {
420 out[o..o + 4].copy_from_slice(&src[o..o + 4]);
421 }
398 winner[i] = layer_idx as u8;
422 winner[i] = layer_idx as u8;
399 }
423 }
400 }
424 }
@@ -406,6 +430,7 @@ pub fn compose(layers: &[LayerMaps]) -> Result<ComposedMaps, ComposeError> {
406 normal,
430 normal,
407 specular,
431 specular,
408 depth,
432 depth,
433 depth_display,
409 winner,
434 winner,
410 })
435 })
411}
436}
@@ -609,6 +634,7 @@ mod tests {
609 .cycle()
634 .cycle()
610 .take(pixels * 2)
635 .take(pixels * 2)
611 .collect(),
636 .collect(),
637 depth_display: None,
612 }
638 }
613 }
639 }
614
640
@@ -639,6 +665,7 @@ mod tests {
639 normal,
665 normal,
640 specular,
666 specular,
641 depth,
667 depth,
668 depth_display: None,
642 }
669 }
643 }
670 }
644
671
src/editor.rs +1 −1
@@ -2001,7 +2001,7 @@ impl EditorApp {
2001 2001 set_view_mode = Some(mode);
2002 2002 }
2003 2003 }
2004 ui.weak("Resultaat = belichte compositie; andere standen tonen de ongebelichte map.");
2004 ui.weak("Resultaat = belichte compositie; andere standen tonen de map zoals opgeslagen (onbelicht, niet gedecodeerd).");
2005 2005 ui.separator();
2006 2006 ui.label("Hoek (export)");
2007 2007 ui.horizontal(|ui| {
@@ -2001,7 +2001,7 @@ impl EditorApp {
2001 set_view_mode = Some(mode);
2001 set_view_mode = Some(mode);
2002 }
2002 }
2003 }
2003 }
2004 ui.weak("Resultaat = belichte compositie; andere standen tonen de ongebelichte map.");
2004 ui.weak("Resultaat = belichte compositie; andere standen tonen de map zoals opgeslagen (onbelicht, niet gedecodeerd).");
2005 ui.separator();
2005 ui.separator();
2006 ui.label("Hoek (export)");
2006 ui.label("Hoek (export)");
2007 ui.horizontal(|ui| {
2007 ui.horizontal(|ui| {
src/export_set.rs +27 −0
@@ -790,6 +790,33 @@ pub(crate) fn decode_depth_native(
790 790 Ok(buf)
791 791 }
792 792
793 /// Stored depth pixels in `fmt` → RGBA8 for display, as stored (Tim t992u): no
794 /// decoding of rgb_bands / rgb_packed. Gray shows as gray, RGB channels show as
795 /// their own RGB, alpha stays alpha; 16-bit samples are reduced to 8-bit display
796 /// precision by rounding (`v·255/65535`).
797 pub(crate) fn depth_display_rgba(native: &[u8], fmt: crate::camera::DepthFormat) -> Vec<u8> {
798 let wide = fmt.bits == 16;
799 let sample = |px: &[u8], c: usize| -> u8 {
800 if wide {
801 let v = u32::from(u16::from_be_bytes([px[c * 2], px[c * 2 + 1]]));
802 ((v * 255 + 32767) / 65535) as u8
803 } else {
804 px[c]
805 }
806 };
807 let bpp = fmt.bytes_per_pixel();
808 let mut out = Vec::with_capacity(native.len() / bpp * 4);
809 for px in native.chunks_exact(bpp) {
810 if fmt.channels() == 2 {
811 let g = sample(px, 0);
812 out.extend_from_slice(&[g, g, g, sample(px, 1)]);
813 } else {
814 out.extend_from_slice(&[sample(px, 0), sample(px, 1), sample(px, 2), sample(px, 3)]);
815 }
816 }
817 out
818 }
819
793 820 /// Raw depth samples in `fmt` → canonical interleaved 16-bit (depth, alpha).
794 821 pub(crate) fn canonical_depth(
795 822 native: &[u8],
@@ -790,6 +790,33 @@ pub(crate) fn decode_depth_native(
790 Ok(buf)
790 Ok(buf)
791}
791}
792
792
793/// Stored depth pixels in `fmt` → RGBA8 for display, as stored (Tim t992u): no
794/// decoding of rgb_bands / rgb_packed. Gray shows as gray, RGB channels show as
795/// their own RGB, alpha stays alpha; 16-bit samples are reduced to 8-bit display
796/// precision by rounding (`v·255/65535`).
797pub(crate) fn depth_display_rgba(native: &[u8], fmt: crate::camera::DepthFormat) -> Vec<u8> {
798 let wide = fmt.bits == 16;
799 let sample = |px: &[u8], c: usize| -> u8 {
800 if wide {
801 let v = u32::from(u16::from_be_bytes([px[c * 2], px[c * 2 + 1]]));
802 ((v * 255 + 32767) / 65535) as u8
803 } else {
804 px[c]
805 }
806 };
807 let bpp = fmt.bytes_per_pixel();
808 let mut out = Vec::with_capacity(native.len() / bpp * 4);
809 for px in native.chunks_exact(bpp) {
810 if fmt.channels() == 2 {
811 let g = sample(px, 0);
812 out.extend_from_slice(&[g, g, g, sample(px, 1)]);
813 } else {
814 out.extend_from_slice(&[sample(px, 0), sample(px, 1), sample(px, 2), sample(px, 3)]);
815 }
816 }
817 out
818}
819
793/// Raw depth samples in `fmt` → canonical interleaved 16-bit (depth, alpha).
820/// Raw depth samples in `fmt` → canonical interleaved 16-bit (depth, alpha).
794pub(crate) fn canonical_depth(
821pub(crate) fn canonical_depth(
795 native: &[u8],
822 native: &[u8],
src/preview2d.rs +90 −1
@@ -65,7 +65,7 @@ pub enum ViewMode {
65 65 DiffuseOnly,
66 66 /// Depth-composited normal map as stored RGB, unlit.
67 67 NormalOnly,
68 /// Depth-composited depth as grayscale, unlit.
68 /// Depth-composited depth map as stored (raw RGB for rgb_bands/rgb_packed), unlit.
69 69 DepthOnly,
70 70 /// Depth-composited specular map as stored RGB, unlit.
71 71 SpecularOnly,
@@ -115,7 +115,18 @@ pub fn composed_map_image(composed: &ComposedMaps, mode: ViewMode) -> ImageBuf {
115 115 ViewMode::SpecularOnly => {
116 116 rgba.copy_from_slice(&composed.specular);
117 117 }
118 ViewMode::DepthOnly if composed.depth_display.is_some() => {
119 // As stored in the atlas (Tim t992u): rgb_bands / rgb_packed show
120 // their raw RGB, 16-bit at display precision; no decoding.
121 let display = composed.depth_display.as_ref().unwrap();
122 for i in 0..pixels {
123 if composed.winner[i] != u8::MAX {
124 rgba[i * 4..i * 4 + 4].copy_from_slice(&display[i * 4..i * 4 + 4]);
125 }
126 }
127 }
118 128 ViewMode::DepthOnly => {
129 // Sources without stored pixels (synthetic layers): canonical gray.
119 130 for i in 0..pixels {
120 131 let o = i * 4;
121 132 if composed.winner[i] == u8::MAX {
@@ -1229,6 +1240,84 @@ mod tests {
1229 1240 }
1230 1241 }
1231 1242
1243 #[test]
1244 fn depth_view_shows_stored_pixels_as_is_resultaat_still_decodes() {
1245 use crate::camera::{DepthEncoding, DepthFormat};
1246 let depth = 40000u16;
1247 let mut gray16_result = None;
1248 for bits in [16u8, 8] {
1249 for encoding in DepthEncoding::ALL {
1250 let fmt = DepthFormat::new(bits, encoding).unwrap();
1251 let project = Temp::new();
1252 let dir = project.0.join("renders/male-body-atlas");
1253 let mut spec = SyntheticRun::body("demo", "male", b"b");
1254 spec.clips = vec![("walk".into(), 1, 0.5)];
1255 spec.target = [2, 2];
1256 spec.depth_format = fmt;
1257 spec.write(&dir, |_, _, _, _| {
1258 pixel(
1259 [200, 100, 50, 255],
1260 [128, 128, 255, 255],
1261 [0, 0, 0, 255],
1262 depth,
1263 )
1264 })
1265 .unwrap();
1266 let catalog = discover_completed_runs(&project.0);
1267 let (body, _) = open_selection(&catalog, Sex::Male, &[]).unwrap();
1268 let request = |view_mode| ComposeRequest {
1269 body: &body,
1270 equipment: &[],
1271 clip_token: "clip000-walk",
1272 frame: 0,
1273 pitch: 0,
1274 yaw: 0,
1275 light: Light::default(),
1276 view_mode,
1277 };
1278 let shown = compose_preview_frame(request(ViewMode::DepthOnly)).unwrap();
1279 // Expected: the stored channels, untranslated; 16-bit → 8-bit display.
1280 let raw = fmt.encode_value(f64::from(depth) / 65535.);
1281 let to8 = |v: u64| {
1282 if bits == 16 {
1283 ((v * 255 + 32767) / 65535) as u8
1284 } else {
1285 v as u8
1286 }
1287 };
1288 let want = match encoding {
1289 DepthEncoding::Gray => [to8(raw[0]), to8(raw[0]), to8(raw[0]), 255],
1290 _ => [to8(raw[0]), to8(raw[1]), to8(raw[2]), 255],
1291 };
1292 for px in shown.rgba.chunks_exact(4) {
1293 assert_eq!(px, want, "{fmt:?}");
1294 }
1295 // 16-bit rgb_packed of a 16-bit canonical value d stores d in every
1296 // channel (d/65535 repeats in base 65536), so only check the others.
1297 if encoding == DepthEncoding::RgbBands
1298 || (encoding == DepthEncoding::RgbPacked && bits == 8)
1299 {
1300 assert!(
1301 want[0] != want[1] || want[1] != want[2],
1302 "{fmt:?}: raw RGB must not look like decoded gray: {want:?}"
1303 );
1304 }
1305 // Resultaat decodes depth: same relit image as gray16 within 1 level.
1306 let lit = compose_preview_frame(request(ViewMode::Resultaat)).unwrap();
1307 if bits == 16 {
1308 match &gray16_result {
1309 None => gray16_result = Some(lit.rgba.clone()),
1310 Some(reference) => {
1311 for (a, b) in lit.rgba.iter().zip(reference) {
1312 assert!((*a as i32 - *b as i32).abs() <= 1, "{fmt:?}");
1313 }
1314 }
1315 }
1316 }
1317 }
1318 }
1319 }
1320
1232 1321 #[test]
1233 1322 fn five_pitch_rows_clamp_and_default_is_zero() {
1234 1323 let mut state = Preview2dState::default();
@@ -65,7 +65,7 @@ pub enum ViewMode {
65 DiffuseOnly,
65 DiffuseOnly,
66 /// Depth-composited normal map as stored RGB, unlit.
66 /// Depth-composited normal map as stored RGB, unlit.
67 NormalOnly,
67 NormalOnly,
68 /// Depth-composited depth as grayscale, unlit.
68 /// Depth-composited depth map as stored (raw RGB for rgb_bands/rgb_packed), unlit.
69 DepthOnly,
69 DepthOnly,
70 /// Depth-composited specular map as stored RGB, unlit.
70 /// Depth-composited specular map as stored RGB, unlit.
71 SpecularOnly,
71 SpecularOnly,
@@ -115,7 +115,18 @@ pub fn composed_map_image(composed: &ComposedMaps, mode: ViewMode) -> ImageBuf {
115 ViewMode::SpecularOnly => {
115 ViewMode::SpecularOnly => {
116 rgba.copy_from_slice(&composed.specular);
116 rgba.copy_from_slice(&composed.specular);
117 }
117 }
118 ViewMode::DepthOnly if composed.depth_display.is_some() => {
119 // As stored in the atlas (Tim t992u): rgb_bands / rgb_packed show
120 // their raw RGB, 16-bit at display precision; no decoding.
121 let display = composed.depth_display.as_ref().unwrap();
122 for i in 0..pixels {
123 if composed.winner[i] != u8::MAX {
124 rgba[i * 4..i * 4 + 4].copy_from_slice(&display[i * 4..i * 4 + 4]);
125 }
126 }
127 }
118 ViewMode::DepthOnly => {
128 ViewMode::DepthOnly => {
129 // Sources without stored pixels (synthetic layers): canonical gray.
119 for i in 0..pixels {
130 for i in 0..pixels {
120 let o = i * 4;
131 let o = i * 4;
121 if composed.winner[i] == u8::MAX {
132 if composed.winner[i] == u8::MAX {
@@ -1229,6 +1240,84 @@ mod tests {
1229 }
1240 }
1230 }
1241 }
1231
1242
1243 #[test]
1244 fn depth_view_shows_stored_pixels_as_is_resultaat_still_decodes() {
1245 use crate::camera::{DepthEncoding, DepthFormat};
1246 let depth = 40000u16;
1247 let mut gray16_result = None;
1248 for bits in [16u8, 8] {
1249 for encoding in DepthEncoding::ALL {
1250 let fmt = DepthFormat::new(bits, encoding).unwrap();
1251 let project = Temp::new();
1252 let dir = project.0.join("renders/male-body-atlas");
1253 let mut spec = SyntheticRun::body("demo", "male", b"b");
1254 spec.clips = vec![("walk".into(), 1, 0.5)];
1255 spec.target = [2, 2];
1256 spec.depth_format = fmt;
1257 spec.write(&dir, |_, _, _, _| {
1258 pixel(
1259 [200, 100, 50, 255],
1260 [128, 128, 255, 255],
1261 [0, 0, 0, 255],
1262 depth,
1263 )
1264 })
1265 .unwrap();
1266 let catalog = discover_completed_runs(&project.0);
1267 let (body, _) = open_selection(&catalog, Sex::Male, &[]).unwrap();
1268 let request = |view_mode| ComposeRequest {
1269 body: &body,
1270 equipment: &[],
1271 clip_token: "clip000-walk",
1272 frame: 0,
1273 pitch: 0,
1274 yaw: 0,
1275 light: Light::default(),
1276 view_mode,
1277 };
1278 let shown = compose_preview_frame(request(ViewMode::DepthOnly)).unwrap();
1279 // Expected: the stored channels, untranslated; 16-bit → 8-bit display.
1280 let raw = fmt.encode_value(f64::from(depth) / 65535.);
1281 let to8 = |v: u64| {
1282 if bits == 16 {
1283 ((v * 255 + 32767) / 65535) as u8
1284 } else {
1285 v as u8
1286 }
1287 };
1288 let want = match encoding {
1289 DepthEncoding::Gray => [to8(raw[0]), to8(raw[0]), to8(raw[0]), 255],
1290 _ => [to8(raw[0]), to8(raw[1]), to8(raw[2]), 255],
1291 };
1292 for px in shown.rgba.chunks_exact(4) {
1293 assert_eq!(px, want, "{fmt:?}");
1294 }
1295 // 16-bit rgb_packed of a 16-bit canonical value d stores d in every
1296 // channel (d/65535 repeats in base 65536), so only check the others.
1297 if encoding == DepthEncoding::RgbBands
1298 || (encoding == DepthEncoding::RgbPacked && bits == 8)
1299 {
1300 assert!(
1301 want[0] != want[1] || want[1] != want[2],
1302 "{fmt:?}: raw RGB must not look like decoded gray: {want:?}"
1303 );
1304 }
1305 // Resultaat decodes depth: same relit image as gray16 within 1 level.
1306 let lit = compose_preview_frame(request(ViewMode::Resultaat)).unwrap();
1307 if bits == 16 {
1308 match &gray16_result {
1309 None => gray16_result = Some(lit.rgba.clone()),
1310 Some(reference) => {
1311 for (a, b) in lit.rgba.iter().zip(reference) {
1312 assert!((*a as i32 - *b as i32).abs() <= 1, "{fmt:?}");
1313 }
1314 }
1315 }
1316 }
1317 }
1318 }
1319 }
1320
1232 #[test]
1321 #[test]
1233 fn five_pitch_rows_clamp_and_default_is_zero() {
1322 fn five_pitch_rows_clamp_and_default_is_zero() {
1234 let mut state = Preview2dState::default();
1323 let mut state = Preview2dState::default();