use crate::mesh::Mesh;
use glam::Vec2;
use rayon::prelude::*;
pub fn generate_lightmap_uvs(mesh: &mut Mesh, resolution: u32) {
let tri_count = mesh.indices.len() / 3;
if tri_count == 0 {
return;
}
let inv_resolution = 1.0 / resolution as f32;
let padding = 2.0 * inv_resolution;
let mut groups: [Vec<usize>; 6] = Default::default();
for t in 0..tri_count {
let v0 = &mesh.vertices[mesh.indices[t * 3] as usize];
let v1 = &mesh.vertices[mesh.indices[t * 3 + 1] as usize];
let v2 = &mesh.vertices[mesh.indices[t * 3 + 2] as usize];
let edge1 = v1.position - v0.position;
let edge2 = v2.position - v0.position;
let face_normal = edge1.cross(edge2).normalize_or_zero();
let abs_n = face_normal.abs();
let group = if abs_n.x >= abs_n.y && abs_n.x >= abs_n.z {
if face_normal.x >= 0.0 {
0
} else {
1
}
} else if abs_n.y >= abs_n.x && abs_n.y >= abs_n.z {
if face_normal.y >= 0.0 {
2
} else {
3
}
} else if face_normal.z >= 0.0 {
4
} else {
5
};
groups[group].push(t);
}
let mut vertex_group: Vec<i32> = vec![-1; mesh.vertices.len()];
for (group_idx, tris) in groups.iter().enumerate() {
for &t in tris {
for k in 0..3 {
let idx = t * 3 + k;
let vi = mesh.indices[idx] as usize;
if vertex_group[vi] == -1 {
vertex_group[vi] = group_idx as i32;
} else if vertex_group[vi] != group_idx as i32 {
let new_vi = mesh.vertices.len();
mesh.vertices.push(mesh.vertices[vi]);
vertex_group.push(group_idx as i32);
mesh.indices[idx] = new_vi as u32;
}
}
}
}
let cell_w = 1.0 / 3.0;
let cell_h = 0.5;
let cell_offsets = [
Vec2::new(0.0, 0.5), Vec2::new(cell_w, 0.5), Vec2::new(cell_w * 2.0, 0.5), Vec2::new(0.0, 0.0), Vec2::new(cell_w, 0.0), Vec2::new(cell_w * 2.0, 0.0), ];
let usable_w = cell_w - padding * 2.0;
let usable_h = cell_h - padding * 2.0;
for (group_idx, tris) in groups.iter().enumerate() {
if tris.is_empty() {
continue;
}
let offset = cell_offsets[group_idx];
let indices_ref = &mesh.indices;
let vert_indices: Vec<u32> = tris
.iter()
.flat_map(|&t| {
let i0 = indices_ref[t * 3];
let i1 = indices_ref[t * 3 + 1];
let i2 = indices_ref[t * 3 + 2];
[i0, i1, i2]
})
.collect();
let vertices_ref = &mesh.vertices;
let (proj_min, proj_max) = vert_indices
.par_iter()
.map(|&vi| {
let proj = project_to_2d(vertices_ref[vi as usize].position, group_idx);
(proj, proj)
})
.reduce(
|| (Vec2::splat(f32::MAX), Vec2::splat(f32::MIN)),
|(min_a, max_a), (min_b, max_b)| (min_a.min(min_b), max_a.max(max_b)),
);
let range = proj_max - proj_min;
let inv_range = Vec2::new(
if range.x > 1e-6 { 1.0 / range.x } else { 0.0 },
if range.y > 1e-6 { 1.0 / range.y } else { 0.0 },
);
for &t in tris {
for k in 0..3 {
let vi = mesh.indices[t * 3 + k] as usize;
let v = &mesh.vertices[vi];
let proj = project_to_2d(v.position, group_idx);
let normalized = (proj - proj_min) * inv_range;
let uv2 = Vec2::new(
normalized.x.mul_add(usable_w, offset.x + padding),
normalized.y.mul_add(usable_h, offset.y + padding),
);
mesh.vertices[vi].uv2 = uv2;
}
}
}
}
#[inline]
const fn project_to_2d(pos: glam::Vec3, group: usize) -> Vec2 {
match group {
0 | 1 => Vec2::new(pos.y, pos.z), 2 | 3 => Vec2::new(pos.x, pos.z), _ => Vec2::new(pos.x, pos.y), }
}
pub fn generate_lightmap_uvs_fast(mesh: &mut Mesh) {
mesh.vertices.par_iter_mut().for_each(|v| {
let abs_n = v.normal.abs();
if abs_n.x >= abs_n.y && abs_n.x >= abs_n.z {
v.uv2 = Vec2::new(
v.position.y.mul_add(0.5, 0.5),
v.position.z.mul_add(0.5, 0.5),
);
} else if abs_n.y >= abs_n.x && abs_n.y >= abs_n.z {
v.uv2 = Vec2::new(
v.position.x.mul_add(0.5, 0.5),
v.position.z.mul_add(0.5, 0.5),
);
} else {
v.uv2 = Vec2::new(
v.position.x.mul_add(0.5, 0.5),
v.position.y.mul_add(0.5, 0.5),
);
}
});
}
#[cfg(test)]
mod tests {
use super::*;
use crate::mesh::{sdf_to_mesh, MarchingCubesConfig};
use crate::types::SdfNode;
use glam::Vec3;
#[test]
fn test_lightmap_uvs_generated() {
let sphere = SdfNode::sphere(1.0);
let config = MarchingCubesConfig {
resolution: 8,
iso_level: 0.0,
compute_normals: true,
..Default::default()
};
let mut mesh = sdf_to_mesh(&sphere, Vec3::splat(-2.0), Vec3::splat(2.0), &config);
assert!(mesh.vertices.iter().all(|v| v.uv2 == Vec2::ZERO));
generate_lightmap_uvs(&mut mesh, 1024);
let non_zero_count = mesh.vertices.iter().filter(|v| v.uv2 != Vec2::ZERO).count();
assert!(non_zero_count > 0, "Expected non-zero UV2 values");
for v in &mesh.vertices {
assert!(
v.uv2.x >= 0.0 && v.uv2.x <= 1.0,
"UV2.x out of range: {}",
v.uv2.x
);
assert!(
v.uv2.y >= 0.0 && v.uv2.y <= 1.0,
"UV2.y out of range: {}",
v.uv2.y
);
}
}
#[test]
fn test_lightmap_uvs_fast() {
let sphere = SdfNode::sphere(1.0);
let config = MarchingCubesConfig {
resolution: 8,
iso_level: 0.0,
compute_normals: true,
..Default::default()
};
let mut mesh = sdf_to_mesh(&sphere, Vec3::splat(-2.0), Vec3::splat(2.0), &config);
generate_lightmap_uvs_fast(&mut mesh);
let non_zero_count = mesh.vertices.iter().filter(|v| v.uv2 != Vec2::ZERO).count();
assert!(non_zero_count > 0);
}
#[test]
fn test_lightmap_uvs_box() {
let box_shape = SdfNode::box3d(1.0, 1.0, 1.0);
let config = MarchingCubesConfig {
resolution: 8,
iso_level: 0.0,
compute_normals: true,
..Default::default()
};
let mut mesh = sdf_to_mesh(&box_shape, Vec3::splat(-2.0), Vec3::splat(2.0), &config);
generate_lightmap_uvs(&mut mesh, 1024);
for v in &mesh.vertices {
assert!(v.uv2.x >= 0.0 && v.uv2.x <= 1.0);
assert!(v.uv2.y >= 0.0 && v.uv2.y <= 1.0);
}
}
#[test]
fn test_lightmap_uvs_no_overlap() {
let sphere = SdfNode::sphere(1.0);
let config = MarchingCubesConfig {
resolution: 8,
iso_level: 0.0,
compute_normals: true,
..Default::default()
};
let mut mesh = sdf_to_mesh(&sphere, Vec3::splat(-2.0), Vec3::splat(2.0), &config);
let verts_before = mesh.vertices.len();
generate_lightmap_uvs(&mut mesh, 1024);
let verts_after = mesh.vertices.len();
assert!(
verts_after >= verts_before,
"Vertex count should not decrease: before={}, after={}",
verts_before,
verts_after
);
let tri_count = mesh.indices.len() / 3;
for t in 0..tri_count {
let uv0 = mesh.vertices[mesh.indices[t * 3] as usize].uv2;
let uv1 = mesh.vertices[mesh.indices[t * 3 + 1] as usize].uv2;
let uv2 = mesh.vertices[mesh.indices[t * 3 + 2] as usize].uv2;
let max_u = uv0.x.max(uv1.x).max(uv2.x);
let min_u = uv0.x.min(uv1.x).min(uv2.x);
let max_v = uv0.y.max(uv1.y).max(uv2.y);
let min_v = uv0.y.min(uv1.y).min(uv2.y);
assert!(
max_u - min_u <= 0.4,
"Triangle {} UV2.x span too large: {}",
t,
max_u - min_u
);
assert!(
max_v - min_v <= 0.6,
"Triangle {} UV2.y span too large: {}",
t,
max_v - min_v
);
}
}
}