Merge feature/preview-maps-as-is: 2D-preview maps as stored (Tim t992u)
@@ -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 compositor51
- 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.@@ -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 {874
pub struct LoadedAtlas {887
pub 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
879
impl LoadedAtlas {894
impl 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,@@ -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 the98
/// source provides them (atlas reader). Only for map inspection (t992u).99
pub depth_display: Option<Vec<u8>>,97
}100
}98
101
99
impl LayerMaps {102
impl 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 layer329
/// 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_display378
.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
@@ -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| {@@ -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): no794
/// decoding of rgb_bands / rgb_packed. Gray shows as gray, RGB channels show as795
/// their own RGB, alpha stays alpha; 16-bit samples are reduced to 8-bit display796
/// 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 u8803
} 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
out818
}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).794
pub(crate) fn canonical_depth(821
pub(crate) fn canonical_depth(795
native: &[u8],822
native: &[u8],@@ -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 show120
// 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 u81284
} else {1285
v as u81286
}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 every1296
// channel (d/65535 repeats in base 65536), so only check the others.1297
if encoding == DepthEncoding::RgbBands1298
|| (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();