use image::imageops::FilterType; use std::env; use std::error::Error; use std::fs::File; use std::io::{BufWriter, Write}; use std::path::{Path, PathBuf}; type Result = std::result::Result>; const SH_C0: f32 = 0.282_094_79; fn unit(v: [f32; 3]) -> [f32; 3] { let n = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt(); if n < 1e-12 { [0.0, 0.0, 0.0] } else { [v[0] / n, v[1] / n, v[2] / n] } } fn cross(a: [f32; 3], b: [f32; 3]) -> [f32; 3] { [ a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0], ] } /// Quaternion (w, x, y, z) from a rotation matrix whose columns are the /// local x/y/z axes in world space, matching pytorch3d's convention. fn quat_from_rot(r: [[f32; 3]; 3]) -> [f32; 4] { let trace = r[0][0] + r[1][1] + r[2][2]; let (w, x, y, z); if trace > 0.0 { let s = (trace + 1.0).sqrt() * 2.0; w = 0.25 * s; x = (r[2][1] - r[1][2]) / s; y = (r[0][2] - r[2][0]) / s; z = (r[1][0] - r[0][1]) / s; } else if r[0][0] > r[1][1] && r[0][0] > r[2][2] { let s = (1.0 + r[0][0] - r[1][1] - r[2][2]).sqrt() * 2.0; w = (r[2][1] - r[1][2]) / s; x = 0.25 * s; y = (r[0][1] + r[1][0]) / s; z = (r[0][2] + r[2][0]) / s; } else if r[1][1] > r[2][2] { let s = (1.0 + r[1][1] - r[0][0] - r[2][2]).sqrt() * 2.0; w = (r[0][2] - r[2][0]) / s; x = (r[0][1] + r[1][0]) / s; y = 0.25 * s; z = (r[1][2] + r[2][1]) / s; } else { let s = (1.0 + r[2][2] - r[0][0] - r[1][1]).sqrt() * 2.0; w = (r[1][0] - r[0][1]) / s; x = (r[0][2] + r[2][0]) / s; y = (r[1][2] + r[2][1]) / s; z = 0.25 * s; } let q = [w, x, y, z]; let n = (q[0] * q[0] + q[1] * q[1] + q[2] * q[2] + q[3] * q[3]).sqrt(); [q[0] / n, q[1] / n, q[2] / n, q[3] / n] } fn load_depth(path: &Path) -> Result<(Vec, usize, usize)> { let bytes = std::fs::read(path)?; if bytes.len() % 4 != 0 { return Err(format!("depth size {} is not a multiple of 4", bytes.len()).into()); } let vals: Vec = bytes .chunks_exact(4) .map(|b| f32::from_le_bytes([b[0], b[1], b[2], b[3]])) .collect(); // The caller supplies --width/--height; a square fallback is kept for // quick tests. The WorldGen pipeline writes 1600x800. Ok((vals, 0, 0)) } fn pano_rgb(path: &Path, w: usize, h: usize) -> Result> { let img = image::open(path)?.to_rgb8(); let resized = image::imageops::resize(&img, w as u32, h as u32, FilterType::Triangle); Ok(resized.into_raw()) } fn rays(w: usize, h: usize) -> Vec<[f32; 3]> { let mut out = Vec::with_capacity(w * h); for y in 0..h { let v = (y as f32 + 0.5) / h as f32; let theta = v * std::f32::consts::PI - std::f32::consts::FRAC_PI_2; let (st, ct) = theta.sin_cos(); for x in 0..w { let u = (x as f32 + 0.5) / w as f32; let phi = u * std::f32::consts::TAU - std::f32::consts::PI; let (sp, cp) = phi.sin_cos(); out.push([ct * sp, st, ct * cp]); } } out } struct Splat { centers: Vec<[f32; 3]>, colors: Vec<[f32; 3]>, scales: Vec<[f32; 3]>, quats: Vec<[f32; 4]>, } fn build_splats(rgb: &[u8], depth: &[f32], w: usize, h: usize) -> Splat { let delta_phi = std::f32::consts::TAU / w as f32; let delta_theta = std::f32::consts::PI / h as f32; let scale_factor = 0.65f32; let mut s = Splat { centers: Vec::with_capacity(w * h), colors: Vec::with_capacity(w * h), scales: Vec::with_capacity(w * h), quats: Vec::with_capacity(w * h), }; let rs = rays(w, h); for i in 0..w * h { let d = depth[i]; if !(d > 0.0) || !d.is_finite() { continue; } let ray = rs[i]; let y = i / w; // WorldGen uses the polar angle 0..pi for the equirectangular // sin(theta) footprint correction, not the latitude -pi/2..pi/2. let theta = (y as f32 + 0.5) / h as f32 * std::f32::consts::PI; let sigma_x = d * delta_phi * theta.sin() * scale_factor; let sigma_y = d * delta_theta * scale_factor; let sigma_z = 1e-3f32; let up = [0.0, 1.0, 0.0]; let mut x_axis = unit(cross(up, ray)); if x_axis[0].is_nan() || x_axis[1].is_nan() || x_axis[2].is_nan() { x_axis = unit(cross([1.0, 0.0, 0.0], ray)); } let y_axis = unit(cross(ray, x_axis)); let r = [ [x_axis[0], y_axis[0], ray[0]], [x_axis[1], y_axis[1], ray[1]], [x_axis[2], y_axis[2], ray[2]], ]; s.centers.push([ray[0] * d, ray[1] * d, ray[2] * d]); s.colors.push([ rgb[i * 3] as f32 / 255.0, rgb[i * 3 + 1] as f32 / 255.0, rgb[i * 3 + 2] as f32 / 255.0, ]); s.scales.push([sigma_x, sigma_y, sigma_z]); s.quats.push(quat_from_rot(r)); } s } fn write_gaussians(path: &Path, s: &Splat) -> Result<()> { let mut f = BufWriter::new(File::create(path)?); writeln!(f, "ply")?; writeln!(f, "format binary_little_endian 1.0")?; writeln!(f, "element vertex {}", s.centers.len())?; for p in ["x", "y", "z", "nx", "ny", "nz"] { writeln!(f, "property float {p}")?; } for i in 0..3 { writeln!(f, "property float f_dc_{i}")?; } writeln!(f, "property float opacity")?; for i in 0..3 { writeln!(f, "property float scale_{i}")?; } for i in 0..4 { writeln!(f, "property float rot_{i}")?; } writeln!(f, "end_header")?; for i in 0..s.centers.len() { let c = s.centers[i]; let col = s.colors[i]; let sc = s.scales[i]; let q = s.quats[i]; let mut row = Vec::with_capacity(17 * 4); for v in [ c[0], c[1], c[2], 0.0, 0.0, 0.0, (col[0] - 0.5) / SH_C0, (col[1] - 0.5) / SH_C0, (col[2] - 0.5) / SH_C0, 1.0, sc[0].ln(), sc[1].ln(), sc[2].ln(), q[0], q[1], q[2], q[3], ] { row.extend_from_slice(&v.to_le_bytes()); } f.write_all(&row)?; } Ok(()) } fn median3(depth: &[f32], w: usize, h: usize) -> Vec { let mut out = vec![0.0f32; w * h]; for y in 0..h { for x in 0..w { let mut v = [0.0f32; 9]; let mut n = 0; for dy in -1i32..=1 { for dx in -1i32..=1 { let yy = (y as i32 + dy).clamp(0, h as i32 - 1) as usize; let xx = (x as i32 + dx).rem_euclid(w as i32) as usize; let d = depth[yy * w + xx]; if d.is_finite() && d > 0.0 { v[n] = d; n += 1; } } } if n == 0 { out[y * w + x] = depth[y * w + x]; } else { v[..n].sort_by(|a, b| a.partial_cmp(b).unwrap()); out[y * w + x] = v[n / 2]; } } } out } fn write_mesh(path: &Path, rgb: &[u8], depth: &[f32], w: usize, h: usize) -> Result { let rs = rays(w, h); let depth = median3(depth, w, h); let mut index = vec![-1i64; w * h]; let mut verts: Vec<([f32; 3], [u8; 3])> = Vec::new(); for i in 0..w * h { if depth[i] > 0.0 && depth[i].is_finite() { index[i] = verts.len() as i64; verts.push(( [rs[i][0] * depth[i], rs[i][1] * depth[i], rs[i][2] * depth[i]], [rgb[i * 3], rgb[i * 3 + 1], rgb[i * 3 + 2]], )); } } let mut tris: Vec<[i32; 3]> = Vec::new(); for y in 0..h - 1 { for x in 0..w { let x1 = (x + 1) % w; let ia = y * w + x; let ib = y * w + x1; let ic = (y + 1) * w + x; let id = (y + 1) * w + x1; let a = index[ia]; let b = index[ib]; let c = index[ic]; let d = index[id]; if a < 0 || b < 0 || c < 0 || d < 0 { continue; } let ds = [depth[ia], depth[ib], depth[ic], depth[id]]; let dmin = ds.iter().cloned().fold(f32::INFINITY, f32::min); let dmax = ds.iter().cloned().fold(0.0f32, f32::max); // Do not bridge a foreground/background depth discontinuity with a // long, stretched quad; leave a hole at the silhouette instead. if dmin > 0.0 && dmax / dmin > 1.8 { continue; } tris.push([a as i32, c as i32, b as i32]); tris.push([b as i32, c as i32, d as i32]); } } let mut f = BufWriter::new(File::create(path)?); writeln!(f, "ply")?; writeln!(f, "format binary_little_endian 1.0")?; writeln!(f, "element vertex {}", verts.len())?; for p in ["x", "y", "z"] { writeln!(f, "property float {p}")?; } for p in ["red", "green", "blue"] { writeln!(f, "property uchar {p}")?; } writeln!(f, "element face {}", tris.len())?; writeln!(f, "property list uchar int vertex_indices")?; writeln!(f, "end_header")?; for (p, c) in &verts { for v in p { f.write_all(&v.to_le_bytes())?; } f.write_all(c)?; } for t in &tris { f.write_all(&[3u8])?; for v in t { f.write_all(&v.to_le_bytes())?; } } Ok(tris.len()) } fn usage() -> ! { eprintln!( "worldgen-vk 0.1.0\n\ usage:\n\ worldgen-vk splat --pano P.png --depth D.f32 --width W --height H --out DIR\n\ worldgen-vk mesh --pano P.png --depth D.f32 --width W --height H --out DIR [--max-size N]\n\ worldgen-vk both --pano P.png --depth D.f32 --width W --height H --out DIR [--max-size N]\n\ depth is raw little-endian f32, H*W, as written by the WorldGen/DA-2 bridge." ); std::process::exit(2); } fn main() -> Result<()> { let args: Vec = env::args().collect(); if args.len() < 2 { usage(); } let mode = args[1].as_str(); let mut pano = None; let mut depth_path = None; let mut out = None; let mut w = 0usize; let mut h = 0usize; let mut max_size = 4096usize; let mut i = 2; while i < args.len() { match args[i].as_str() { "--pano" => { pano = Some(PathBuf::from(&args[i + 1])); i += 2; } "--depth" => { depth_path = Some(PathBuf::from(&args[i + 1])); i += 2; } "--out" => { out = Some(PathBuf::from(&args[i + 1])); i += 2; } "--width" => { w = args[i + 1].parse()?; i += 2; } "--height" => { h = args[i + 1].parse()?; i += 2; } "--max-size" => { max_size = args[i + 1].parse()?; i += 2; } _ => usage(), } } let (pano, depth_path, out) = match (pano, depth_path, out) { (Some(a), Some(b), Some(c)) => (a, b, c), _ => usage(), }; if w == 0 || h == 0 { usage(); } let (depth, _, _) = load_depth(&depth_path)?; if depth.len() != w * h { return Err(format!("depth has {} values, expected {}x{}", depth.len(), w, h).into()); } std::fs::create_dir_all(&out)?; let rgb = pano_rgb(&pano, w, h)?; let mut made = Vec::new(); let mut splat_count = 0; let mut tri_count = 0; if mode == "splat" || mode == "both" { let s = build_splats(&rgb, &depth, w, h); splat_count = s.centers.len(); write_gaussians(&out.join("gaussians.ply"), &s)?; made.push("gaussians.ply"); } if mode == "mesh" || mode == "both" { // WorldGen's torch mesh path caps the long edge at 4096. let scale = if w.max(h) > max_size { max_size as f32 / w.max(h) as f32 } else { 1.0 }; let (mw, mh) = ( ((w as f32 * scale) as usize).max(2), ((h as f32 * scale) as usize).max(2), ); let rgb2 = image::imageops::resize( &image::RgbImage::from_raw(w as u32, h as u32, rgb.clone()).unwrap(), mw as u32, mh as u32, FilterType::Triangle, ) .into_raw(); let mut depth2 = vec![0f32; mw * mh]; for y in 0..mh { let sy = (y as f32 + 0.5) * h as f32 / mh as f32; for x in 0..mw { let sx = (x as f32 + 0.5) * w as f32 / mw as f32; let x0 = (sx as usize).min(w - 1); let y0 = (sy as usize).min(h - 1); depth2[y * mw + x] = depth[y0 * w + x0]; } } tri_count = write_mesh(&out.join("mesh.ply"), &rgb2, &depth2, mw, mh)?; made.push("mesh.ply"); } let meta = format!( "{{\n \"mode\": \"{}\",\n \"width\": {},\n \"height\": {},\n \"splats\": {},\n \"triangles\": {},\n \"files\": [{}],\n \"note\": \"WorldGen RGBD panorama to 3DGS/mesh, Rust port\"\n}}\n", mode, w, h, splat_count, tri_count, made.iter().map(|s| format!("\"{s}\"")).collect::>().join(", ") ); std::fs::write(out.join("worldgen.json"), meta)?; println!( "{}: {} splats, {} triangles -> {}", mode, splat_count, tri_count, out.display() ); Ok(()) }