use rayon::prelude::*;
use crate::{
generator::{TextureError, TextureMap, Workspace, linear_to_srgb, validate_dimensions},
normal::{BoundaryMode, height_to_normal},
};
pub struct SurfaceSample {
pub height: f64,
pub color: [f32; 3],
pub roughness: f32,
pub metallic: f32,
pub occlusion: f32,
pub emissive: [f32; 3],
}
impl SurfaceSample {
#[inline]
pub fn matte(height: f64, color: [f32; 3], roughness: f32) -> Self {
Self {
height,
color,
roughness,
metallic: 0.0,
occlusion: 1.0,
emissive: [0.0, 0.0, 0.0],
}
}
}
pub trait SurfaceCell {
fn sample(&self, x: u32, y: u32, u: f64, v: f64) -> SurfaceSample;
}
pub fn generate_surface<C: SurfaceCell + Sync>(
width: u32,
height: u32,
normal_strength: f32,
workspace: Option<&mut Workspace>,
cell: &C,
) -> Result<TextureMap, TextureError> {
generate_surface_impl(width, height, normal_strength, workspace, cell, false)
}
pub fn generate_surface_emissive<C: SurfaceCell + Sync>(
width: u32,
height: u32,
normal_strength: f32,
workspace: Option<&mut Workspace>,
cell: &C,
) -> Result<TextureMap, TextureError> {
generate_surface_impl(width, height, normal_strength, workspace, cell, true)
}
fn generate_surface_impl<C: SurfaceCell + Sync>(
width: u32,
height: u32,
normal_strength: f32,
mut workspace: Option<&mut Workspace>,
cell: &C,
emit: bool,
) -> Result<TextureMap, TextureError> {
validate_dimensions(width, height)?;
let w = width as usize;
let h = height as usize;
let n = w * h;
let mut heights = workspace
.as_deref_mut()
.map_or_else(Vec::new, |ws| ws.take_grid());
heights.clear();
heights.resize(n, 0.0);
let mut albedo = vec![0u8; n * 4];
let mut roughness = vec![0u8; n * 4];
let mut emissive = if emit { vec![0u8; n * 4] } else { Vec::new() };
let write_pixel = |x: usize,
y: usize,
height_slot: &mut f64,
albedo_row: &mut [u8],
orm_row: &mut [u8],
emissive_px: Option<&mut [u8]>| {
let u = x as f64 / w as f64;
let v = y as f64 / h as f64;
let s = cell.sample(x as u32, y as u32, u, v);
*height_slot = s.height;
let ai = x * 4;
albedo_row[ai] = linear_to_srgb(s.color[0]);
albedo_row[ai + 1] = linear_to_srgb(s.color[1]);
albedo_row[ai + 2] = linear_to_srgb(s.color[2]);
albedo_row[ai + 3] = 255;
orm_row[ai] = (s.occlusion.clamp(0.0, 1.0) * 255.0).round() as u8;
orm_row[ai + 1] = (s.roughness.clamp(0.0, 1.0) * 255.0).round() as u8;
orm_row[ai + 2] = (s.metallic.clamp(0.0, 1.0) * 255.0).round() as u8;
orm_row[ai + 3] = 255;
if let Some(e) = emissive_px {
e[0] = linear_to_srgb(s.emissive[0]);
e[1] = linear_to_srgb(s.emissive[1]);
e[2] = linear_to_srgb(s.emissive[2]);
e[3] = 255;
}
};
if emit {
heights
.par_chunks_mut(w)
.zip(albedo.par_chunks_mut(w * 4))
.zip(roughness.par_chunks_mut(w * 4))
.zip(emissive.par_chunks_mut(w * 4))
.enumerate()
.for_each(|(y, (((height_row, albedo_row), orm_row), emissive_row))| {
for (x, height_slot) in height_row.iter_mut().enumerate() {
let ai = x * 4;
write_pixel(
x,
y,
height_slot,
albedo_row,
orm_row,
Some(&mut emissive_row[ai..ai + 4]),
);
}
});
} else {
heights
.par_chunks_mut(w)
.zip(albedo.par_chunks_mut(w * 4))
.zip(roughness.par_chunks_mut(w * 4))
.enumerate()
.for_each(|(y, ((height_row, albedo_row), orm_row))| {
for (x, height_slot) in height_row.iter_mut().enumerate() {
write_pixel(x, y, height_slot, albedo_row, orm_row, None);
}
});
}
let normal = height_to_normal(&heights, width, height, normal_strength, BoundaryMode::Wrap);
if let Some(ws) = workspace {
ws.return_grid(heights);
}
Ok(TextureMap {
albedo,
normal,
roughness,
width,
height,
mip_level_count: 1,
emissive: if emit { Some(emissive) } else { None },
})
}
#[inline]
pub fn lerp(a: f32, b: f32, t: f32) -> f32 {
a + (b - a) * t.clamp(0.0, 1.0)
}
#[cfg(test)]
mod tests {
use super::*;
struct Flat;
impl SurfaceCell for Flat {
fn sample(&self, _x: u32, _y: u32, _u: f64, _v: f64) -> SurfaceSample {
SurfaceSample {
height: 0.5,
color: [1.0, 0.0, 0.0],
roughness: 0.5,
metallic: 1.0,
occlusion: 0.0,
emissive: [0.0, 0.0, 0.0],
}
}
}
#[test]
fn driver_packs_orm_channels() {
let map = generate_surface(4, 4, 1.0, None, &Flat).expect("generate");
assert_eq!(map.roughness[0], 0, "occlusion 0.0 → 0");
assert_eq!(map.roughness[1], 128, "roughness 0.5 → 128");
assert_eq!(map.roughness[2], 255, "metallic 1.0 → 255");
assert_eq!(map.roughness[3], 255);
assert_eq!(map.albedo[0], 255);
assert_eq!(map.albedo[3], 255);
assert_eq!(&map.normal[0..4], &[128, 128, 255, 255]);
}
#[test]
fn driver_rejects_invalid_dimensions() {
assert!(generate_surface(0, 4, 1.0, None, &Flat).is_err());
assert!(generate_surface(4, 0, 1.0, None, &Flat).is_err());
}
#[test]
fn driver_reuses_workspace_buffers() {
let mut ws = Workspace::new();
let a = generate_surface(8, 8, 1.0, Some(&mut ws), &Flat).expect("first");
let b = generate_surface(8, 8, 1.0, Some(&mut ws), &Flat).expect("second");
assert_eq!(a.albedo, b.albedo);
assert_eq!(a.normal, b.normal);
}
#[test]
fn matte_sets_dielectric_defaults() {
let s = SurfaceSample::matte(0.3, [0.1, 0.2, 0.3], 0.7);
assert_eq!(s.metallic, 0.0);
assert_eq!(s.occlusion, 1.0);
}
#[test]
fn lerp_clamps_t() {
assert_eq!(lerp(0.0, 1.0, -1.0), 0.0);
assert_eq!(lerp(0.0, 1.0, 2.0), 1.0);
assert_eq!(lerp(0.0, 1.0, 0.5), 0.5);
}
}