use crate::io::IoError;
use crate::mesh::Mesh;
use std::fs::File;
use std::io::{BufWriter, Write};
use std::path::Path;
const UE5_BIN_MAGIC: &[u8; 4] = b"ALUE";
const UE5_BIN_VERSION: u16 = 1;
const UE5_JSON_VERSION: u32 = 1;
#[derive(Debug, Clone)]
pub struct Ue5MeshConfig {
pub scale: f32,
pub name: String,
pub lod_index: u32,
}
impl Default for Ue5MeshConfig {
fn default() -> Self {
Self {
scale: 100.0,
name: "SM_AliceSdf".to_string(),
lod_index: 0,
}
}
}
#[inline(always)]
fn convert_position(x: f32, y: f32, z: f32, scale: f32) -> (f32, f32, f32) {
(x * scale, z * scale, y * scale)
}
#[inline(always)]
const fn convert_direction(x: f32, y: f32, z: f32) -> (f32, f32, f32) {
(x, z, y)
}
#[inline(always)]
fn convert_tangent(x: f32, y: f32, z: f32, w: f32) -> (f32, f32, f32, f32) {
(x, z, y, -w)
}
fn compute_bounds(mesh: &Mesh, scale: f32) -> ([f32; 3], [f32; 3]) {
if mesh.vertices.is_empty() {
return ([0.0; 3], [0.0; 3]);
}
let mut min = [f32::MAX; 3];
let mut max = [f32::MIN; 3];
for v in &mesh.vertices {
let (ux, uy, uz) = convert_position(v.position.x, v.position.y, v.position.z, scale);
min[0] = min[0].min(ux);
min[1] = min[1].min(uy);
min[2] = min[2].min(uz);
max[0] = max[0].max(ux);
max[1] = max[1].max(uy);
max[2] = max[2].max(uz);
}
let origin = [
(min[0] + max[0]) * 0.5,
(min[1] + max[1]) * 0.5,
(min[2] + max[2]) * 0.5,
];
let extent = [
(max[0] - min[0]) * 0.5,
(max[1] - min[1]) * 0.5,
(max[2] - min[2]) * 0.5,
];
(origin, extent)
}
pub fn export_ue5_mesh(
mesh: &Mesh,
path: impl AsRef<Path>,
config: &Ue5MeshConfig,
) -> Result<(), IoError> {
export_ue5_mesh_with_lods(std::slice::from_ref(mesh), &[1.0], path, config)
}
pub fn export_ue5_mesh_binary(
mesh: &Mesh,
path: impl AsRef<Path>,
config: &Ue5MeshConfig,
) -> Result<(), IoError> {
let file = File::create(path)?;
let mut w = BufWriter::new(file);
let vc = mesh.vertices.len() as u32;
let ic = mesh.indices.len() as u32;
let flags: u16 = 0x3F;
w.write_all(UE5_BIN_MAGIC)?;
w.write_all(&UE5_BIN_VERSION.to_le_bytes())?;
w.write_all(&vc.to_le_bytes())?;
w.write_all(&ic.to_le_bytes())?;
w.write_all(&flags.to_le_bytes())?;
for v in &mesh.vertices {
let (px, py, pz) = convert_position(v.position.x, v.position.y, v.position.z, config.scale);
w.write_all(&px.to_le_bytes())?;
w.write_all(&py.to_le_bytes())?;
w.write_all(&pz.to_le_bytes())?;
let (nx, ny, nz) = convert_direction(v.normal.x, v.normal.y, v.normal.z);
w.write_all(&nx.to_le_bytes())?;
w.write_all(&ny.to_le_bytes())?;
w.write_all(&nz.to_le_bytes())?;
w.write_all(&v.uv.x.to_le_bytes())?;
w.write_all(&v.uv.y.to_le_bytes())?;
let (tx, ty, tz, tw) = convert_tangent(v.tangent.x, v.tangent.y, v.tangent.z, v.tangent.w);
w.write_all(&tx.to_le_bytes())?;
w.write_all(&ty.to_le_bytes())?;
w.write_all(&tz.to_le_bytes())?;
w.write_all(&tw.to_le_bytes())?;
w.write_all(&v.color[0].to_le_bytes())?;
w.write_all(&v.color[1].to_le_bytes())?;
w.write_all(&v.color[2].to_le_bytes())?;
w.write_all(&v.color[3].to_le_bytes())?;
}
let tri_count = mesh.indices.len() / 3;
for t in 0..tri_count {
let base = t * 3;
let i0 = mesh.indices[base];
let i1 = mesh.indices[base + 1];
let i2 = mesh.indices[base + 2];
w.write_all(&i0.to_le_bytes())?;
w.write_all(&i2.to_le_bytes())?;
w.write_all(&i1.to_le_bytes())?;
}
w.flush()?;
Ok(())
}
pub fn export_ue5_mesh_with_lods(
meshes: &[Mesh],
lod_screen_sizes: &[f32],
path: impl AsRef<Path>,
config: &Ue5MeshConfig,
) -> Result<(), IoError> {
if meshes.is_empty() {
return Err(IoError::InvalidFormat("No meshes provided".into()));
}
if lod_screen_sizes.len() != meshes.len() {
return Err(IoError::InvalidFormat(format!(
"Expected {} screen sizes for {} LODs, got {}",
meshes.len(),
meshes.len(),
lod_screen_sizes.len()
)));
}
let file = File::create(path)?;
let mut w = BufWriter::new(file);
let (origin, extent) = compute_bounds(&meshes[0], config.scale);
write!(w, "{{\n \"alice_sdf_static_mesh\": {{\n")?;
writeln!(w, " \"version\": {},", UE5_JSON_VERSION)?;
writeln!(w, " \"name\": \"{}\",", config.name)?;
writeln!(w, " \"lods\": [")?;
for (lod_idx, mesh) in meshes.iter().enumerate() {
if lod_idx > 0 {
writeln!(w, ",")?;
}
let ic = mesh.indices.len();
let tri_count = ic / 3;
let min_vi = if mesh.indices.is_empty() {
0
} else {
*mesh.indices.iter().min().unwrap()
};
let max_vi = if mesh.indices.is_empty() {
0
} else {
*mesh.indices.iter().max().unwrap()
};
writeln!(w, " {{")?;
writeln!(w, " \"lod_index\": {},", lod_idx)?;
writeln!(
w,
" \"screen_size\": {:.6},",
lod_screen_sizes[lod_idx]
)?;
writeln!(w, " \"sections\": [{{")?;
writeln!(w, " \"material_index\": 0,")?;
writeln!(w, " \"first_index\": 0,")?;
writeln!(w, " \"num_triangles\": {},", tri_count)?;
writeln!(w, " \"min_vertex_index\": {},", min_vi)?;
writeln!(w, " \"max_vertex_index\": {}", max_vi)?;
writeln!(w, " }}],")?;
writeln!(w, " \"vertex_data\": {{")?;
write_ue5_vertex_positions(&mut w, mesh, config.scale)?;
writeln!(w, ",")?;
write_ue5_vertex_normals(&mut w, mesh)?;
writeln!(w, ",")?;
write_ue5_vertex_uvs(&mut w, mesh)?;
writeln!(w, ",")?;
write_ue5_vertex_tangents(&mut w, mesh)?;
writeln!(w)?;
writeln!(w, " }},")?;
write!(w, " \"index_data\": [")?;
for t in 0..tri_count {
if t > 0 {
write!(w, ", ")?;
}
let base = t * 3;
let i0 = mesh.indices[base];
let i1 = mesh.indices[base + 1];
let i2 = mesh.indices[base + 2];
write!(w, "{}, {}, {}", i0, i2, i1)?;
}
writeln!(w, "]")?;
write!(w, " }}")?;
}
write!(w, "\n ],\n")?;
writeln!(w, " \"bounds\": {{")?;
writeln!(
w,
" \"origin\": [{:.6}, {:.6}, {:.6}],",
origin[0], origin[1], origin[2]
)?;
writeln!(
w,
" \"extent\": [{:.6}, {:.6}, {:.6}]",
extent[0], extent[1], extent[2]
)?;
writeln!(w, " }}")?;
write!(w, " }}\n}}\n")?;
w.flush()?;
Ok(())
}
fn write_ue5_vertex_positions(w: &mut impl Write, mesh: &Mesh, scale: f32) -> Result<(), IoError> {
write!(w, " \"positions\": [")?;
for (i, v) in mesh.vertices.iter().enumerate() {
if i > 0 {
write!(w, ", ")?;
}
let (ux, uy, uz) = convert_position(v.position.x, v.position.y, v.position.z, scale);
write!(w, "{:.6}, {:.6}, {:.6}", ux, uy, uz)?;
}
write!(w, "]")?;
Ok(())
}
fn write_ue5_vertex_normals(w: &mut impl Write, mesh: &Mesh) -> Result<(), IoError> {
write!(w, " \"normals\": [")?;
for (i, v) in mesh.vertices.iter().enumerate() {
if i > 0 {
write!(w, ", ")?;
}
let (nx, ny, nz) = convert_direction(v.normal.x, v.normal.y, v.normal.z);
write!(w, "{:.6}, {:.6}, {:.6}", nx, ny, nz)?;
}
write!(w, "]")?;
Ok(())
}
fn write_ue5_vertex_uvs(w: &mut impl Write, mesh: &Mesh) -> Result<(), IoError> {
write!(w, " \"uvs\": [[")?;
for (i, v) in mesh.vertices.iter().enumerate() {
if i > 0 {
write!(w, ", ")?;
}
write!(w, "{:.6}, {:.6}", v.uv.x, v.uv.y)?;
}
write!(w, "], [")?;
for (i, v) in mesh.vertices.iter().enumerate() {
if i > 0 {
write!(w, ", ")?;
}
write!(w, "{:.6}, {:.6}", v.uv2.x, v.uv2.y)?;
}
write!(w, "]]")?;
Ok(())
}
fn write_ue5_vertex_tangents(w: &mut impl Write, mesh: &Mesh) -> Result<(), IoError> {
write!(w, " \"tangents\": [")?;
for (i, v) in mesh.vertices.iter().enumerate() {
if i > 0 {
write!(w, ", ")?;
}
let (tx, ty, tz, tw) = convert_tangent(v.tangent.x, v.tangent.y, v.tangent.z, v.tangent.w);
write!(w, "{:.6}, {:.6}, {:.6}, {:.6}", tx, ty, tz, tw)?;
}
write!(w, "]")?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::mesh::{sdf_to_mesh, MarchingCubesConfig, Vertex};
use crate::types::SdfNode;
use glam::{Vec2, Vec3, Vec4};
use std::fs;
fn temp_path(name: &str) -> std::path::PathBuf {
let mut path = std::env::temp_dir();
path.push(format!("alice_sdf_ue5_{}", name));
path
}
fn make_triangle_mesh() -> Mesh {
let v0 = Vertex::with_all(
Vec3::new(1.0, 2.0, 3.0),
Vec3::new(0.0, 1.0, 0.0),
Vec2::new(0.0, 0.0),
Vec2::new(0.1, 0.2),
Vec4::new(1.0, 0.0, 0.0, 1.0),
[1.0, 0.0, 0.0, 1.0],
0,
);
let v1 = Vertex::with_all(
Vec3::new(4.0, 5.0, 6.0),
Vec3::new(1.0, 0.0, 0.0),
Vec2::new(1.0, 0.0),
Vec2::new(0.3, 0.4),
Vec4::new(0.0, 1.0, 0.0, 1.0),
[0.0, 1.0, 0.0, 1.0],
0,
);
let v2 = Vertex::with_all(
Vec3::new(7.0, 8.0, 9.0),
Vec3::new(0.0, 0.0, 1.0),
Vec2::new(0.0, 1.0),
Vec2::new(0.5, 0.6),
Vec4::new(0.0, 0.0, 1.0, 1.0),
[0.0, 0.0, 1.0, 1.0],
0,
);
Mesh {
vertices: vec![v0, v1, v2],
indices: vec![0, 1, 2],
}
}
#[test]
fn test_ue5_json_export_structure() {
let mesh = make_triangle_mesh();
let path = temp_path("structure.ue5_mesh");
export_ue5_mesh(&mesh, &path, &Ue5MeshConfig::default()).unwrap();
let content = fs::read_to_string(&path).unwrap();
assert!(content.contains("\"alice_sdf_static_mesh\""));
assert!(content.contains("\"version\": 1"));
assert!(content.contains("\"name\": \"SM_AliceSdf\""));
assert!(content.contains("\"lods\""));
assert!(content.contains("\"lod_index\": 0"));
assert!(content.contains("\"screen_size\""));
assert!(content.contains("\"sections\""));
assert!(content.contains("\"positions\""));
assert!(content.contains("\"normals\""));
assert!(content.contains("\"uvs\""));
assert!(content.contains("\"tangents\""));
assert!(content.contains("\"index_data\""));
assert!(content.contains("\"bounds\""));
assert!(content.contains("\"origin\""));
assert!(content.contains("\"extent\""));
fs::remove_file(&path).ok();
}
#[test]
fn test_ue5_scale_factor() {
let mesh = make_triangle_mesh();
let path = temp_path("scale.ue5_mesh");
export_ue5_mesh(&mesh, &path, &Ue5MeshConfig::default()).unwrap();
let content = fs::read_to_string(&path).unwrap();
assert!(content.contains("100.000000"));
assert!(content.contains("300.000000"));
assert!(content.contains("200.000000"));
fs::remove_file(&path).ok();
}
#[test]
fn test_ue5_coordinate_swap() {
let mesh = make_triangle_mesh();
let path = temp_path("coord_swap.ue5_mesh");
let config = Ue5MeshConfig {
scale: 1.0, ..Default::default()
};
export_ue5_mesh(&mesh, &path, &config).unwrap();
let content = fs::read_to_string(&path).unwrap();
assert!(content.contains("1.000000, 3.000000, 2.000000"));
assert!(content.contains("0.000000, 0.000000, 1.000000"));
fs::remove_file(&path).ok();
}
#[test]
fn test_ue5_winding_flip() {
let mesh = make_triangle_mesh();
let path = temp_path("winding.ue5_mesh");
let config = Ue5MeshConfig {
scale: 1.0,
..Default::default()
};
export_ue5_mesh(&mesh, &path, &config).unwrap();
let content = fs::read_to_string(&path).unwrap();
assert!(content.contains("\"index_data\": [0, 2, 1]"));
fs::remove_file(&path).ok();
}
#[test]
fn test_ue5_lod_export() {
let lod0 = make_triangle_mesh();
let lod1 = Mesh {
vertices: vec![Vertex::new(Vec3::ZERO, Vec3::Y)],
indices: vec![0, 0, 0],
};
let path = temp_path("lods.ue5_mesh");
let screen_sizes = vec![1.0, 0.5];
let config = Ue5MeshConfig {
scale: 1.0,
..Default::default()
};
export_ue5_mesh_with_lods(&[lod0, lod1], &screen_sizes, &path, &config).unwrap();
let content = fs::read_to_string(&path).unwrap();
assert!(content.contains("\"lod_index\": 0"));
assert!(content.contains("\"lod_index\": 1"));
assert!(content.contains("\"screen_size\": 1.000000"));
assert!(content.contains("\"screen_size\": 0.500000"));
fs::remove_file(&path).ok();
}
#[test]
fn test_ue5_binary_header() {
let mesh = make_triangle_mesh();
let path = temp_path("header.ue5_mesh_bin");
export_ue5_mesh_binary(&mesh, &path, &Ue5MeshConfig::default()).unwrap();
let data = fs::read(&path).unwrap();
assert_eq!(&data[0..4], b"ALUE");
assert_eq!(u16::from_le_bytes([data[4], data[5]]), 1);
assert_eq!(u32::from_le_bytes([data[6], data[7], data[8], data[9]]), 3);
assert_eq!(
u32::from_le_bytes([data[10], data[11], data[12], data[13]]),
3
);
let expected = 16 + 3 * 64 + 3 * 4;
assert_eq!(data.len(), expected);
fs::remove_file(&path).ok();
}
#[test]
fn test_ue5_binary_position_conversion() {
let mesh = make_triangle_mesh();
let path = temp_path("pos_conv.ue5_mesh_bin");
let config = Ue5MeshConfig {
scale: 100.0,
..Default::default()
};
export_ue5_mesh_binary(&mesh, &path, &config).unwrap();
let data = fs::read(&path).unwrap();
let px = f32::from_le_bytes(data[16..20].try_into().unwrap());
let py = f32::from_le_bytes(data[20..24].try_into().unwrap());
let pz = f32::from_le_bytes(data[24..28].try_into().unwrap());
assert!((px - 100.0).abs() < 1e-3);
assert!((py - 300.0).abs() < 1e-3); assert!((pz - 200.0).abs() < 1e-3);
fs::remove_file(&path).ok();
}
#[test]
fn test_ue5_binary_normal_conversion() {
let mesh = make_triangle_mesh();
let path = temp_path("norm_conv.ue5_mesh_bin");
let config = Ue5MeshConfig {
scale: 1.0,
..Default::default()
};
export_ue5_mesh_binary(&mesh, &path, &config).unwrap();
let data = fs::read(&path).unwrap();
let nx = f32::from_le_bytes(data[28..32].try_into().unwrap());
let ny = f32::from_le_bytes(data[32..36].try_into().unwrap());
let nz = f32::from_le_bytes(data[36..40].try_into().unwrap());
assert!((nx - 0.0).abs() < 1e-6);
assert!((ny - 0.0).abs() < 1e-6); assert!((nz - 1.0).abs() < 1e-6);
fs::remove_file(&path).ok();
}
#[test]
fn test_ue5_empty_mesh() {
let mesh = Mesh::new();
let path_json = temp_path("empty.ue5_mesh");
let path_bin = temp_path("empty.ue5_mesh_bin");
export_ue5_mesh(&mesh, &path_json, &Ue5MeshConfig::default()).unwrap();
export_ue5_mesh_binary(&mesh, &path_bin, &Ue5MeshConfig::default()).unwrap();
let content = fs::read_to_string(&path_json).unwrap();
assert!(content.contains("\"alice_sdf_static_mesh\""));
let data = fs::read(&path_bin).unwrap();
assert_eq!(data.len(), 16);
fs::remove_file(&path_json).ok();
fs::remove_file(&path_bin).ok();
}
#[test]
fn test_ue5_sphere_export() {
let sphere = SdfNode::sphere(1.0);
let mc_config = MarchingCubesConfig {
resolution: 8,
iso_level: 0.0,
compute_normals: true,
..Default::default()
};
let mesh = sdf_to_mesh(&sphere, Vec3::splat(-2.0), Vec3::splat(2.0), &mc_config);
let path = temp_path("sphere.ue5_mesh");
export_ue5_mesh(&mesh, &path, &Ue5MeshConfig::default()).unwrap();
let content = fs::read_to_string(&path).unwrap();
assert!(content.contains("\"alice_sdf_static_mesh\""));
assert!(content.contains("\"bounds\""));
fs::remove_file(&path).ok();
}
#[test]
fn test_ue5_bounds_calculation() {
let mesh = make_triangle_mesh();
let path = temp_path("bounds.ue5_mesh");
let config = Ue5MeshConfig {
scale: 1.0,
..Default::default()
};
export_ue5_mesh(&mesh, &path, &config).unwrap();
let content = fs::read_to_string(&path).unwrap();
assert!(content.contains("\"origin\": [4.000000, 6.000000, 5.000000]"));
assert!(content.contains("\"extent\": [3.000000, 3.000000, 3.000000]"));
fs::remove_file(&path).ok();
}
#[test]
fn test_ue5_custom_name() {
let mesh = make_triangle_mesh();
let path = temp_path("named.ue5_mesh");
let config = Ue5MeshConfig {
name: "SM_MyCustomMesh".to_string(),
..Default::default()
};
export_ue5_mesh(&mesh, &path, &config).unwrap();
let content = fs::read_to_string(&path).unwrap();
assert!(content.contains("\"name\": \"SM_MyCustomMesh\""));
fs::remove_file(&path).ok();
}
#[test]
fn test_ue5_lod_screen_sizes_mismatch() {
let mesh = make_triangle_mesh();
let path = temp_path("mismatch.ue5_mesh");
let result =
export_ue5_mesh_with_lods(&[mesh], &[1.0, 0.5], &path, &Ue5MeshConfig::default());
assert!(matches!(result, Err(IoError::InvalidFormat(_))));
}
#[test]
fn test_ue5_empty_meshes_error() {
let path = temp_path("no_meshes.ue5_mesh");
let result = export_ue5_mesh_with_lods(&[], &[], &path, &Ue5MeshConfig::default());
assert!(matches!(result, Err(IoError::InvalidFormat(_))));
}
#[test]
fn test_ue5_section_vertex_range() {
let mesh = make_triangle_mesh();
let path = temp_path("section.ue5_mesh");
let config = Ue5MeshConfig {
scale: 1.0,
..Default::default()
};
export_ue5_mesh(&mesh, &path, &config).unwrap();
let content = fs::read_to_string(&path).unwrap();
assert!(content.contains("\"min_vertex_index\": 0"));
assert!(content.contains("\"max_vertex_index\": 2"));
assert!(content.contains("\"num_triangles\": 1"));
fs::remove_file(&path).ok();
}
}