Something went wrong. Try again.
Homebrew 4D Game Engine In Rust, inspired by Engine4D
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466//! Headless visual showcase for the Rust4D primitive catalog.//!//! This is both a demo generator and a regression tool. It renders each//! primitive through the real GPU slice + render pipelines into offscreen//! textures and writes PPM frames, no window needed.//!//! Usage://!//! ```bash//! cargo run --example shape_showcase .scratchpad/captures-gallery//! magick .scratchpad/captures-gallery/duocylinder_mid_identity.ppm duocylinder.png//! ```//!//! What to look for://! - hypersphere: sphere grows/shrinks across W offsets//! - spherinder: sphere remains roughly constant through its W tube//! - duocylinder: a torus-like slice at W=0//! - no cracks or hairline T-junctions (would indicate a broken primitive//! tetrahedralization despite the CPU watertightness tests)
use rust4d_core::{Material, ShapeTemplate, Transform4D};use rust4d_render::camera4d::Camera4D;use rust4d_render::pipeline::{ perspective_matrix, RenderPipeline, RenderUniforms, SliceParams, SlicePipeline,};use rust4d_render::RenderableGeometry;
use std::path::{Path, PathBuf};
const WIDTH: u32 = 900;const HEIGHT: u32 = 700;const MAX_TRIANGLES: usize = 1_200_000;
struct ShowcaseShape { name: &'static str, template: ShapeTemplate, color: [f32; 4], radius: f32,}
impl ShowcaseShape { fn geometry(&self) -> RenderableGeometry { let shape = self.template.create_shape(); let mut geom = RenderableGeometry::new(); let material = Material { base_color: self.color, }; geom.add_components_with_color( &Transform4D::identity(), shape.as_ref(), &material, &|_v, material| material.base_color, ); geom }}
struct HeadlessGpu { device: wgpu::Device, queue: wgpu::Queue, slice_pipeline: SlicePipeline, render_pipeline: RenderPipeline, color_texture: wgpu::Texture, readback_buffer: wgpu::Buffer, padded_bytes_per_row: u32,}
impl HeadlessGpu { fn new() -> Self { let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor { backends: wgpu::Backends::all(), ..Default::default() });
let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions { power_preference: wgpu::PowerPreference::default(), compatible_surface: None, force_fallback_adapter: false, })) .expect("no GPU adapter available");
let info = adapter.get_info(); println!( "[GPU] adapter: {} ({:?}, {:?})", info.name, info.device_type, info.backend );
let (device, queue) = pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor { label: Some("Shape Showcase Headless Device"), required_features: wgpu::Features::empty(), required_limits: wgpu::Limits::default(), memory_hints: wgpu::MemoryHints::default(), trace: wgpu::Trace::Off, })) .expect("failed to create device");
let slice_pipeline = SlicePipeline::new(&device, MAX_TRIANGLES); let format = wgpu::TextureFormat::Rgba8UnormSrgb; let mut render_pipeline = RenderPipeline::new(&device, format); render_pipeline.ensure_depth_texture(&device, WIDTH, HEIGHT);
let color_texture = device.create_texture(&wgpu::TextureDescriptor { label: Some("Shape Showcase Color"), size: wgpu::Extent3d { width: WIDTH, height: HEIGHT, depth_or_array_layers: 1, }, mip_level_count: 1, sample_count: 1, dimension: wgpu::TextureDimension::D2, format, usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC, view_formats: &[], });
let unpadded = WIDTH * 4; let padded_bytes_per_row = unpadded.div_ceil(wgpu::COPY_BYTES_PER_ROW_ALIGNMENT) * wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
let readback_buffer = device.create_buffer(&wgpu::BufferDescriptor { label: Some("Shape Showcase Readback"), size: (padded_bytes_per_row * HEIGHT) as u64, usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ, mapped_at_creation: false, });
Self { device, queue, slice_pipeline, render_pipeline, color_texture, readback_buffer, padded_bytes_per_row, } }
fn upload_geometry(&mut self, geometry: &RenderableGeometry) { self.slice_pipeline.upload_tetrahedra( &self.device, &geometry.vertices, &geometry.tetrahedra, ); println!( "[GPU] uploaded {} vertices, {} tetrahedra", geometry.vertex_count(), geometry.tetrahedron_count() ); }
fn capture(&mut self, camera: &Camera4D, tetrahedron_count: u32, path: &Path) -> u32 { let pos = camera.position; let slice_params = SliceParams { slice_w: camera.get_slice_w(), tetrahedron_count, _padding: [0.0; 2], camera_matrix: camera.rotation_matrix(), camera_position: [pos.x, pos.y, pos.z, pos.w], }; self.slice_pipeline .update_params(&self.queue, &slice_params);
let projection_matrix = perspective_matrix( 40.0_f32.to_radians(), WIDTH as f32 / HEIGHT as f32, 0.1, 100.0, ); let identity = [ [1.0, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0], [0.0, 0.0, 1.0, 0.0], [0.0, 0.0, 0.0, 1.0], ]; self.render_pipeline.update_uniforms( &self.queue, &RenderUniforms { view_matrix: identity, projection_matrix, light_dir: [0.4, 0.9, 0.35], _padding: 0.0, ambient_strength: 0.35, diffuse_strength: 0.75, w_color_strength: 0.25, w_range: 2.5, ..RenderUniforms::default() }, );
let view = self .color_texture .create_view(&wgpu::TextureViewDescriptor::default()); let mut encoder = self .device .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("Shape Showcase Encoder"), });
self.slice_pipeline.reset_counter(&self.queue); self.slice_pipeline.run_slice_pass(&mut encoder); self.render_pipeline .prepare_indirect_draw(&mut encoder, self.slice_pipeline.counter_buffer()); self.render_pipeline.render( &mut encoder, &view, self.slice_pipeline.output_buffer(), wgpu::Color { r: 0.035, g: 0.035, b: 0.055, a: 1.0, }, );
encoder.copy_texture_to_buffer( wgpu::TexelCopyTextureInfo { texture: &self.color_texture, mip_level: 0, origin: wgpu::Origin3d::ZERO, aspect: wgpu::TextureAspect::All, }, wgpu::TexelCopyBufferInfo { buffer: &self.readback_buffer, layout: wgpu::TexelCopyBufferLayout { offset: 0, bytes_per_row: Some(self.padded_bytes_per_row), rows_per_image: Some(HEIGHT), }, }, wgpu::Extent3d { width: WIDTH, height: HEIGHT, depth_or_array_layers: 1, }, );
self.queue.submit(std::iter::once(encoder.finish()));
let slice = self.readback_buffer.slice(..); let (tx, rx) = std::sync::mpsc::channel(); slice.map_async(wgpu::MapMode::Read, move |r| tx.send(r).unwrap()); let _ = self.device.poll(wgpu::PollType::Wait); rx.recv().unwrap().expect("readback map failed");
{ let data = slice.get_mapped_range(); write_ppm(path, &data, WIDTH, HEIGHT, self.padded_bytes_per_row); } self.readback_buffer.unmap();
let vertex_count = self.read_counter(); let tris = vertex_count / 3; println!( "[CAPTURE] {} slice={:.3} triangles={tris}", path.file_name().unwrap().to_string_lossy(), camera.get_slice_w(), ); tris }
fn read_counter(&self) -> u32 { let staging = self.device.create_buffer(&wgpu::BufferDescriptor { label: Some("Shape Showcase Counter Staging"), size: 4, usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ, mapped_at_creation: false, }); let mut encoder = self .device .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None }); encoder.copy_buffer_to_buffer(self.slice_pipeline.counter_buffer(), 0, &staging, 0, 4); self.queue.submit(std::iter::once(encoder.finish()));
let slice = staging.slice(..); let (tx, rx) = std::sync::mpsc::channel(); slice.map_async(wgpu::MapMode::Read, move |r| tx.send(r).unwrap()); let _ = self.device.poll(wgpu::PollType::Wait); rx.recv().unwrap().expect("counter map failed"); let count = { let data = slice.get_mapped_range(); u32::from_le_bytes(data[..4].try_into().unwrap()) }; staging.unmap(); count }}
fn write_ppm(path: &Path, data: &[u8], width: u32, height: u32, padded_bytes_per_row: u32) { let mut out = Vec::with_capacity((width * height * 3) as usize + 32); out.extend_from_slice(format!("P6\n{} {}\n255\n", width, height).as_bytes()); for y in 0..height { let row_start = (y * padded_bytes_per_row) as usize; for x in 0..width { let px = row_start + (x * 4) as usize; out.extend_from_slice(&data[px..px + 3]); } } std::fs::write(path, out).expect("failed to write ppm");}
fn shapes() -> Vec<ShowcaseShape> { vec![ ShowcaseShape { name: "tesseract", template: ShapeTemplate::tesseract(2.2), color: [1.0, 0.82, 0.32, 1.0], radius: 1.9, }, ShowcaseShape { name: "hypersphere", template: ShapeTemplate::Hypersphere { radius: 1.25, subdivisions: 2, }, color: [0.35, 0.70, 1.0, 1.0], radius: 1.25, }, ShowcaseShape { name: "pentachoron", template: ShapeTemplate::Pentachoron { circumradius: 1.4 }, color: [1.0, 0.38, 0.34, 1.0], radius: 1.4, }, ShowcaseShape { name: "hexadecachoron", template: ShapeTemplate::Hexadecachoron { circumradius: 1.35 }, color: [0.55, 1.0, 0.48, 1.0], radius: 1.35, }, ShowcaseShape { name: "icositetrachoron", template: ShapeTemplate::Icositetrachoron { circumradius: 1.5 }, color: [0.72, 0.45, 1.0, 1.0], radius: 1.5, }, ShowcaseShape { name: "hexacosichoron", template: ShapeTemplate::Hexacosichoron { circumradius: 1.2 }, color: [1.0, 0.55, 0.88, 1.0], radius: 1.2, }, ShowcaseShape { name: "spherinder", template: ShapeTemplate::Spherinder { radius: 1.05, half_height: 1.25, subdivisions: 2, }, color: [0.28, 0.95, 0.88, 1.0], radius: 1.65, }, ShowcaseShape { name: "cubinder", template: ShapeTemplate::Cubinder { radius: 1.05, half_size: 0.9, segments: 32, }, color: [0.95, 0.72, 0.24, 1.0], radius: 1.65, }, ShowcaseShape { name: "duocylinder", template: ShapeTemplate::Duocylinder { radius_xy: 1.0, radius_zw: 1.0, segments: 32, }, color: [0.40, 0.65, 1.0, 1.0], radius: 1.45, }, ]}
fn camera_for(angle: &str, slice_offset: f32) -> Camera4D { let mut camera = Camera4D::new(); camera.slice_offset = slice_offset; match angle { "xw" => camera.rotate_xw(0.45), "zw" => camera.rotate_w(0.45), _ => {} } // Keep the object at the origin centered in the slice plane for every // orientation. Leaving the camera at world Z=5 after a ZW rotation makes // the plane miss small objects entirely; positioning along `-forward` // preserves the usual identity-camera view distance while maintaining // dot(ana, origin - camera.position) == 0. camera.position = -camera.forward() * 5.0; camera}
fn main() { env_logger::init();
let out_dir = std::env::args() .nth(1) .map(PathBuf::from) .unwrap_or_else(|| PathBuf::from(".scratchpad/captures-gallery")); std::fs::create_dir_all(&out_dir).expect("create output dir");
let mut gpu = HeadlessGpu::new(); let mut total_captures = 0usize; let mut zero_captures = 0usize;
for shape in shapes() { println!("\n[SHAPE] {}", shape.name); let geom = shape.geometry(); let tetrahedron_count = geom.tetrahedron_count() as u32; gpu.upload_geometry(&geom);
for angle in ["identity", "xw", "zw"] { for (label, slice_offset) in [ ("minus", -0.20 * shape.radius), ("mid", 0.0), ("plus", 0.20 * shape.radius), ] { let camera = camera_for(angle, slice_offset); let path = out_dir.join(format!("{}_{}_{}.ppm", shape.name, label, angle)); let tris = gpu.capture(&camera, tetrahedron_count, &path); total_captures += 1; if tris == 0 { zero_captures += 1; eprintln!( "[WARN] zero triangles for {} {label} {angle}; inspect if intentional", shape.name ); } } } }
println!( "\n[SUMMARY] {total_captures} captures written to {}; zero-triangle captures: {zero_captures}", out_dir.display() );}
#[cfg(test)]mod tests { use super::*;
#[test] fn test_showcase_shapes_create_geometry() { for shape in shapes() { let geom = shape.geometry(); assert!(geom.vertex_count() > 0, "{} has no vertices", shape.name); assert!( geom.tetrahedron_count() > 0, "{} has no tetrahedra", shape.name ); } }
#[test] fn test_showcase_gpu_types_still_match() { // Keeps this example honest if the pipeline types change. assert_eq!(std::mem::size_of::<rust4d_render::pipeline::Vertex4D>(), 32); assert_eq!( std::mem::size_of::<rust4d_render::pipeline::GpuTetrahedron>(), 16 ); }}