Register a texture with optional normal-map preprocessing. For normal maps (is_normal_map=true), mip chain is built with vector-space averaging instead of scalar RGB averaging, and per-mip variance (1 - |vector_avg|²) is baked into the alpha channel. The shader reads alpha as a Toksvig-style σ² addition that accumulates normal-direction disagreement across the footprint the sampler ends up integr
(
&mut self,
width: u32,
height: u32,
data: &[u8],
is_normal_map: bool,
)
| 11593 | /// scalar LEADR/LEAN filter. Alpha is unused by glTF normal maps |
| 11594 | /// (they carry (x,y,z) in RGB) so we can safely repurpose it. |
| 11595 | pub fn register_texture_kind( |
| 11596 | &mut self, |
| 11597 | width: u32, |
| 11598 | height: u32, |
| 11599 | data: &[u8], |
| 11600 | is_normal_map: bool, |
| 11601 | ) -> u32 { |
| 11602 | let max_dim = if width > height { width } else { height }; |
| 11603 | // On Android/Vulkan, multi-level mipmap upload can fail silently. |
| 11604 | // Use single mip for 2D textures; only generate mipmaps on desktop. |
| 11605 | #[cfg(target_os = "android")] |
| 11606 | let mip_count = 1u32; |
| 11607 | #[cfg(not(target_os = "android"))] |
| 11608 | let mip_count = (max_dim as f32).log2().floor() as u32 + 1; |
| 11609 | |
| 11610 | // Generate mip chain data |
| 11611 | let mut mip_data = Vec::with_capacity(data.len() * 2); // overallocate |
| 11612 | if is_normal_map { |
| 11613 | // Level 0: normalize input RGB and clear alpha to 0 (no |
| 11614 | // variance at the finest level — each texel is assumed unit). |
| 11615 | mip_data.reserve(data.len()); |
| 11616 | for i in 0..(width as usize * height as usize) { |
| 11617 | let r = data[i * 4]; |
| 11618 | let g = data[i * 4 + 1]; |
| 11619 | let b = data[i * 4 + 2]; |
| 11620 | mip_data.push(r); |
| 11621 | mip_data.push(g); |
| 11622 | mip_data.push(b); |
| 11623 | mip_data.push(0); |
| 11624 | } |
| 11625 | } else { |
| 11626 | mip_data.extend_from_slice(data); |
| 11627 | } |
| 11628 | let mut mip_offsets = vec![0usize]; // byte offset of each mip level |
| 11629 | let mut mw = width; |
| 11630 | let mut mh = height; |
| 11631 | for _ in 1..mip_count { |
| 11632 | let prev_offset = *mip_offsets.last().unwrap(); |
| 11633 | let pw = mw as usize; // previous width |
| 11634 | let ph = mh as usize; // previous height |
| 11635 | mw = if mw > 1 { mw / 2 } else { 1 }; |
| 11636 | mh = if mh > 1 { mh / 2 } else { 1 }; |
| 11637 | mip_offsets.push(mip_data.len()); |
| 11638 | for y in 0..mh as usize { |
| 11639 | for x in 0..mw as usize { |
| 11640 | let sx = x * 2; |
| 11641 | let sy = y * 2; |
| 11642 | let sx1 = (sx + 1).min(pw - 1); |
| 11643 | let sy1 = (sy + 1).min(ph - 1); |
| 11644 | if is_normal_map { |
| 11645 | // Decode 4 children to signed [-1, 1] vectors |
| 11646 | let dec = |r: u8, g: u8, b: u8| -> [f32; 3] { |
| 11647 | [ |
| 11648 | r as f32 * (2.0 / 255.0) - 1.0, |
| 11649 | g as f32 * (2.0 / 255.0) - 1.0, |
| 11650 | b as f32 * (2.0 / 255.0) - 1.0, |
| 11651 | ] |
| 11652 | }; |
no test coverage detected