#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct PbrtExportConfig {
pub version: String,
pub film_width: u32,
pub film_height: u32,
pub samples_per_pixel: u32,
pub output_filename: String,
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct PbrtMaterial {
pub name: String,
pub diffuse: [f32; 3],
pub roughness: f32,
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct PbrtShape {
pub shape_type: String,
pub vertices: Vec<[f32; 3]>,
pub faces: Vec<[u32; 3]>,
pub material_name: String,
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct PbrtCamera {
pub position: [f32; 3],
pub look_at: [f32; 3],
pub fov_deg: f32,
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct PbrtInfiniteLight {
pub intensity: f32,
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct PbrtScene {
pub config: PbrtExportConfig,
pub shapes: Vec<PbrtShape>,
pub camera: Option<PbrtCamera>,
pub infinite_lights: Vec<PbrtInfiniteLight>,
}
#[allow(dead_code)]
pub fn default_pbrt_config() -> PbrtExportConfig {
PbrtExportConfig {
version: "3".to_string(),
film_width: 1280,
film_height: 720,
samples_per_pixel: 64,
output_filename: "output.exr".to_string(),
}
}
#[allow(dead_code)]
pub fn new_pbrt_scene(cfg: &PbrtExportConfig) -> PbrtScene {
PbrtScene {
config: cfg.clone(),
shapes: Vec::new(),
camera: None,
infinite_lights: Vec::new(),
}
}
#[allow(dead_code)]
pub fn pbrt_add_shape(scene: &mut PbrtScene, shape: PbrtShape) {
scene.shapes.push(shape);
}
#[allow(dead_code)]
pub fn pbrt_add_triangle_mesh(
scene: &mut PbrtScene,
verts: &[[f32; 3]],
faces: &[[u32; 3]],
material: &str,
) {
let shape = PbrtShape {
shape_type: "trianglemesh".to_string(),
vertices: verts.to_vec(),
faces: faces.to_vec(),
material_name: material.to_string(),
};
scene.shapes.push(shape);
}
#[allow(dead_code)]
pub fn pbrt_to_string(scene: &PbrtScene) -> String {
let mut out = String::new();
out.push_str(&format!(
"# PBRT v{} scene generated by oxihuman-export\n\n",
scene.config.version
));
out.push_str(&format!(
"Film \"image\" \"integer xresolution\" [{xr}] \"integer yresolution\" [{yr}] \
\"integer pixelsamples\" [{spp}] \"string filename\" [\"{fname}\"]\n\n",
xr = scene.config.film_width,
yr = scene.config.film_height,
spp = scene.config.samples_per_pixel,
fname = scene.config.output_filename,
));
if let Some(cam) = &scene.camera {
let p = cam.position;
let l = cam.look_at;
out.push_str(&format!(
"LookAt {} {} {} {} {} {} 0 1 0\n",
p[0], p[1], p[2], l[0], l[1], l[2]
));
out.push_str(&format!(
"Camera \"perspective\" \"float fov\" [{}]\n\n",
cam.fov_deg
));
}
out.push_str("WorldBegin\n\n");
for light in &scene.infinite_lights {
out.push_str(&format!(
"LightSource \"infinite\" \"rgb L\" [{v} {v} {v}]\n",
v = light.intensity
));
}
if !scene.infinite_lights.is_empty() {
out.push('\n');
}
for shape in &scene.shapes {
out.push_str(&format!(
"NamedMaterial \"{}\"\n",
shape.material_name
));
out.push_str(&format!("Shape \"{}\"", shape.shape_type));
if !shape.vertices.is_empty() {
let pts: Vec<String> = shape
.vertices
.iter()
.map(|v| format!("{} {} {}", v[0], v[1], v[2]))
.collect();
out.push_str(&format!(" \"point3 P\" [{}]", pts.join(" ")));
}
if !shape.faces.is_empty() {
let idx: Vec<String> = shape
.faces
.iter()
.flat_map(|f| [f[0].to_string(), f[1].to_string(), f[2].to_string()])
.collect();
out.push_str(&format!(" \"integer indices\" [{}]", idx.join(" ")));
}
out.push('\n');
}
out.push_str("\nWorldEnd\n");
out
}
#[allow(dead_code)]
pub fn pbrt_write_to_file(scene: &PbrtScene, path: &str) -> Result<(), String> {
let content = pbrt_to_string(scene);
std::fs::write(path, content).map_err(|e| e.to_string())
}
#[allow(dead_code)]
pub fn pbrt_shape_count(scene: &PbrtScene) -> usize {
scene.shapes.len()
}
#[allow(dead_code)]
pub fn pbrt_set_camera(
scene: &mut PbrtScene,
pos: [f32; 3],
look_at: [f32; 3],
fov_deg: f32,
) {
scene.camera = Some(PbrtCamera {
position: pos,
look_at,
fov_deg,
});
}
#[allow(dead_code)]
pub fn pbrt_add_infinite_light(scene: &mut PbrtScene, intensity: f32) {
scene.infinite_lights.push(PbrtInfiniteLight { intensity });
}
#[allow(dead_code)]
pub fn pbrt_scene_clear(scene: &mut PbrtScene) {
scene.shapes.clear();
scene.infinite_lights.clear();
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let cfg = default_pbrt_config();
assert_eq!(cfg.film_width, 1280);
assert_eq!(cfg.film_height, 720);
assert_eq!(cfg.samples_per_pixel, 64);
}
#[test]
fn test_new_scene_empty() {
let cfg = default_pbrt_config();
let scene = new_pbrt_scene(&cfg);
assert_eq!(pbrt_shape_count(&scene), 0);
assert!(scene.camera.is_none());
assert!(scene.infinite_lights.is_empty());
}
#[test]
fn test_add_shape() {
let cfg = default_pbrt_config();
let mut scene = new_pbrt_scene(&cfg);
let shape = PbrtShape {
shape_type: "sphere".to_string(),
vertices: Vec::new(),
faces: Vec::new(),
material_name: "matte".to_string(),
};
pbrt_add_shape(&mut scene, shape);
assert_eq!(pbrt_shape_count(&scene), 1);
}
#[test]
fn test_add_triangle_mesh() {
let cfg = default_pbrt_config();
let mut scene = new_pbrt_scene(&cfg);
let verts = [[0.0f32, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
let faces = [[0u32, 1, 2]];
pbrt_add_triangle_mesh(&mut scene, &verts, &faces, "matte");
assert_eq!(pbrt_shape_count(&scene), 1);
assert_eq!(scene.shapes[0].vertices.len(), 3);
assert_eq!(scene.shapes[0].faces.len(), 1);
}
#[test]
fn test_pbrt_to_string_contains_world() {
let cfg = default_pbrt_config();
let scene = new_pbrt_scene(&cfg);
let s = pbrt_to_string(&scene);
assert!(s.contains("WorldBegin"));
assert!(s.contains("WorldEnd"));
}
#[test]
fn test_set_camera() {
let cfg = default_pbrt_config();
let mut scene = new_pbrt_scene(&cfg);
pbrt_set_camera(&mut scene, [0.0, 1.0, -5.0], [0.0, 0.0, 0.0], 60.0);
assert!(scene.camera.is_some());
let cam = scene.camera.as_ref().expect("should succeed");
assert!((cam.fov_deg - 60.0).abs() < 1e-5);
}
#[test]
fn test_add_infinite_light() {
let cfg = default_pbrt_config();
let mut scene = new_pbrt_scene(&cfg);
pbrt_add_infinite_light(&mut scene, 1.5);
assert_eq!(scene.infinite_lights.len(), 1);
assert!((scene.infinite_lights[0].intensity - 1.5).abs() < 1e-5);
}
#[test]
fn test_scene_clear() {
let cfg = default_pbrt_config();
let mut scene = new_pbrt_scene(&cfg);
let verts = [[0.0f32, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
let faces = [[0u32, 1, 2]];
pbrt_add_triangle_mesh(&mut scene, &verts, &faces, "matte");
pbrt_add_infinite_light(&mut scene, 1.0);
pbrt_scene_clear(&mut scene);
assert_eq!(pbrt_shape_count(&scene), 0);
assert!(scene.infinite_lights.is_empty());
}
#[test]
fn test_to_string_has_film() {
let cfg = default_pbrt_config();
let scene = new_pbrt_scene(&cfg);
let s = pbrt_to_string(&scene);
assert!(s.contains("Film"));
assert!(s.contains("1280"));
assert!(s.contains("720"));
}
#[test]
fn test_write_to_file() {
let cfg = default_pbrt_config();
let scene = new_pbrt_scene(&cfg);
let path = "/tmp/pbrt_export_test_oxihuman.pbrt";
let result = pbrt_write_to_file(&scene, path);
assert!(result.is_ok());
let _ = std::fs::remove_file(path);
}
}