refresh: stack al día (vello 0.7 / wgpu 27 / parley 0.6) + motor 3D voxel
Re-sincroniza las fuentes desde el monorepo (estaba en vello 0.5/wgpu 24 y con la estructura vieja de eventloop) y suma el 3D: - bump del workspace a vello 0.7 / wgpu 27 / parley 0.6, + accesskit 0.24 / accesskit_winit 0.33 / vello_hybrid 0.0.9. - nuevos crates: llimphi-3d (voxels ray-march + mallas en un depth compartido, montable dentro de un View 2D vía set_viewport+scissor) y llimphi-voxel (world-gen, personajes, director de escenas) + shared/foreign-vox (puente .vox). - README: sección "Not just 2D — a 3D voxel engine" + GIF (docs/llimphi_voxel.gif). - excluido modules/allichay (arrastra deps fuera del alcance del front-door). - cargo check --workspace: verde. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,169 @@
|
||||
//! Demo headless de las **edades cuantizadas** del personaje: los 5 estadios
|
||||
//! (bebé/niño/joven/adulto/viejo) parados en fila sobre arena, para ver la
|
||||
//! progresión de proporciones (el bebé cabezón → el adulto alto). El corto arranca
|
||||
//! mostrando al **niño** recién nacido.
|
||||
//!
|
||||
//! `cargo run -p llimphi-voxel --example ages_demo --release` → `/tmp/ages.png`
|
||||
|
||||
use std::fs::File;
|
||||
use std::io::BufWriter;
|
||||
|
||||
use llimphi_3d::glam::Vec3;
|
||||
use llimphi_3d::{Atmosphere, Camera3d, Renderer3d, Scene3d, VoxelGrid, VoxelRenderer};
|
||||
use llimphi_hal::{wgpu, Hal};
|
||||
use llimphi_raster::peniko::Color;
|
||||
use llimphi_raster::{vello, Renderer};
|
||||
use llimphi_voxel::{Actor, Age, Material};
|
||||
|
||||
const W: u32 = 960;
|
||||
const H: u32 = 540;
|
||||
const FMT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
|
||||
|
||||
fn main() {
|
||||
let hal = pollster::block_on(Hal::new(None)).expect("hal");
|
||||
let mut renderer = Renderer::new(&hal).expect("renderer");
|
||||
|
||||
// Piso plano de arena (aísla los cuerpos; sin relieve que distraiga).
|
||||
let dim = [44u32, 16, 20];
|
||||
let mut grid = VoxelGrid::new(dim);
|
||||
for z in 0..dim[2] {
|
||||
for x in 0..dim[0] {
|
||||
grid.set(x, 0, z, Material::Sand.color());
|
||||
grid.set(x, 1, z, Material::Sand.color());
|
||||
}
|
||||
}
|
||||
grid.reset_dirty();
|
||||
let floor_top = 2.0; // y del suelo (sobre las 2 capas)
|
||||
|
||||
let mut vr = VoxelRenderer::new(&hal.device, &hal.queue, FMT, &grid);
|
||||
vr.sun_dir = [0.5, 0.7, 0.4];
|
||||
vr.atmosphere = Atmosphere { sky_zenith: [96, 150, 210], sky_horizon: [226, 208, 168], fog_density: 0.0 };
|
||||
|
||||
// 5 actores, uno por edad, espaciados en X. El grid se centra en el origen, así
|
||||
// la coord de mundo del actor = local − dim/2.
|
||||
let ages = [Age::Baby, Age::Child, Age::Teen, Age::Adult, Age::Elder];
|
||||
let palettes: [([f32; 3], [f32; 3]); 5] = [
|
||||
([0.90, 0.74, 0.60], [0.86, 0.40, 0.42]), // bebé
|
||||
([0.88, 0.70, 0.56], [0.36, 0.62, 0.82]), // niño
|
||||
([0.86, 0.68, 0.54], [0.40, 0.74, 0.46]), // joven
|
||||
([0.84, 0.66, 0.52], [0.82, 0.66, 0.30]), // adulto
|
||||
([0.82, 0.64, 0.50], [0.62, 0.52, 0.74]), // viejo
|
||||
];
|
||||
let mut actor_r = Vec::new();
|
||||
for (k, (age, (skin, shirt))) in ages.iter().zip(palettes).enumerate() {
|
||||
let lx = 12.0 + k as f32 * 5.0; // fila apretada centrada en el grid
|
||||
let wx = lx - dim[0] as f32 / 2.0;
|
||||
let wz = 0.0; // centro en z (mundo)
|
||||
let mut a = Actor::new(Vec3::new(wx, floor_top - dim[1] as f32 / 2.0, wz), std::f32::consts::PI)
|
||||
.with_age(*age)
|
||||
.with_colors(skin, shirt, [0.20, 0.22, 0.30]);
|
||||
a.look_at(None);
|
||||
let (v, i) = a.mesh();
|
||||
let mut r = Renderer3d::new(&hal.device, FMT);
|
||||
r.set_geometry(&hal.device, &v, &i);
|
||||
r.set_model(a.model());
|
||||
actor_r.push(r);
|
||||
}
|
||||
|
||||
// Cámara frontal baja y CERCA, encuadrando la fila a la altura del pecho.
|
||||
let feet_y = floor_top - dim[1] as f32 / 2.0;
|
||||
let camera = Camera3d::orbit(Vec3::new(0.0, feet_y + 1.0, 0.0), 0_f32.to_radians(), 8_f32.to_radians(), 17.0);
|
||||
|
||||
let refs: Vec<&Renderer3d> = actor_r.iter().collect();
|
||||
let mut scene = Scene3d::new();
|
||||
let pixels = render(&hal, &mut renderer, &mut scene, &mut vr, &refs, &camera);
|
||||
write_png(&pixels, "/tmp/ages.png");
|
||||
eprintln!("escrito /tmp/ages.png (bebé · niño · joven · adulto · viejo)");
|
||||
}
|
||||
|
||||
fn render(
|
||||
hal: &Hal,
|
||||
renderer: &mut Renderer,
|
||||
scene: &mut Scene3d,
|
||||
vr: &mut VoxelRenderer,
|
||||
meshes: &[&Renderer3d],
|
||||
camera: &Camera3d,
|
||||
) -> Vec<u8> {
|
||||
let inter = hal.device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some("inter"),
|
||||
size: wgpu::Extent3d { width: W, height: H, depth_or_array_layers: 1 },
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: FMT,
|
||||
usage: wgpu::TextureUsages::STORAGE_BINDING
|
||||
| wgpu::TextureUsages::TEXTURE_BINDING
|
||||
| wgpu::TextureUsages::RENDER_ATTACHMENT
|
||||
| wgpu::TextureUsages::COPY_SRC,
|
||||
view_formats: &[],
|
||||
});
|
||||
let view = inter.create_view(&wgpu::TextureViewDescriptor::default());
|
||||
renderer
|
||||
.render_to_view(hal, &vello::Scene::new(), &view, W, H, Color::from_rgba8(0, 0, 0, 255))
|
||||
.expect("base");
|
||||
let mut enc = hal
|
||||
.device
|
||||
.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("ages") });
|
||||
scene.render(&hal.device, &hal.queue, &mut enc, &view, (W, H), camera, Some(vr), meshes);
|
||||
hal.queue.submit(std::iter::once(enc.finish()));
|
||||
let _ = hal.device.poll(wgpu::PollType::wait_indefinitely());
|
||||
readback(hal, &inter)
|
||||
}
|
||||
|
||||
fn readback(hal: &Hal, target: &wgpu::Texture) -> Vec<u8> {
|
||||
let unpadded = (W * 4) as usize;
|
||||
let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT as usize;
|
||||
let padded = unpadded.div_ceil(align) * align;
|
||||
let buf = hal.device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("readback"),
|
||||
size: (padded * H as usize) as u64,
|
||||
usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
let mut enc = hal
|
||||
.device
|
||||
.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
|
||||
enc.copy_texture_to_buffer(
|
||||
wgpu::TexelCopyTextureInfo {
|
||||
texture: target,
|
||||
mip_level: 0,
|
||||
origin: wgpu::Origin3d::ZERO,
|
||||
aspect: wgpu::TextureAspect::All,
|
||||
},
|
||||
wgpu::TexelCopyBufferInfo {
|
||||
buffer: &buf,
|
||||
layout: wgpu::TexelCopyBufferLayout {
|
||||
offset: 0,
|
||||
bytes_per_row: Some(padded as u32),
|
||||
rows_per_image: Some(H),
|
||||
},
|
||||
},
|
||||
wgpu::Extent3d { width: W, height: H, depth_or_array_layers: 1 },
|
||||
);
|
||||
hal.queue.submit(std::iter::once(enc.finish()));
|
||||
let slice = buf.slice(..);
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
slice.map_async(wgpu::MapMode::Read, move |r| {
|
||||
let _ = tx.send(r);
|
||||
});
|
||||
let _ = hal.device.poll(wgpu::PollType::wait_indefinitely());
|
||||
rx.recv().unwrap().unwrap();
|
||||
let data = slice.get_mapped_range();
|
||||
let mut pixels = Vec::with_capacity((W * H * 4) as usize);
|
||||
for row in 0..H as usize {
|
||||
let s = row * padded;
|
||||
pixels.extend_from_slice(&data[s..s + unpadded]);
|
||||
}
|
||||
drop(data);
|
||||
buf.unmap();
|
||||
pixels
|
||||
}
|
||||
|
||||
fn write_png(pixels: &[u8], path: &str) {
|
||||
let file = File::create(path).expect("png");
|
||||
let mut enc = png::Encoder::new(BufWriter::new(file), W, H);
|
||||
enc.set_color(png::ColorType::Rgba);
|
||||
enc.set_depth(png::BitDepth::Eight);
|
||||
let mut wtr = enc.write_header().unwrap();
|
||||
wtr.write_image_data(pixels).unwrap();
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
//! Demo headless de la **secuencia de nacimiento** (modos de cámara): un montaje
|
||||
//! 2×2 de cuatro momentos —
|
||||
//! 1. la cámara cae del cielo mirando abajo (ve el huevo),
|
||||
//! 2. casi tocando suelo (el huevo se raja),
|
||||
//! 3. recién nacido: la cámara sale del sujeto,
|
||||
//! 4. plano de seguimiento detrás del niño.
|
||||
//!
|
||||
//! `cargo run -p llimphi-voxel --example birth_demo --release` → `/tmp/birth.png`
|
||||
|
||||
use std::fs::File;
|
||||
use std::io::BufWriter;
|
||||
|
||||
use llimphi_3d::glam::Vec3;
|
||||
use llimphi_3d::{Atmosphere, Renderer3d, Scene3d, VoxelGrid, VoxelRenderer};
|
||||
use llimphi_hal::{wgpu, Hal};
|
||||
use llimphi_raster::peniko::Color;
|
||||
use llimphi_raster::{vello, Renderer};
|
||||
use llimphi_voxel::{Age, BirthSequence, Egg, Hatchling, Material};
|
||||
|
||||
// Cada cuadro del montaje (mitad de un lienzo 960×540 → 2×2).
|
||||
const TW: u32 = 480;
|
||||
const TH: u32 = 270;
|
||||
const FMT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
|
||||
|
||||
fn main() {
|
||||
let hal = pollster::block_on(Hal::new(None)).expect("hal");
|
||||
let mut renderer = Renderer::new(&hal).expect("renderer");
|
||||
|
||||
// Piso plano de arena, centrado en el origen.
|
||||
let dim = [48u32, 28, 48];
|
||||
let mut grid = VoxelGrid::new(dim);
|
||||
for z in 0..dim[2] {
|
||||
for x in 0..dim[0] {
|
||||
grid.set(x, 0, z, Material::Sand.color());
|
||||
grid.set(x, 1, z, Material::Sand.color());
|
||||
}
|
||||
}
|
||||
grid.reset_dirty();
|
||||
let feet_y = 2.0 - dim[1] as f32 / 2.0; // suelo en mundo
|
||||
|
||||
let mut vr = VoxelRenderer::new(&hal.device, &hal.queue, FMT, &grid);
|
||||
vr.sun_dir = [0.5, 0.7, 0.4];
|
||||
vr.atmosphere = Atmosphere { sky_zenith: [96, 150, 210], sky_horizon: [226, 208, 168], fog_density: 0.0 };
|
||||
|
||||
// Huevo en el centro, sobre el suelo. La secuencia hace caer la cámara sobre él.
|
||||
let egg = Egg::new(Vec3::new(0.0, feet_y, 0.0), 1.4, Hatchling::human(Age::Baby));
|
||||
let seq = BirthSequence::new(egg);
|
||||
|
||||
// Cuatro instantes clave de la secuencia.
|
||||
let ts = [
|
||||
seq.t_land * 0.35, // cayendo, alto
|
||||
seq.t_land * 0.93, // casi en el suelo, el huevo se raja
|
||||
seq.t_land + seq.t_pull * 0.5, // saliendo del sujeto
|
||||
seq.duration(), // seguimiento detrás del niño
|
||||
];
|
||||
|
||||
// Lienzo final 2×2.
|
||||
let fw = TW * 2;
|
||||
let fh = TH * 2;
|
||||
let mut canvas = vec![0u8; (fw * fh * 4) as usize];
|
||||
|
||||
for (idx, &t) in ts.iter().enumerate() {
|
||||
let mut egg_t = seq.egg;
|
||||
egg_t.hatch = seq.hatch(t);
|
||||
let camera = seq.camera(t);
|
||||
|
||||
let mut meshes: Vec<Renderer3d> = Vec::new();
|
||||
let (ev, ei) = egg_t.mesh();
|
||||
let mut er = Renderer3d::new(&hal.device, FMT);
|
||||
er.set_geometry(&hal.device, &ev, &ei);
|
||||
er.set_model(egg_t.model());
|
||||
meshes.push(er);
|
||||
// El recién nacido aparece una vez que el huevo está bien abierto.
|
||||
if egg_t.hatch > 0.5 {
|
||||
let baby = seq.newborn();
|
||||
let (bv, bi) = baby.mesh();
|
||||
let mut br = Renderer3d::new(&hal.device, FMT);
|
||||
br.set_geometry(&hal.device, &bv, &bi);
|
||||
br.set_model(baby.model());
|
||||
meshes.push(br);
|
||||
}
|
||||
|
||||
let refs: Vec<&Renderer3d> = meshes.iter().collect();
|
||||
let mut scene = Scene3d::new();
|
||||
let cam = {
|
||||
let mut c = camera;
|
||||
c.fovy_rad = 55_f32.to_radians();
|
||||
c
|
||||
};
|
||||
let tile = render(&hal, &mut renderer, &mut scene, &mut vr, &refs, &cam);
|
||||
|
||||
// Pegar el cuadro en su celda del 2×2.
|
||||
let (cx, cy) = ((idx as u32 % 2) * TW, (idx as u32 / 2) * TH);
|
||||
for row in 0..TH {
|
||||
let src = (row * TW * 4) as usize;
|
||||
let dst = (((cy + row) * fw + cx) * 4) as usize;
|
||||
canvas[dst..dst + (TW * 4) as usize].copy_from_slice(&tile[src..src + (TW * 4) as usize]);
|
||||
}
|
||||
}
|
||||
|
||||
write_png(&canvas, fw, fh, "/tmp/birth.png");
|
||||
eprintln!("escrito /tmp/birth.png (caída · rajadura · nace · seguimiento)");
|
||||
}
|
||||
|
||||
fn render(
|
||||
hal: &Hal,
|
||||
renderer: &mut Renderer,
|
||||
scene: &mut Scene3d,
|
||||
vr: &mut VoxelRenderer,
|
||||
meshes: &[&Renderer3d],
|
||||
camera: &llimphi_3d::Camera3d,
|
||||
) -> Vec<u8> {
|
||||
let inter = hal.device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some("inter"),
|
||||
size: wgpu::Extent3d { width: TW, height: TH, depth_or_array_layers: 1 },
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: FMT,
|
||||
usage: wgpu::TextureUsages::STORAGE_BINDING
|
||||
| wgpu::TextureUsages::TEXTURE_BINDING
|
||||
| wgpu::TextureUsages::RENDER_ATTACHMENT
|
||||
| wgpu::TextureUsages::COPY_SRC,
|
||||
view_formats: &[],
|
||||
});
|
||||
let view = inter.create_view(&wgpu::TextureViewDescriptor::default());
|
||||
// Fondo cielo (sin niebla los misses del voxel descartan a este color base).
|
||||
renderer
|
||||
.render_to_view(hal, &vello::Scene::new(), &view, TW, TH, Color::from_rgba8(150, 184, 224, 255))
|
||||
.expect("base");
|
||||
let mut enc = hal
|
||||
.device
|
||||
.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("birth") });
|
||||
scene.render(&hal.device, &hal.queue, &mut enc, &view, (TW, TH), camera, Some(vr), meshes);
|
||||
hal.queue.submit(std::iter::once(enc.finish()));
|
||||
let _ = hal.device.poll(wgpu::PollType::wait_indefinitely());
|
||||
readback(hal, &inter)
|
||||
}
|
||||
|
||||
fn readback(hal: &Hal, target: &wgpu::Texture) -> Vec<u8> {
|
||||
let unpadded = (TW * 4) as usize;
|
||||
let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT as usize;
|
||||
let padded = unpadded.div_ceil(align) * align;
|
||||
let buf = hal.device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("readback"),
|
||||
size: (padded * TH as usize) as u64,
|
||||
usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
let mut enc = hal
|
||||
.device
|
||||
.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
|
||||
enc.copy_texture_to_buffer(
|
||||
wgpu::TexelCopyTextureInfo {
|
||||
texture: target,
|
||||
mip_level: 0,
|
||||
origin: wgpu::Origin3d::ZERO,
|
||||
aspect: wgpu::TextureAspect::All,
|
||||
},
|
||||
wgpu::TexelCopyBufferInfo {
|
||||
buffer: &buf,
|
||||
layout: wgpu::TexelCopyBufferLayout {
|
||||
offset: 0,
|
||||
bytes_per_row: Some(padded as u32),
|
||||
rows_per_image: Some(TH),
|
||||
},
|
||||
},
|
||||
wgpu::Extent3d { width: TW, height: TH, depth_or_array_layers: 1 },
|
||||
);
|
||||
hal.queue.submit(std::iter::once(enc.finish()));
|
||||
let slice = buf.slice(..);
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
slice.map_async(wgpu::MapMode::Read, move |r| {
|
||||
let _ = tx.send(r);
|
||||
});
|
||||
let _ = hal.device.poll(wgpu::PollType::wait_indefinitely());
|
||||
rx.recv().unwrap().unwrap();
|
||||
let data = slice.get_mapped_range();
|
||||
let mut pixels = Vec::with_capacity((TW * TH * 4) as usize);
|
||||
for row in 0..TH as usize {
|
||||
let s = row * padded;
|
||||
pixels.extend_from_slice(&data[s..s + unpadded]);
|
||||
}
|
||||
drop(data);
|
||||
buf.unmap();
|
||||
pixels
|
||||
}
|
||||
|
||||
fn write_png(pixels: &[u8], w: u32, h: u32, path: &str) {
|
||||
let file = File::create(path).expect("png");
|
||||
let mut enc = png::Encoder::new(BufWriter::new(file), w, h);
|
||||
enc.set_color(png::ColorType::Rgba);
|
||||
enc.set_depth(png::BitDepth::Eight);
|
||||
let mut wtr = enc.write_header().unwrap();
|
||||
wtr.write_image_data(pixels).unwrap();
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
//! Demo headless del **creador de mundos**: rinde la receta del **desierto**
|
||||
//! ([`WorldRecipe::desert`]) — llano de arena, pocas montañas, pocos ríos, cactus.
|
||||
//! Es el mundo de apertura del corto.
|
||||
//!
|
||||
//! `cargo run -p llimphi-voxel --example desert_demo --release -- [dim_xz] [seed]`
|
||||
//! → `/tmp/desert.png`
|
||||
|
||||
use std::fs::File;
|
||||
use std::io::BufWriter;
|
||||
|
||||
use llimphi_3d::glam::Vec3;
|
||||
use llimphi_3d::{Atmosphere, Camera3d, Scene3d, VoxelRenderer};
|
||||
use llimphi_hal::{wgpu, Hal};
|
||||
use llimphi_raster::peniko::Color;
|
||||
use llimphi_raster::{vello, Renderer};
|
||||
use llimphi_voxel::WorldRecipe;
|
||||
|
||||
const W: u32 = 960;
|
||||
const H: u32 = 540;
|
||||
const FMT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
|
||||
|
||||
fn main() {
|
||||
let dim_xz: u32 = std::env::args().nth(1).and_then(|s| s.parse().ok()).unwrap_or(160);
|
||||
let seed: u32 = std::env::args().nth(2).and_then(|s| s.parse().ok()).unwrap_or(7);
|
||||
let dy: u32 = (dim_xz * 4 / 10).max(48);
|
||||
let dim = [dim_xz, dy, dim_xz];
|
||||
|
||||
let hal = pollster::block_on(Hal::new(None)).expect("hal");
|
||||
let mut renderer = Renderer::new(&hal).expect("renderer");
|
||||
|
||||
let recipe = WorldRecipe::desert(seed);
|
||||
let grid = recipe.generate(dim);
|
||||
|
||||
let mut vr = VoxelRenderer::new(&hal.device, &hal.queue, FMT, &grid);
|
||||
vr.sun_dir = [0.62, 0.42, 0.28]; // sol más bajo → relieve/sombras de las dunas y cactus
|
||||
vr.atmosphere = Atmosphere {
|
||||
sky_zenith: [92, 146, 208],
|
||||
sky_horizon: [228, 206, 162], // horizonte arenoso/caluroso
|
||||
fog_density: 0.22 / dim_xz as f32, // niebla suave: no lavar el llano
|
||||
};
|
||||
|
||||
// Cámara baja, en 3/4, para leer el llano + los cactus recortados contra el cielo.
|
||||
let camera = Camera3d::orbit(
|
||||
Vec3::new(0.0, dy as f32 * -0.18, 0.0),
|
||||
38_f32.to_radians(),
|
||||
14_f32.to_radians(),
|
||||
dim_xz as f32 * 1.5,
|
||||
);
|
||||
|
||||
let mut scene = Scene3d::new();
|
||||
let pixels = render(&hal, &mut renderer, &mut scene, &mut vr, &camera);
|
||||
write_png(&pixels, "/tmp/desert.png");
|
||||
eprintln!("escrito /tmp/desert.png (desierto {dim_xz}³ seed {seed})");
|
||||
}
|
||||
|
||||
fn render(
|
||||
hal: &Hal,
|
||||
renderer: &mut Renderer,
|
||||
scene: &mut Scene3d,
|
||||
vr: &mut VoxelRenderer,
|
||||
camera: &Camera3d,
|
||||
) -> Vec<u8> {
|
||||
let inter = hal.device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some("inter"),
|
||||
size: wgpu::Extent3d { width: W, height: H, depth_or_array_layers: 1 },
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: FMT,
|
||||
usage: wgpu::TextureUsages::STORAGE_BINDING
|
||||
| wgpu::TextureUsages::TEXTURE_BINDING
|
||||
| wgpu::TextureUsages::RENDER_ATTACHMENT
|
||||
| wgpu::TextureUsages::COPY_SRC,
|
||||
view_formats: &[],
|
||||
});
|
||||
let view = inter.create_view(&wgpu::TextureViewDescriptor::default());
|
||||
renderer
|
||||
.render_to_view(hal, &vello::Scene::new(), &view, W, H, Color::from_rgba8(0, 0, 0, 255))
|
||||
.expect("base");
|
||||
let mut enc = hal
|
||||
.device
|
||||
.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("desert") });
|
||||
scene.render(&hal.device, &hal.queue, &mut enc, &view, (W, H), camera, Some(vr), &[]);
|
||||
hal.queue.submit(std::iter::once(enc.finish()));
|
||||
let _ = hal.device.poll(wgpu::PollType::wait_indefinitely());
|
||||
readback(hal, &inter)
|
||||
}
|
||||
|
||||
fn readback(hal: &Hal, target: &wgpu::Texture) -> Vec<u8> {
|
||||
let unpadded = (W * 4) as usize;
|
||||
let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT as usize;
|
||||
let padded = unpadded.div_ceil(align) * align;
|
||||
let buf = hal.device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("readback"),
|
||||
size: (padded * H as usize) as u64,
|
||||
usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
let mut enc = hal
|
||||
.device
|
||||
.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
|
||||
enc.copy_texture_to_buffer(
|
||||
wgpu::TexelCopyTextureInfo {
|
||||
texture: target,
|
||||
mip_level: 0,
|
||||
origin: wgpu::Origin3d::ZERO,
|
||||
aspect: wgpu::TextureAspect::All,
|
||||
},
|
||||
wgpu::TexelCopyBufferInfo {
|
||||
buffer: &buf,
|
||||
layout: wgpu::TexelCopyBufferLayout {
|
||||
offset: 0,
|
||||
bytes_per_row: Some(padded as u32),
|
||||
rows_per_image: Some(H),
|
||||
},
|
||||
},
|
||||
wgpu::Extent3d { width: W, height: H, depth_or_array_layers: 1 },
|
||||
);
|
||||
hal.queue.submit(std::iter::once(enc.finish()));
|
||||
let slice = buf.slice(..);
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
slice.map_async(wgpu::MapMode::Read, move |r| {
|
||||
let _ = tx.send(r);
|
||||
});
|
||||
let _ = hal.device.poll(wgpu::PollType::wait_indefinitely());
|
||||
rx.recv().unwrap().unwrap();
|
||||
let data = slice.get_mapped_range();
|
||||
let mut pixels = Vec::with_capacity((W * H * 4) as usize);
|
||||
for row in 0..H as usize {
|
||||
let s = row * padded;
|
||||
pixels.extend_from_slice(&data[s..s + unpadded]);
|
||||
}
|
||||
drop(data);
|
||||
buf.unmap();
|
||||
pixels
|
||||
}
|
||||
|
||||
fn write_png(pixels: &[u8], path: &str) {
|
||||
let file = File::create(path).expect("png");
|
||||
let mut enc = png::Encoder::new(BufWriter::new(file), W, H);
|
||||
enc.set_color(png::ColorType::Rgba);
|
||||
enc.set_depth(png::BitDepth::Eight);
|
||||
let mut wtr = enc.write_header().unwrap();
|
||||
wtr.write_image_data(pixels).unwrap();
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
//! Demo headless del **objeto potencial**: el huevo en tres momentos — intacto,
|
||||
//! rajándose, y abierto con el **bebé recién nacido** al lado. Es el corazón de la
|
||||
//! apertura del corto (el huevo nace en el desierto).
|
||||
//!
|
||||
//! `cargo run -p llimphi-voxel --example egg_demo --release` → `/tmp/egg.png`
|
||||
|
||||
use std::fs::File;
|
||||
use std::io::BufWriter;
|
||||
|
||||
use llimphi_3d::glam::Vec3;
|
||||
use llimphi_3d::{Atmosphere, Camera3d, Renderer3d, Scene3d, VoxelGrid, VoxelRenderer};
|
||||
use llimphi_hal::{wgpu, Hal};
|
||||
use llimphi_raster::peniko::Color;
|
||||
use llimphi_raster::{vello, Renderer};
|
||||
use llimphi_voxel::{Age, Egg, Hatchling, Material};
|
||||
|
||||
const W: u32 = 960;
|
||||
const H: u32 = 540;
|
||||
const FMT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
|
||||
|
||||
fn main() {
|
||||
let hal = pollster::block_on(Hal::new(None)).expect("hal");
|
||||
let mut renderer = Renderer::new(&hal).expect("renderer");
|
||||
|
||||
// Piso plano de arena.
|
||||
let dim = [44u32, 16, 20];
|
||||
let mut grid = VoxelGrid::new(dim);
|
||||
for z in 0..dim[2] {
|
||||
for x in 0..dim[0] {
|
||||
grid.set(x, 0, z, Material::Sand.color());
|
||||
grid.set(x, 1, z, Material::Sand.color());
|
||||
}
|
||||
}
|
||||
grid.reset_dirty();
|
||||
let feet_y = 2.0 - dim[1] as f32 / 2.0; // y del suelo en mundo
|
||||
|
||||
let mut vr = VoxelRenderer::new(&hal.device, &hal.queue, FMT, &grid);
|
||||
vr.sun_dir = [0.5, 0.7, 0.4];
|
||||
vr.atmosphere = Atmosphere { sky_zenith: [96, 150, 210], sky_horizon: [226, 208, 168], fog_density: 0.0 };
|
||||
|
||||
let mut meshes: Vec<Renderer3d> = Vec::new();
|
||||
let push = |meshes: &mut Vec<Renderer3d>, hal: &Hal, v: &[_], i: &[u16], model| {
|
||||
let mut r = Renderer3d::new(&hal.device, FMT);
|
||||
r.set_geometry(&hal.device, v, i);
|
||||
r.set_model(model);
|
||||
meshes.push(r);
|
||||
};
|
||||
|
||||
// Tres huevos en fila: intacto, rajándose, abierto.
|
||||
let xs = [-11.0_f32, 0.0, 11.0];
|
||||
let hatches = [0.0_f32, 0.55, 1.0];
|
||||
for (x, h) in xs.iter().zip(hatches) {
|
||||
let mut egg = Egg::new(Vec3::new(*x, feet_y, 0.0), 1.6, Hatchling::human(Age::Baby));
|
||||
egg.hatch = h;
|
||||
let (v, i) = egg.mesh();
|
||||
push(&mut meshes, &hal, &v, &i, egg.model());
|
||||
// En el abierto, el bebé recién nacido sale, un paso al frente.
|
||||
if egg.is_open() {
|
||||
let mut baby = egg.newborn();
|
||||
baby.pos = egg.pos + Vec3::new(0.0, 0.0, 1.4); // un paso hacia la cámara
|
||||
baby.facing = std::f32::consts::PI;
|
||||
let (bv, bi) = baby.mesh();
|
||||
push(&mut meshes, &hal, &bv, &bi, baby.model());
|
||||
}
|
||||
}
|
||||
|
||||
let camera = Camera3d::orbit(Vec3::new(0.0, feet_y + 1.0, 0.0), 0_f32.to_radians(), 10_f32.to_radians(), 19.0);
|
||||
let refs: Vec<&Renderer3d> = meshes.iter().collect();
|
||||
let mut scene = Scene3d::new();
|
||||
let pixels = render(&hal, &mut renderer, &mut scene, &mut vr, &refs, &camera);
|
||||
write_png(&pixels, "/tmp/egg.png");
|
||||
eprintln!("escrito /tmp/egg.png (intacto · rajándose · abierto + bebé)");
|
||||
}
|
||||
|
||||
fn render(
|
||||
hal: &Hal,
|
||||
renderer: &mut Renderer,
|
||||
scene: &mut Scene3d,
|
||||
vr: &mut VoxelRenderer,
|
||||
meshes: &[&Renderer3d],
|
||||
camera: &Camera3d,
|
||||
) -> Vec<u8> {
|
||||
let inter = hal.device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some("inter"),
|
||||
size: wgpu::Extent3d { width: W, height: H, depth_or_array_layers: 1 },
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: FMT,
|
||||
usage: wgpu::TextureUsages::STORAGE_BINDING
|
||||
| wgpu::TextureUsages::TEXTURE_BINDING
|
||||
| wgpu::TextureUsages::RENDER_ATTACHMENT
|
||||
| wgpu::TextureUsages::COPY_SRC,
|
||||
view_formats: &[],
|
||||
});
|
||||
let view = inter.create_view(&wgpu::TextureViewDescriptor::default());
|
||||
renderer
|
||||
.render_to_view(hal, &vello::Scene::new(), &view, W, H, Color::from_rgba8(0, 0, 0, 255))
|
||||
.expect("base");
|
||||
let mut enc = hal
|
||||
.device
|
||||
.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("egg") });
|
||||
scene.render(&hal.device, &hal.queue, &mut enc, &view, (W, H), camera, Some(vr), meshes);
|
||||
hal.queue.submit(std::iter::once(enc.finish()));
|
||||
let _ = hal.device.poll(wgpu::PollType::wait_indefinitely());
|
||||
readback(hal, &inter)
|
||||
}
|
||||
|
||||
fn readback(hal: &Hal, target: &wgpu::Texture) -> Vec<u8> {
|
||||
let unpadded = (W * 4) as usize;
|
||||
let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT as usize;
|
||||
let padded = unpadded.div_ceil(align) * align;
|
||||
let buf = hal.device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("readback"),
|
||||
size: (padded * H as usize) as u64,
|
||||
usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
let mut enc = hal
|
||||
.device
|
||||
.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
|
||||
enc.copy_texture_to_buffer(
|
||||
wgpu::TexelCopyTextureInfo {
|
||||
texture: target,
|
||||
mip_level: 0,
|
||||
origin: wgpu::Origin3d::ZERO,
|
||||
aspect: wgpu::TextureAspect::All,
|
||||
},
|
||||
wgpu::TexelCopyBufferInfo {
|
||||
buffer: &buf,
|
||||
layout: wgpu::TexelCopyBufferLayout {
|
||||
offset: 0,
|
||||
bytes_per_row: Some(padded as u32),
|
||||
rows_per_image: Some(H),
|
||||
},
|
||||
},
|
||||
wgpu::Extent3d { width: W, height: H, depth_or_array_layers: 1 },
|
||||
);
|
||||
hal.queue.submit(std::iter::once(enc.finish()));
|
||||
let slice = buf.slice(..);
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
slice.map_async(wgpu::MapMode::Read, move |r| {
|
||||
let _ = tx.send(r);
|
||||
});
|
||||
let _ = hal.device.poll(wgpu::PollType::wait_indefinitely());
|
||||
rx.recv().unwrap().unwrap();
|
||||
let data = slice.get_mapped_range();
|
||||
let mut pixels = Vec::with_capacity((W * H * 4) as usize);
|
||||
for row in 0..H as usize {
|
||||
let s = row * padded;
|
||||
pixels.extend_from_slice(&data[s..s + unpadded]);
|
||||
}
|
||||
drop(data);
|
||||
buf.unmap();
|
||||
pixels
|
||||
}
|
||||
|
||||
fn write_png(pixels: &[u8], path: &str) {
|
||||
let file = File::create(path).expect("png");
|
||||
let mut enc = png::Encoder::new(BufWriter::new(file), W, H);
|
||||
enc.set_color(png::ColorType::Rgba);
|
||||
enc.set_depth(png::BitDepth::Eight);
|
||||
let mut wtr = enc.write_header().unwrap();
|
||||
wtr.write_image_data(pixels).unwrap();
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
//! Demo headless de la mecánica núcleo de juego voxel: **mirar → romper**.
|
||||
//!
|
||||
//! Renderiza el terreno, tira un [`raycast`] desde la cámara hacia el centro de
|
||||
//! la vista hasta el primer voxel sólido, y **cava un cráter** (vacía los voxels
|
||||
//! en un radio del impacto). Cada edición sube SÓLO su sub-caja vía
|
||||
//! `VoxelRenderer::sync` (no re-sube el mundo). Vuelca PNG antes/después e
|
||||
//! imprime los bytes subidos vs el grid completo.
|
||||
//!
|
||||
//! `cargo run -p llimphi-voxel --example raycast_edit --release -- [dim_xz] [seed]`
|
||||
|
||||
use std::fs::File;
|
||||
use std::io::BufWriter;
|
||||
|
||||
use llimphi_3d::glam::Vec3;
|
||||
use llimphi_3d::{Atmosphere, Camera3d, VoxelRenderer};
|
||||
use llimphi_hal::{wgpu, Hal};
|
||||
use llimphi_raster::peniko::Color;
|
||||
use llimphi_raster::{vello, Renderer};
|
||||
use llimphi_voxel::{raycast, terrain};
|
||||
|
||||
const W: u32 = 880;
|
||||
const H: u32 = 560;
|
||||
const FMT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
|
||||
|
||||
fn main() {
|
||||
let dim_xz: u32 = std::env::args().nth(1).and_then(|s| s.parse().ok()).unwrap_or(160);
|
||||
let seed: u32 = std::env::args().nth(2).and_then(|s| s.parse().ok()).unwrap_or(7);
|
||||
let dy = (dim_xz * 4 / 10).max(48);
|
||||
let dim = [dim_xz, dy, dim_xz];
|
||||
|
||||
let hal = pollster::block_on(Hal::new(None)).expect("hal");
|
||||
let mut renderer = Renderer::new(&hal).expect("renderer");
|
||||
|
||||
let mut grid = terrain(dim, seed);
|
||||
let mut vr = VoxelRenderer::new(&hal.device, &hal.queue, FMT, &grid);
|
||||
vr.sun_dir = [0.55, 0.6, 0.3];
|
||||
vr.atmosphere = Atmosphere {
|
||||
sky_zenith: [64, 118, 196],
|
||||
sky_horizon: [202, 218, 236],
|
||||
fog_density: 0.5 / dim_xz as f32,
|
||||
};
|
||||
|
||||
let inter = make_target(&hal);
|
||||
let inter_view = inter.create_view(&wgpu::TextureViewDescriptor::default());
|
||||
|
||||
let camera = Camera3d::orbit(
|
||||
Vec3::new(0.0, dy as f32 * 0.30, 0.0),
|
||||
45_f32.to_radians(),
|
||||
14_f32.to_radians(),
|
||||
dim_xz as f32 * 0.78,
|
||||
);
|
||||
|
||||
// (antes)
|
||||
draw(&hal, &mut renderer, &mut vr, &inter, &inter_view, &camera, "/tmp/edit_before.png");
|
||||
|
||||
// Rayo desde la cámara hacia el centro de la vista. La grilla está centrada
|
||||
// en el origen → origen del rayo en grilla = eye_mundo + dim/2.
|
||||
let dimv = Vec3::new(dim[0] as f32, dim[1] as f32, dim[2] as f32);
|
||||
let ro = camera.eye + dimv * 0.5;
|
||||
let rd = (camera.target - camera.eye).normalize();
|
||||
match raycast(&grid, [ro.x, ro.y, ro.z], [rd.x, rd.y, rd.z], dim_xz as f32 * 3.0) {
|
||||
Some(hit) => {
|
||||
eprintln!("impacto en {:?} (cara {:?}, dist {:.1})", hit.cell, hit.normal, hit.dist);
|
||||
// Cavar un cráter esférico alrededor del impacto.
|
||||
let r = 12i32;
|
||||
let [cx, cy, cz] = hit.cell;
|
||||
for dz in -r..=r {
|
||||
for dyy in -r..=r {
|
||||
for dx in -r..=r {
|
||||
if dx * dx + dyy * dyy + dz * dz <= r * r {
|
||||
let (x, y, z) = (cx + dx, cy + dyy, cz + dz);
|
||||
if x >= 0 && y >= 0 && z >= 0 {
|
||||
grid.clear(x as u32, y as u32, z as u32);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let uploaded = vr.sync(&hal.queue, &mut grid);
|
||||
let full = dim[0] * dim[1] * dim[2] * 4;
|
||||
eprintln!(
|
||||
"cráter r={r} → sync subió {} KiB ({:.3}% del grid completo de {} KiB) — incremental",
|
||||
uploaded / 1024,
|
||||
uploaded as f32 / full as f32 * 100.0,
|
||||
full / 1024,
|
||||
);
|
||||
}
|
||||
None => eprintln!("el rayo no pegó terreno (ajustá cámara)"),
|
||||
}
|
||||
|
||||
// (después)
|
||||
draw(&hal, &mut renderer, &mut vr, &inter, &inter_view, &camera, "/tmp/edit_after.png");
|
||||
eprintln!("escrito /tmp/edit_before.png y /tmp/edit_after.png ({W}x{H})");
|
||||
}
|
||||
|
||||
fn make_target(hal: &Hal) -> wgpu::Texture {
|
||||
hal.device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some("inter"),
|
||||
size: wgpu::Extent3d { width: W, height: H, depth_or_array_layers: 1 },
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: FMT,
|
||||
usage: wgpu::TextureUsages::STORAGE_BINDING
|
||||
| wgpu::TextureUsages::TEXTURE_BINDING
|
||||
| wgpu::TextureUsages::RENDER_ATTACHMENT
|
||||
| wgpu::TextureUsages::COPY_SRC,
|
||||
view_formats: &[],
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn draw(
|
||||
hal: &Hal,
|
||||
renderer: &mut Renderer,
|
||||
vr: &mut VoxelRenderer,
|
||||
target: &wgpu::Texture,
|
||||
target_view: &wgpu::TextureView,
|
||||
camera: &Camera3d,
|
||||
out: &str,
|
||||
) {
|
||||
renderer
|
||||
.render_to_view(hal, &vello::Scene::new(), target_view, W, H, Color::from_rgba8(0, 0, 0, 255))
|
||||
.expect("base");
|
||||
let mut enc = hal
|
||||
.device
|
||||
.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("voxel") });
|
||||
vr.render(&hal.device, &hal.queue, &mut enc, target_view, (W, H), camera);
|
||||
hal.queue.submit(std::iter::once(enc.finish()));
|
||||
let _ = hal.device.poll(wgpu::PollType::wait_indefinitely());
|
||||
write_png(hal, target, out);
|
||||
}
|
||||
|
||||
fn write_png(hal: &Hal, target: &wgpu::Texture, path: &str) {
|
||||
let unpadded = (W * 4) as usize;
|
||||
let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT as usize;
|
||||
let padded = unpadded.div_ceil(align) * align;
|
||||
let buf = hal.device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("readback"),
|
||||
size: (padded * H as usize) as u64,
|
||||
usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
let mut enc = hal
|
||||
.device
|
||||
.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
|
||||
enc.copy_texture_to_buffer(
|
||||
wgpu::TexelCopyTextureInfo {
|
||||
texture: target,
|
||||
mip_level: 0,
|
||||
origin: wgpu::Origin3d::ZERO,
|
||||
aspect: wgpu::TextureAspect::All,
|
||||
},
|
||||
wgpu::TexelCopyBufferInfo {
|
||||
buffer: &buf,
|
||||
layout: wgpu::TexelCopyBufferLayout {
|
||||
offset: 0,
|
||||
bytes_per_row: Some(padded as u32),
|
||||
rows_per_image: Some(H),
|
||||
},
|
||||
},
|
||||
wgpu::Extent3d { width: W, height: H, depth_or_array_layers: 1 },
|
||||
);
|
||||
hal.queue.submit(std::iter::once(enc.finish()));
|
||||
let slice = buf.slice(..);
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
slice.map_async(wgpu::MapMode::Read, move |r| {
|
||||
let _ = tx.send(r);
|
||||
});
|
||||
let _ = hal.device.poll(wgpu::PollType::wait_indefinitely());
|
||||
rx.recv().unwrap().unwrap();
|
||||
let data = slice.get_mapped_range();
|
||||
let mut pixels = Vec::with_capacity((W * H * 4) as usize);
|
||||
for row in 0..H as usize {
|
||||
let s = row * padded;
|
||||
pixels.extend_from_slice(&data[s..s + unpadded]);
|
||||
}
|
||||
drop(data);
|
||||
buf.unmap();
|
||||
let file = File::create(path).expect("png");
|
||||
let mut enc = png::Encoder::new(BufWriter::new(file), W, H);
|
||||
enc.set_color(png::ColorType::Rgba);
|
||||
enc.set_depth(png::BitDepth::Eight);
|
||||
let mut w = enc.write_header().unwrap();
|
||||
w.write_image_data(&pixels).unwrap();
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
//! Demo headless de M6 (primera rebanada): **world-gen procedural + atmósfera**.
|
||||
//!
|
||||
//! Genera un paisaje voxel grande por ruido fractal ([`llimphi_3d::terrain`]) y
|
||||
//! lo ray-marchea con **cielo gradiente + niebla por distancia** ([`Atmosphere`])
|
||||
//! — lo que hace legible el borde lejano de un mundo grande (sin niebla, el
|
||||
//! horizonte del terreno se ve como un muro recortado). Imprime además el ahorro
|
||||
//! de memoria del brick pool sparse: un mundo es casi todo aire, así que el pool
|
||||
//! ocupa una fracción del grid denso.
|
||||
//!
|
||||
//! `cargo run -p llimphi-3d --example terrain_demo --release -- [dim_xz] [seed]`
|
||||
//! → escribe /tmp/m6_terrain_{a,b,c}.png (tres ángulos de órbita).
|
||||
|
||||
use std::fs::File;
|
||||
use std::io::BufWriter;
|
||||
|
||||
use llimphi_3d::glam::Vec3;
|
||||
use llimphi_3d::{Atmosphere, Camera3d, VoxelRenderer};
|
||||
use llimphi_hal::{wgpu, Hal};
|
||||
use llimphi_voxel::terrain;
|
||||
use llimphi_raster::peniko::Color;
|
||||
use llimphi_raster::{vello, Renderer};
|
||||
|
||||
const W: u32 = 960;
|
||||
const H: u32 = 540;
|
||||
const FMT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
|
||||
|
||||
fn main() {
|
||||
let dim_xz: u32 = std::env::args().nth(1).and_then(|s| s.parse().ok()).unwrap_or(192);
|
||||
let seed: u32 = std::env::args().nth(2).and_then(|s| s.parse().ok()).unwrap_or(1337);
|
||||
let dy: u32 = (dim_xz * 4 / 10).max(48); // mundo "ancho y bajo": continente, no torre.
|
||||
let dim = [dim_xz, dy, dim_xz];
|
||||
|
||||
let hal = pollster::block_on(Hal::new(None)).expect("hal");
|
||||
let mut renderer = Renderer::new(&hal).expect("renderer");
|
||||
|
||||
let grid = terrain(dim, seed);
|
||||
let mut vr = VoxelRenderer::new(&hal.device, &hal.queue, FMT, &grid);
|
||||
let (used, total) = vr.brick_usage();
|
||||
let (pool, dense) = vr.memory_bytes();
|
||||
eprintln!(
|
||||
"terreno {}×{}×{} (seed {seed}) — brick pool {used}/{total} bricks ({:.1}%) → {} KiB vs denso {} KiB ({:.1}× menos)",
|
||||
dim[0], dim[1], dim[2],
|
||||
used as f32 / total as f32 * 100.0,
|
||||
pool / 1024,
|
||||
dense / 1024,
|
||||
dense as f32 / pool.max(1) as f32,
|
||||
);
|
||||
|
||||
// Atmósfera diurna: sol bajo (luz rasante = relieve marcado), niebla suave
|
||||
// escalada al tamaño del mundo (lo lejano desvanece, lo cercano queda nítido).
|
||||
vr.sun_dir = [0.55, 0.55, 0.35];
|
||||
vr.atmosphere = Atmosphere {
|
||||
sky_zenith: [64, 118, 196],
|
||||
sky_horizon: [200, 216, 234],
|
||||
fog_density: 0.5 / dim_xz as f32,
|
||||
};
|
||||
|
||||
let inter = hal.device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some("inter"),
|
||||
size: wgpu::Extent3d { width: W, height: H, depth_or_array_layers: 1 },
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: FMT,
|
||||
usage: wgpu::TextureUsages::STORAGE_BINDING
|
||||
| wgpu::TextureUsages::TEXTURE_BINDING
|
||||
| wgpu::TextureUsages::RENDER_ATTACHMENT
|
||||
| wgpu::TextureUsages::COPY_SRC,
|
||||
view_formats: &[],
|
||||
});
|
||||
let inter_view = inter.create_view(&wgpu::TextureViewDescriptor::default());
|
||||
|
||||
let d = dim_xz as f32;
|
||||
for (tag, yaw) in [("a", 35.0_f32), ("b", 120.0), ("c", 230.0)] {
|
||||
// (1) Fondo vello (lo tapa la atmósfera del pase voxel, pero dejamos el
|
||||
// mismo orden que el runtime: [vello base] → [GPU 3D]).
|
||||
let base = vello::Scene::new();
|
||||
renderer
|
||||
.render_to_view(&hal, &base, &inter_view, W, H, Color::from_rgba8(0, 0, 0, 255))
|
||||
.expect("render base");
|
||||
|
||||
// (2) Pase voxel. Órbita mirando un poco por encima del centro para que
|
||||
// entre cielo en cuadro; pitch bajo = vista de paisaje.
|
||||
let camera = Camera3d::orbit(
|
||||
Vec3::new(0.0, dy as f32 * 0.12, 0.0),
|
||||
yaw.to_radians(),
|
||||
22_f32.to_radians(),
|
||||
d * 1.45,
|
||||
);
|
||||
let mut enc = hal
|
||||
.device
|
||||
.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("voxel-pass") });
|
||||
vr.render(&hal.device, &hal.queue, &mut enc, &inter_view, (W, H), &camera);
|
||||
hal.queue.submit(std::iter::once(enc.finish()));
|
||||
let _ = hal.device.poll(wgpu::PollType::wait_indefinitely());
|
||||
|
||||
let out = format!("/tmp/m6_terrain_{tag}.png");
|
||||
write_png(&hal, &inter, &out);
|
||||
eprintln!("escrito {out} ({W}x{H}, yaw={yaw}°)");
|
||||
}
|
||||
}
|
||||
|
||||
fn write_png(hal: &Hal, target: &wgpu::Texture, path: &str) {
|
||||
let unpadded = (W * 4) as usize;
|
||||
let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT as usize;
|
||||
let padded = unpadded.div_ceil(align) * align;
|
||||
let buf = hal.device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("readback"),
|
||||
size: (padded * H as usize) as u64,
|
||||
usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
let mut enc = hal
|
||||
.device
|
||||
.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
|
||||
enc.copy_texture_to_buffer(
|
||||
wgpu::TexelCopyTextureInfo {
|
||||
texture: target,
|
||||
mip_level: 0,
|
||||
origin: wgpu::Origin3d::ZERO,
|
||||
aspect: wgpu::TextureAspect::All,
|
||||
},
|
||||
wgpu::TexelCopyBufferInfo {
|
||||
buffer: &buf,
|
||||
layout: wgpu::TexelCopyBufferLayout {
|
||||
offset: 0,
|
||||
bytes_per_row: Some(padded as u32),
|
||||
rows_per_image: Some(H),
|
||||
},
|
||||
},
|
||||
wgpu::Extent3d { width: W, height: H, depth_or_array_layers: 1 },
|
||||
);
|
||||
hal.queue.submit(std::iter::once(enc.finish()));
|
||||
let slice = buf.slice(..);
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
slice.map_async(wgpu::MapMode::Read, move |r| {
|
||||
let _ = tx.send(r);
|
||||
});
|
||||
let _ = hal.device.poll(wgpu::PollType::wait_indefinitely());
|
||||
rx.recv().unwrap().unwrap();
|
||||
let data = slice.get_mapped_range();
|
||||
let mut pixels = Vec::with_capacity((W * H * 4) as usize);
|
||||
for row in 0..H as usize {
|
||||
let s = row * padded;
|
||||
pixels.extend_from_slice(&data[s..s + unpadded]);
|
||||
}
|
||||
drop(data);
|
||||
buf.unmap();
|
||||
let file = File::create(path).expect("png");
|
||||
let mut enc = png::Encoder::new(BufWriter::new(file), W, H);
|
||||
enc.set_color(png::ColorType::Rgba);
|
||||
enc.set_depth(png::BitDepth::Eight);
|
||||
let mut w = enc.write_header().unwrap();
|
||||
w.write_image_data(&pixels).unwrap();
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
//! Demo headless de M6 — **vuelo por dentro del mundo** (cámara libre).
|
||||
//!
|
||||
//! Complementa a `terrain_demo` (órbita desde afuera): acá la [`Camera3d::fly`]
|
||||
//! recorre el paisaje procedural a baja altura, siguiendo el relieve
|
||||
//! ([`VoxelGrid::height_at`]) para no meterse dentro de la roca, con la misma
|
||||
//! atmósfera (cielo + niebla). Es el plano "showreel" del motor y ejercita el
|
||||
//! ray-march DDA **desde adentro** de la grilla (no sólo orbitándola).
|
||||
//!
|
||||
//! `cargo run -p llimphi-3d --example terrain_flythrough --release -- [dim_xz] [seed] [frames]`
|
||||
//! → escribe /tmp/m6_fly_##.png
|
||||
|
||||
use std::fs::File;
|
||||
use std::io::BufWriter;
|
||||
|
||||
use llimphi_3d::glam::Vec3;
|
||||
use llimphi_3d::{Atmosphere, Camera3d, VoxelRenderer};
|
||||
use llimphi_hal::{wgpu, Hal};
|
||||
use llimphi_voxel::terrain;
|
||||
use llimphi_raster::peniko::Color;
|
||||
use llimphi_raster::{vello, Renderer};
|
||||
|
||||
const W: u32 = 960;
|
||||
const H: u32 = 540;
|
||||
const FMT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
|
||||
|
||||
fn main() {
|
||||
let dim_xz: u32 = std::env::args().nth(1).and_then(|s| s.parse().ok()).unwrap_or(192);
|
||||
let seed: u32 = std::env::args().nth(2).and_then(|s| s.parse().ok()).unwrap_or(1337);
|
||||
let frames: u32 = std::env::args().nth(3).and_then(|s| s.parse().ok()).unwrap_or(6);
|
||||
let dy: u32 = (dim_xz * 4 / 10).max(48);
|
||||
let dim = [dim_xz, dy, dim_xz];
|
||||
|
||||
let hal = pollster::block_on(Hal::new(None)).expect("hal");
|
||||
let mut renderer = Renderer::new(&hal).expect("renderer");
|
||||
|
||||
let grid = terrain(dim, seed);
|
||||
let mut vr = VoxelRenderer::new(&hal.device, &hal.queue, FMT, &grid);
|
||||
vr.sun_dir = [0.55, 0.5, 0.32];
|
||||
vr.atmosphere = Atmosphere {
|
||||
sky_zenith: [64, 118, 196],
|
||||
sky_horizon: [202, 218, 236],
|
||||
fog_density: 0.9 / dim_xz as f32, // un poco más densa: estamos adentro
|
||||
};
|
||||
|
||||
let inter = hal.device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some("inter"),
|
||||
size: wgpu::Extent3d { width: W, height: H, depth_or_array_layers: 1 },
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: FMT,
|
||||
usage: wgpu::TextureUsages::STORAGE_BINDING
|
||||
| wgpu::TextureUsages::TEXTURE_BINDING
|
||||
| wgpu::TextureUsages::RENDER_ATTACHMENT
|
||||
| wgpu::TextureUsages::COPY_SRC,
|
||||
view_formats: &[],
|
||||
});
|
||||
let inter_view = inter.create_view(&wgpu::TextureViewDescriptor::default());
|
||||
|
||||
let (dxf, dyf, dzf) = (dim[0] as f32, dim[1] as f32, dim[2] as f32);
|
||||
let half_z = dzf * 0.5;
|
||||
|
||||
// Altura del terreno (en y de MUNDO) sobre una ventana hacia adelante, para
|
||||
// que el vuelo suba por encima de los picos que vienen.
|
||||
let ground_world_y = |gx: f32, gz: f32| -> f32 {
|
||||
let mut hmax = 0u32;
|
||||
let gxi = gx.clamp(0.0, dxf - 1.0) as u32;
|
||||
for dz in 0..16u32 {
|
||||
let gzi = (gz as i32 + dz as i32).clamp(0, dim[2] as i32 - 1) as u32;
|
||||
if let Some(h) = grid.height_at(gxi, gzi) {
|
||||
hmax = hmax.max(h);
|
||||
}
|
||||
}
|
||||
hmax as f32 - dyf * 0.5 // grid y → mundo y (grilla centrada en origen)
|
||||
};
|
||||
|
||||
for i in 0..frames {
|
||||
let t = if frames > 1 { i as f32 / (frames - 1) as f32 } else { 0.0 };
|
||||
// Avanza en +Z (yaw=0 mira +Z); curva suave en X.
|
||||
let pz = (-0.8 + 1.5 * t) * half_z;
|
||||
let px = (t * std::f32::consts::PI).sin() * dxf * 0.18;
|
||||
let gx = px + dxf * 0.5;
|
||||
let gz = pz + dzf * 0.5;
|
||||
let py = ground_world_y(gx, gz) + dyf * 0.16 + 6.0; // despejado sobre el relieve
|
||||
let yaw = (px / dxf) * -0.6; // mira hacia donde curva
|
||||
let camera = Camera3d::fly(Vec3::new(px, py, pz), yaw, -0.12);
|
||||
|
||||
let base = vello::Scene::new();
|
||||
renderer
|
||||
.render_to_view(&hal, &base, &inter_view, W, H, Color::from_rgba8(0, 0, 0, 255))
|
||||
.expect("render base");
|
||||
|
||||
let mut enc = hal
|
||||
.device
|
||||
.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("voxel-pass") });
|
||||
vr.render(&hal.device, &hal.queue, &mut enc, &inter_view, (W, H), &camera);
|
||||
hal.queue.submit(std::iter::once(enc.finish()));
|
||||
let _ = hal.device.poll(wgpu::PollType::wait_indefinitely());
|
||||
|
||||
let out = format!("/tmp/m6_fly_{i:02}.png");
|
||||
write_png(&hal, &inter, &out);
|
||||
eprintln!("escrito {out} (eye=[{px:.0},{py:.0},{pz:.0}], yaw={:.2})", yaw);
|
||||
}
|
||||
}
|
||||
|
||||
fn write_png(hal: &Hal, target: &wgpu::Texture, path: &str) {
|
||||
let unpadded = (W * 4) as usize;
|
||||
let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT as usize;
|
||||
let padded = unpadded.div_ceil(align) * align;
|
||||
let buf = hal.device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("readback"),
|
||||
size: (padded * H as usize) as u64,
|
||||
usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
let mut enc = hal
|
||||
.device
|
||||
.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
|
||||
enc.copy_texture_to_buffer(
|
||||
wgpu::TexelCopyTextureInfo {
|
||||
texture: target,
|
||||
mip_level: 0,
|
||||
origin: wgpu::Origin3d::ZERO,
|
||||
aspect: wgpu::TextureAspect::All,
|
||||
},
|
||||
wgpu::TexelCopyBufferInfo {
|
||||
buffer: &buf,
|
||||
layout: wgpu::TexelCopyBufferLayout {
|
||||
offset: 0,
|
||||
bytes_per_row: Some(padded as u32),
|
||||
rows_per_image: Some(H),
|
||||
},
|
||||
},
|
||||
wgpu::Extent3d { width: W, height: H, depth_or_array_layers: 1 },
|
||||
);
|
||||
hal.queue.submit(std::iter::once(enc.finish()));
|
||||
let slice = buf.slice(..);
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
slice.map_async(wgpu::MapMode::Read, move |r| {
|
||||
let _ = tx.send(r);
|
||||
});
|
||||
let _ = hal.device.poll(wgpu::PollType::wait_indefinitely());
|
||||
rx.recv().unwrap().unwrap();
|
||||
let data = slice.get_mapped_range();
|
||||
let mut pixels = Vec::with_capacity((W * H * 4) as usize);
|
||||
for row in 0..H as usize {
|
||||
let s = row * padded;
|
||||
pixels.extend_from_slice(&data[s..s + unpadded]);
|
||||
}
|
||||
drop(data);
|
||||
buf.unmap();
|
||||
let file = File::create(path).expect("png");
|
||||
let mut enc = png::Encoder::new(BufWriter::new(file), W, H);
|
||||
enc.set_color(png::ColorType::Rgba);
|
||||
enc.set_depth(png::BitDepth::Eight);
|
||||
let mut w = enc.write_header().unwrap();
|
||||
w.write_image_data(&pixels).unwrap();
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
//! Demo headless de M6 — **LOD del horizonte**: más allá de la ventana voxel fina,
|
||||
//! una **malla gruesa** del terreno circundante ([`lod_skirt`]) muestra colinas
|
||||
//! lejanas en vez de un muro de niebla. Voxel cerca / malla-LOD lejos, compuestos
|
||||
//! por el depth compartido de [`Scene3d`].
|
||||
//!
|
||||
//! Rinde dos PNG para el contraste:
|
||||
//! - `/tmp/m6_lod_off.png` — sólo voxels (el terreno se corta en el borde de la
|
||||
//! ventana; la niebla tapa el vacío = "muro").
|
||||
//! - `/tmp/m6_lod_on.png` — voxels + falda LOD (el horizonte sigue con relieve).
|
||||
//!
|
||||
//! `cargo run -p llimphi-voxel --example terrain_lod --release -- [dim_xz] [seed]`
|
||||
|
||||
use std::fs::File;
|
||||
use std::io::BufWriter;
|
||||
|
||||
use llimphi_3d::glam::Vec3;
|
||||
use llimphi_3d::{Atmosphere, Camera3d, Renderer3d, Scene3d, VoxelRenderer};
|
||||
use llimphi_hal::{wgpu, Hal};
|
||||
use llimphi_raster::peniko::Color;
|
||||
use llimphi_raster::{vello, Renderer};
|
||||
use llimphi_voxel::{lod_skirt, lod_skirt_pyramid, terrain, LodParams, LodRing};
|
||||
|
||||
const W: u32 = 960;
|
||||
const H: u32 = 540;
|
||||
const FMT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
|
||||
|
||||
fn main() {
|
||||
let dim_xz: u32 = std::env::args().nth(1).and_then(|s| s.parse().ok()).unwrap_or(128);
|
||||
let seed: u32 = std::env::args().nth(2).and_then(|s| s.parse().ok()).unwrap_or(1337);
|
||||
let dy: u32 = (dim_xz * 4 / 10).max(48);
|
||||
let dim = [dim_xz, dy, dim_xz];
|
||||
|
||||
let hal = pollster::block_on(Hal::new(None)).expect("hal");
|
||||
let mut renderer = Renderer::new(&hal).expect("renderer");
|
||||
|
||||
// Ventana voxel fina en mundo [0, dim_xz); su centro de mundo es (dim/2, dim/2)
|
||||
// y se renderiza centrada en el origen (rendered = local − dim/2).
|
||||
let grid = terrain(dim, seed);
|
||||
let sun = [0.5, 0.45, 0.32];
|
||||
let atmo = Atmosphere {
|
||||
sky_zenith: [70, 120, 196],
|
||||
sky_horizon: [200, 216, 234],
|
||||
fog_density: 1.1 / dim_xz as f32,
|
||||
};
|
||||
let mut vr = VoxelRenderer::new(&hal.device, &hal.queue, FMT, &grid);
|
||||
vr.sun_dir = sun;
|
||||
vr.atmosphere = atmo;
|
||||
|
||||
// Falda LOD alrededor: centro = (dim/2, dim/2) en mundo, hueco = la ventana.
|
||||
let center = [dim_xz as i32 / 2, dim_xz as i32 / 2];
|
||||
let p = LodParams {
|
||||
center_xz: center,
|
||||
window_xz: dim_xz,
|
||||
span: dim_xz as i32 * 3, // horizonte a ~3 ventanas de distancia
|
||||
stride: 6,
|
||||
sky_horizon: atmo.sky_horizon,
|
||||
fog_density: atmo.fog_density,
|
||||
sun_dir: sun,
|
||||
};
|
||||
let (verts, indices) = lod_skirt(&p, dim, seed);
|
||||
eprintln!("falda LOD: {} vértices, {} triángulos", verts.len(), indices.len() / 3);
|
||||
let mut skirt = Renderer3d::new(&hal.device, FMT);
|
||||
skirt.set_geometry(&hal.device, &verts, &indices);
|
||||
|
||||
let mut scene = Scene3d::new();
|
||||
|
||||
// Cámara elevada cerca del borde -Z mirando hacia +Z (el horizonte): ve la
|
||||
// ventana fina cerca y, detrás, la falda lejana.
|
||||
let mut hmax = 0u32;
|
||||
for z in (0..dim[2]).step_by(4) {
|
||||
for x in (0..dim[0]).step_by(4) {
|
||||
if let Some(h) = grid.height_at(x, z) {
|
||||
hmax = hmax.max(h);
|
||||
}
|
||||
}
|
||||
}
|
||||
let eye_y = (hmax as f32 - dy as f32 * 0.5) + dy as f32 * 0.30 + 6.0;
|
||||
let camera = Camera3d::fly(Vec3::new(0.0, eye_y, -(dim[2] as f32) * 0.46), 0.0, -0.13);
|
||||
|
||||
// Toma 1: sólo voxels (sin falda) — horizonte = niebla/vacío.
|
||||
let off = render(&hal, &mut renderer, &mut scene, &mut vr, &[], &camera);
|
||||
write_png(&off, "/tmp/m6_lod_off.png");
|
||||
// Toma 2: voxels + falda LOD (un nivel) — horizonte con relieve.
|
||||
let on = render(&hal, &mut renderer, &mut scene, &mut vr, &[&skirt], &camera);
|
||||
write_png(&on, "/tmp/m6_lod_on.png");
|
||||
eprintln!("escritos /tmp/m6_lod_off.png (sin LOD) y /tmp/m6_lod_on.png (con LOD)");
|
||||
|
||||
// --- Un nivel vs PIRÁMIDE multi-nivel, con niebla baja para que se vea hasta
|
||||
// dónde llega cada uno (con la niebla normal el horizonte se taparía igual). El
|
||||
// único nivel se corta a ~3 ventanas; la pirámide llega a ~16.
|
||||
let low_fog = 0.30 / dim_xz as f32;
|
||||
vr.atmosphere = Atmosphere { fog_density: low_fog, ..atmo };
|
||||
// Cámara aérea (alta, mirando hacia abajo) para esta comparación: así el terreno
|
||||
// lejano se despliega en el suelo en vez de apretarse contra la línea del horizonte
|
||||
// — se ve **hasta dónde** llega cada falda.
|
||||
let cam_high = Camera3d::fly(
|
||||
Vec3::new(0.0, eye_y + dy as f32 * 2.2, -(dim[2] as f32) * 0.5),
|
||||
0.0,
|
||||
-0.62,
|
||||
);
|
||||
|
||||
let p_single = LodParams { fog_density: low_fog, ..clone_params(&p) };
|
||||
let (sv, si) = lod_skirt(&p_single, dim, seed);
|
||||
let mut single = Renderer3d::new(&hal.device, FMT);
|
||||
single.set_geometry(&hal.device, &sv, &si);
|
||||
let single_shot = render(&hal, &mut renderer, &mut scene, &mut vr, &[&single], &cam_high);
|
||||
write_png(&single_shot, "/tmp/m6_lod_single.png");
|
||||
|
||||
let rings = [
|
||||
LodRing { stride: 6, span: dim_xz as i32 * 3 },
|
||||
LodRing { stride: 16, span: dim_xz as i32 * 8 },
|
||||
LodRing { stride: 40, span: dim_xz as i32 * 16 },
|
||||
];
|
||||
let p_pyr = LodParams { fog_density: low_fog, ..clone_params(&p) };
|
||||
let meshes = lod_skirt_pyramid(&p_pyr, dim, seed, &rings);
|
||||
let total_tris: usize = meshes.iter().map(|(_, i)| i.len() / 3).sum();
|
||||
eprintln!("pirámide LOD: {} anillos, {} triángulos a {} voxels de alcance", meshes.len(), total_tris, dim_xz as i32 * 16);
|
||||
let renderers: Vec<Renderer3d> = meshes
|
||||
.iter()
|
||||
.map(|(v, i)| {
|
||||
let mut r = Renderer3d::new(&hal.device, FMT);
|
||||
r.set_geometry(&hal.device, v, i);
|
||||
r
|
||||
})
|
||||
.collect();
|
||||
let refs: Vec<&Renderer3d> = renderers.iter().collect();
|
||||
let pyr_shot = render(&hal, &mut renderer, &mut scene, &mut vr, &refs, &cam_high);
|
||||
write_png(&pyr_shot, "/tmp/m6_lod_pyramid.png");
|
||||
eprintln!("escritos /tmp/m6_lod_single.png (1 nivel) y /tmp/m6_lod_pyramid.png (multi-nivel)");
|
||||
}
|
||||
|
||||
/// Copia los campos de un [`LodParams`] (no deriva `Clone` a propósito por el
|
||||
/// `sun_dir`; acá lo replicamos para variar sólo la niebla).
|
||||
fn clone_params(p: &LodParams) -> LodParams {
|
||||
LodParams {
|
||||
center_xz: p.center_xz,
|
||||
window_xz: p.window_xz,
|
||||
span: p.span,
|
||||
stride: p.stride,
|
||||
sky_horizon: p.sky_horizon,
|
||||
fog_density: p.fog_density,
|
||||
sun_dir: p.sun_dir,
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn render(
|
||||
hal: &Hal,
|
||||
renderer: &mut Renderer,
|
||||
scene: &mut Scene3d,
|
||||
vr: &mut VoxelRenderer,
|
||||
meshes: &[&Renderer3d],
|
||||
camera: &Camera3d,
|
||||
) -> Vec<u8> {
|
||||
let inter = hal.device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some("inter"),
|
||||
size: wgpu::Extent3d { width: W, height: H, depth_or_array_layers: 1 },
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: FMT,
|
||||
usage: wgpu::TextureUsages::STORAGE_BINDING
|
||||
| wgpu::TextureUsages::TEXTURE_BINDING
|
||||
| wgpu::TextureUsages::RENDER_ATTACHMENT
|
||||
| wgpu::TextureUsages::COPY_SRC,
|
||||
view_formats: &[],
|
||||
});
|
||||
let view = inter.create_view(&wgpu::TextureViewDescriptor::default());
|
||||
renderer
|
||||
.render_to_view(hal, &vello::Scene::new(), &view, W, H, Color::from_rgba8(0, 0, 0, 255))
|
||||
.expect("base");
|
||||
let mut enc = hal
|
||||
.device
|
||||
.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("lod") });
|
||||
scene.render(&hal.device, &hal.queue, &mut enc, &view, (W, H), camera, Some(vr), meshes);
|
||||
hal.queue.submit(std::iter::once(enc.finish()));
|
||||
let _ = hal.device.poll(wgpu::PollType::wait_indefinitely());
|
||||
readback(hal, &inter)
|
||||
}
|
||||
|
||||
fn readback(hal: &Hal, target: &wgpu::Texture) -> Vec<u8> {
|
||||
let unpadded = (W * 4) as usize;
|
||||
let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT as usize;
|
||||
let padded = unpadded.div_ceil(align) * align;
|
||||
let buf = hal.device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("readback"),
|
||||
size: (padded * H as usize) as u64,
|
||||
usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
let mut enc = hal
|
||||
.device
|
||||
.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
|
||||
enc.copy_texture_to_buffer(
|
||||
wgpu::TexelCopyTextureInfo {
|
||||
texture: target,
|
||||
mip_level: 0,
|
||||
origin: wgpu::Origin3d::ZERO,
|
||||
aspect: wgpu::TextureAspect::All,
|
||||
},
|
||||
wgpu::TexelCopyBufferInfo {
|
||||
buffer: &buf,
|
||||
layout: wgpu::TexelCopyBufferLayout {
|
||||
offset: 0,
|
||||
bytes_per_row: Some(padded as u32),
|
||||
rows_per_image: Some(H),
|
||||
},
|
||||
},
|
||||
wgpu::Extent3d { width: W, height: H, depth_or_array_layers: 1 },
|
||||
);
|
||||
hal.queue.submit(std::iter::once(enc.finish()));
|
||||
let slice = buf.slice(..);
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
slice.map_async(wgpu::MapMode::Read, move |r| {
|
||||
let _ = tx.send(r);
|
||||
});
|
||||
let _ = hal.device.poll(wgpu::PollType::wait_indefinitely());
|
||||
rx.recv().unwrap().unwrap();
|
||||
let data = slice.get_mapped_range();
|
||||
let mut pixels = Vec::with_capacity((W * H * 4) as usize);
|
||||
for row in 0..H as usize {
|
||||
let s = row * padded;
|
||||
pixels.extend_from_slice(&data[s..s + unpadded]);
|
||||
}
|
||||
drop(data);
|
||||
buf.unmap();
|
||||
pixels
|
||||
}
|
||||
|
||||
fn write_png(pixels: &[u8], path: &str) {
|
||||
let file = File::create(path).expect("png");
|
||||
let mut enc = png::Encoder::new(BufWriter::new(file), W, H);
|
||||
enc.set_color(png::ColorType::Rgba);
|
||||
enc.set_depth(png::BitDepth::Eight);
|
||||
let mut wtr = enc.write_header().unwrap();
|
||||
wtr.write_image_data(pixels).unwrap();
|
||||
}
|
||||
@@ -0,0 +1,351 @@
|
||||
//! Demo headless de M6 — **streaming toroidal**: una ventana voxel acotada que
|
||||
//! se desliza por un mundo procedural **ilimitado** ([`WorldStream`]) re-subiendo
|
||||
//! a la GPU **sólo la franja de bricks que entra** (no la ventana entera, ni
|
||||
//! reconstruyendo el renderer): la textura del brick pool es un **ring buffer**
|
||||
//! (`world_brick mod cdim`) y el shader envuelve la celda lógica con un offset de
|
||||
//! origen ([`VoxelRenderer::scroll_to`]).
|
||||
//!
|
||||
//! La cámara se queda quieta en el **centro** de la ventana mirando adelante; lo
|
||||
//! que avanza es el **foco de mundo** (`focus_z`), que marcha mucho más allá del
|
||||
//! tamaño de la ventana. Cada cuadro [`WorldStream::follow`] reubica la ventana y
|
||||
//! `scroll_to` sube sólo la franja → cada PNG es **paisaje nuevo y distinto** sin
|
||||
//! "muro" ni repetición, y el reporte muestra que se suben **KiB**, no MiB.
|
||||
//!
|
||||
//! **Prueba de paridad**: en el último cuadro se compara el render scrolleado
|
||||
//! contra un renderer **reconstruido de cero** en ese mismo origen — deben dar
|
||||
//! la **misma imagen** (el toroidal no degrada el contenido). Falla con assert si
|
||||
//! divergen.
|
||||
//!
|
||||
//! `cargo run -p llimphi-voxel --example terrain_streaming --release -- [dim_xz] [seed] [frames]`
|
||||
//! → escribe /tmp/m6_stream_##.png
|
||||
|
||||
use std::fs::File;
|
||||
use std::io::BufWriter;
|
||||
|
||||
use llimphi_3d::glam::Vec3;
|
||||
use llimphi_3d::{Atmosphere, Camera3d, VoxelGrid, VoxelRenderer};
|
||||
use llimphi_hal::{wgpu, Hal};
|
||||
use llimphi_raster::peniko::Color;
|
||||
use llimphi_raster::{vello, Renderer};
|
||||
use llimphi_voxel::{fill_terrain_window, WorldStream};
|
||||
|
||||
const W: u32 = 960;
|
||||
const H: u32 = 540;
|
||||
const FMT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
|
||||
|
||||
fn main() {
|
||||
let dim_xz: u32 = std::env::args().nth(1).and_then(|s| s.parse().ok()).unwrap_or(160);
|
||||
let seed: u32 = std::env::args().nth(2).and_then(|s| s.parse().ok()).unwrap_or(1337);
|
||||
let frames: u32 = std::env::args().nth(3).and_then(|s| s.parse().ok()).unwrap_or(6);
|
||||
let dy: u32 = (dim_xz * 4 / 10).max(48);
|
||||
let dim = [dim_xz, dy, dim_xz];
|
||||
|
||||
let hal = pollster::block_on(Hal::new(None)).expect("hal");
|
||||
let mut renderer = Renderer::new(&hal).expect("renderer");
|
||||
|
||||
// Ventana de mundo (paso = lado de brick). Centro inicial en mundo (0,0).
|
||||
let step = llimphi_3d::VOXEL_BRICK;
|
||||
let mut stream = WorldStream::new(dim, seed, 0, 0, step);
|
||||
|
||||
// El renderer se construye UNA vez, desde un grid en **mundo (0,0)** (donde
|
||||
// `brick_origin = 0` es consistente con el ring buffer: celda física P ⟺
|
||||
// world_brick ≡ P mod cdim). El primer `scroll_to` lo lleva al origen real
|
||||
// del stream; de ahí en más, sólo franjas.
|
||||
let mut zero = VoxelGrid::new(dim);
|
||||
fill_terrain_window(&mut zero, [0, 0], seed);
|
||||
let mut vr = VoxelRenderer::new(&hal.device, &hal.queue, FMT, &zero);
|
||||
vr.sun_dir = [0.55, 0.5, 0.32];
|
||||
vr.atmosphere = Atmosphere {
|
||||
sky_zenith: [64, 118, 196],
|
||||
sky_horizon: [202, 218, 236],
|
||||
fog_density: 0.7 / dim_xz as f32,
|
||||
};
|
||||
let (_, total_bricks) = vr.brick_usage();
|
||||
let full_pool_kib = vr.memory_bytes().0 / 1024;
|
||||
|
||||
let inter = make_target(&hal);
|
||||
let inter_view = inter.create_view(&wgpu::TextureViewDescriptor::default());
|
||||
|
||||
let mut last_pixels: Vec<u8> = Vec::new();
|
||||
|
||||
for i in 0..frames {
|
||||
// El foco de mundo marcha en +Z; en pocos cuadros recorre varias ventanas
|
||||
// (cada PNG es mundo nuevo) pero cada paso entra sólo ~¼ de ventana, así
|
||||
// se ve que el scroll sube una **franja**, no el mundo entero.
|
||||
let focus_z = i as i32 * (dim_xz as i32 / 4);
|
||||
stream.follow(0, focus_z);
|
||||
// Streaming toroidal: sube sólo la franja de bricks que entró.
|
||||
let uploaded = vr.scroll_to(&hal.device, &hal.queue, stream.origin_voxel(), stream.grid());
|
||||
|
||||
// Cámara: sobre los picos de la ventana, atrás, mirando +Z hacia abajo.
|
||||
let camera = camera_for(stream.grid(), dim);
|
||||
|
||||
last_pixels = render_to_pixels(&hal, &mut renderer, &inter, &inter_view, &mut vr, &camera);
|
||||
let out = format!("/tmp/m6_stream_{i:02}.png");
|
||||
encode_png(&last_pixels, W, H, &out);
|
||||
|
||||
let [ox, oz] = stream.origin();
|
||||
let (used, _) = vr.brick_usage();
|
||||
eprintln!(
|
||||
"{out} — foco_z={focus_z}, origen=({ox},{oz}), subido {} KiB de {} KiB de ventana ({}/{} bricks vivos)",
|
||||
uploaded / 1024,
|
||||
full_pool_kib,
|
||||
used,
|
||||
total_bricks,
|
||||
);
|
||||
}
|
||||
|
||||
// --- Paridad: el render scrolleado del último cuadro debe coincidir con un
|
||||
// renderer RECONSTRUIDO de cero en ese mismo origen (mismo contenido lógico).
|
||||
let camera = camera_for(stream.grid(), dim);
|
||||
|
||||
let mut fresh = VoxelRenderer::new(&hal.device, &hal.queue, FMT, stream.grid());
|
||||
fresh.sun_dir = vr.sun_dir;
|
||||
fresh.atmosphere = vr.atmosphere;
|
||||
let fresh_pixels = render_to_pixels(&hal, &mut renderer, &inter, &inter_view, &mut fresh, &camera);
|
||||
|
||||
let (max_d, mean_d) = diff(&last_pixels, &fresh_pixels);
|
||||
encode_png(&fresh_pixels, W, H, "/tmp/m6_stream_fresh.png");
|
||||
eprintln!(
|
||||
"PARIDAD scroll-vs-rebuild: max |Δ|={max_d}, media |Δ|={mean_d:.3} (0 = idéntico) → /tmp/m6_stream_fresh.png"
|
||||
);
|
||||
assert!(
|
||||
max_d <= 2,
|
||||
"el streaming toroidal divergió del rebuild (max |Δ|={max_d})"
|
||||
);
|
||||
eprintln!("PARIDAD OK — el toroidal rinde idéntico al rebuild, subiendo sólo la franja.");
|
||||
|
||||
// --- Pool-grow: arrancar con un pool minúsculo (grid vacío) y scrollear a una
|
||||
// ventana densa **lejana** (sin solape → todo entra, sin bulk stale) fuerza al
|
||||
// brick pool a crecer. La paridad vs un rebuild prueba que creció sin huecos.
|
||||
let far = [dim_xz as i32 * 2, 0, dim_xz as i32 * 2]; // brick-aligned, lejos del origen
|
||||
let mut far_grid = VoxelGrid::new(dim);
|
||||
fill_terrain_window(&mut far_grid, [far[0], far[2]], seed);
|
||||
|
||||
let mut tiny = VoxelRenderer::new(&hal.device, &hal.queue, FMT, &VoxelGrid::new(dim));
|
||||
tiny.sun_dir = vr.sun_dir;
|
||||
tiny.atmosphere = vr.atmosphere;
|
||||
let cap0 = tiny.pool_capacity();
|
||||
tiny.scroll_to(&hal.device, &hal.queue, far, &far_grid);
|
||||
let cap1 = tiny.pool_capacity();
|
||||
|
||||
let cam = camera_for(&far_grid, dim);
|
||||
let tiny_px = render_to_pixels(&hal, &mut renderer, &inter, &inter_view, &mut tiny, &cam);
|
||||
let mut far_fresh = VoxelRenderer::new(&hal.device, &hal.queue, FMT, &far_grid);
|
||||
far_fresh.sun_dir = vr.sun_dir;
|
||||
far_fresh.atmosphere = vr.atmosphere;
|
||||
let far_fresh_px = render_to_pixels(&hal, &mut renderer, &inter, &inter_view, &mut far_fresh, &cam);
|
||||
let (gmax, _) = diff(&tiny_px, &far_fresh_px);
|
||||
encode_png(&tiny_px, W, H, "/tmp/m6_stream_grow.png");
|
||||
eprintln!("POOL-GROW: pool {cap0} → {cap1} slots, render vs rebuild max|Δ|={gmax}");
|
||||
assert!(cap1 > cap0, "el pool no creció (arrancó con capacidad suficiente)");
|
||||
assert!(gmax <= 2, "el pool creció con huecos (max|Δ|={gmax})");
|
||||
eprintln!("POOL-GROW OK — el pool creció y la ventana densa quedó completa, sin huecos.");
|
||||
|
||||
// --- Persistencia de ediciones: un pilar magenta editado sobre la columna de
|
||||
// mundo (0,0) debe seguir ahí tras alejarse miles de voxels y volver (el
|
||||
// terreno se regenera desde la semilla, el overlay de `edits` lo re-aplica).
|
||||
let mut s = WorldStream::new(dim, seed, 0, 0, llimphi_3d::VOXEL_BRICK);
|
||||
let (lx0, lz0) = s.world_to_local(0, 0).unwrap();
|
||||
let gh = s.grid().height_at(lx0, lz0).unwrap_or(dim[1] / 2) as i32;
|
||||
// Torre magenta gruesa (5×5) y alta sobre la columna de mundo (0,0): el
|
||||
// terreno nunca pone magenta, así el píxel es inequívocamente la edición.
|
||||
let top = (gh + 22).min(dim[1] as i32 - 1);
|
||||
for wy in (gh + 1)..top {
|
||||
for dx in -2..=2 {
|
||||
for dz in -2..=2 {
|
||||
s.edit(dx, wy, dz, Some([240, 40, 220]));
|
||||
}
|
||||
}
|
||||
}
|
||||
// Cámara apuntada a la torre (mundo centrado en el origen → columna (0,0) cae
|
||||
// en el centro; `y` de mundo = `y` de grilla − dim_y/2).
|
||||
let half_y = dim[1] as f32 * 0.5;
|
||||
let tower_mid = (gh + 11) as f32 - half_y;
|
||||
let pcam = Camera3d {
|
||||
eye: Vec3::new(14.0, tower_mid + 10.0, -(dim[2] as f32) * 0.32),
|
||||
target: Vec3::new(0.0, tower_mid, 0.0),
|
||||
..Camera3d::default()
|
||||
};
|
||||
// Se reconstruye el renderer desde `s.grid()` en cada toma: acá probamos la
|
||||
// PERSISTENCIA (la edición sobrevive el regen), no el upload incremental (ya
|
||||
// verificado arriba). Subir la torre por scroll caería en bricks "bulk" que el
|
||||
// toroidal no re-sube (asume terreno determinista; la edición rompe eso).
|
||||
let mut render_grid = |g: &VoxelGrid| {
|
||||
let mut r = VoxelRenderer::new(&hal.device, &hal.queue, FMT, g);
|
||||
r.sun_dir = vr.sun_dir;
|
||||
r.atmosphere = vr.atmosphere;
|
||||
render_to_pixels(&hal, &mut renderer, &inter, &inter_view, &mut r, &pcam)
|
||||
};
|
||||
let before = render_grid(s.grid());
|
||||
encode_png(&before, W, H, "/tmp/m6_persist_before.png");
|
||||
|
||||
// Alejarse miles de voxels (el terreno se regenera, la torre sale de ventana)
|
||||
// y volver al origen (se regenera de nuevo + overlay re-aplica la torre).
|
||||
s.follow(4000, 4000);
|
||||
s.follow(0, 0);
|
||||
let after = render_grid(s.grid());
|
||||
encode_png(&after, W, H, "/tmp/m6_persist_after.png");
|
||||
|
||||
// Conteo de píxeles magenta (la torre) en ambas tomas: deben ser ~iguales.
|
||||
// Magenta = rojo y azul ambos claramente por encima del verde (robusto a la
|
||||
// luz coloreada/AO, que baja los valores absolutos pero no esa relación).
|
||||
let magenta = |px: &[u8]| -> u32 {
|
||||
px.chunks_exact(4)
|
||||
.filter(|c| {
|
||||
let (r, g, b) = (c[0] as i32, c[1] as i32, c[2] as i32);
|
||||
r > g + 40 && b > g + 40
|
||||
})
|
||||
.count() as u32
|
||||
};
|
||||
let (mb, ma) = (magenta(&before), magenta(&after));
|
||||
eprintln!("PERSISTENCIA: pilar magenta = {mb} px antes / {ma} px tras alejarse+volver ({} ediciones)", s.edit_count());
|
||||
assert!(mb > 200, "el pilar debería verse antes ({mb} px)");
|
||||
assert!(ma * 100 >= mb * 90, "el pilar se perdió al volver ({mb}→{ma} px)");
|
||||
eprintln!("PERSISTENCIA OK — la edición sobrevivió el regen del streaming.");
|
||||
|
||||
// --- CAS a disco: guardar el blob de ediciones en un archivo nombrado por su
|
||||
// BLAKE3 y recargarlo en un mundo FRESCO (simula reiniciar el programa).
|
||||
let blob = s.export_edits();
|
||||
let addr = blake3::hash(&blob).to_hex();
|
||||
let dir = std::path::Path::new("/tmp/m6_cas");
|
||||
std::fs::create_dir_all(dir).ok();
|
||||
let path = dir.join(format!("{addr}.edits"));
|
||||
std::fs::write(&path, &blob).expect("escribir blob");
|
||||
|
||||
let mut loaded = WorldStream::new(dim, seed, 0, 0, llimphi_3d::VOXEL_BRICK);
|
||||
let from_disk = std::fs::read(&path).expect("leer blob");
|
||||
let n = loaded.import_edits(&from_disk).expect("blob válido");
|
||||
let cas = magenta(&render_grid(loaded.grid()));
|
||||
encode_png(&render_grid(loaded.grid()), W, H, "/tmp/m6_persist_cas.png");
|
||||
eprintln!("CAS A DISCO: {n} ediciones desde {} ({} bytes), torre = {cas} px", path.display(), blob.len());
|
||||
assert!(cas * 100 >= mb * 90, "la torre no se restauró desde disco ({mb}→{cas} px)");
|
||||
eprintln!("CAS OK — las ediciones se restauraron desde un archivo direccionado por BLAKE3.");
|
||||
}
|
||||
|
||||
/// Cámara de la ventana: posada sobre los **picos** del terreno (muestreo de
|
||||
/// alturas), atrás del centro y mirando hacia adelante y abajo — encuadra el
|
||||
/// relieve sin importar si el centro cae en agua o en una cima.
|
||||
fn camera_for(grid: &VoxelGrid, dim: [u32; 3]) -> Camera3d {
|
||||
let (dx, dy, dz) = (dim[0], dim[1], dim[2]);
|
||||
let mut hmax = 0u32;
|
||||
for z in (0..dz).step_by(4) {
|
||||
for x in (0..dx).step_by(4) {
|
||||
if let Some(h) = grid.height_at(x, z) {
|
||||
hmax = hmax.max(h);
|
||||
}
|
||||
}
|
||||
}
|
||||
let (dyf, dzf) = (dy as f32, dz as f32);
|
||||
let eye_y = (hmax as f32 - dyf * 0.5) + dyf * 0.28 + 8.0;
|
||||
Camera3d::fly(Vec3::new(0.0, eye_y, -dzf * 0.42), 0.0, -0.30)
|
||||
}
|
||||
|
||||
/// Diferencia entre dos buffers RGBA: `(max abs por canal, media abs)`.
|
||||
fn diff(a: &[u8], b: &[u8]) -> (u8, f64) {
|
||||
let mut max = 0u8;
|
||||
let mut sum = 0u64;
|
||||
for (x, y) in a.iter().zip(b.iter()) {
|
||||
let d = x.abs_diff(*y);
|
||||
max = max.max(d);
|
||||
sum += d as u64;
|
||||
}
|
||||
(max, sum as f64 / a.len().max(1) as f64)
|
||||
}
|
||||
|
||||
fn make_target(hal: &Hal) -> wgpu::Texture {
|
||||
hal.device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some("inter"),
|
||||
size: wgpu::Extent3d { width: W, height: H, depth_or_array_layers: 1 },
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: FMT,
|
||||
usage: wgpu::TextureUsages::STORAGE_BINDING
|
||||
| wgpu::TextureUsages::TEXTURE_BINDING
|
||||
| wgpu::TextureUsages::RENDER_ATTACHMENT
|
||||
| wgpu::TextureUsages::COPY_SRC,
|
||||
view_formats: &[],
|
||||
})
|
||||
}
|
||||
|
||||
/// Rinde el voxel renderer a `inter` (sobre un fondo negro de vello) y devuelve
|
||||
/// los píxeles RGBA planos.
|
||||
fn render_to_pixels(
|
||||
hal: &Hal,
|
||||
renderer: &mut Renderer,
|
||||
inter: &wgpu::Texture,
|
||||
inter_view: &wgpu::TextureView,
|
||||
vr: &mut VoxelRenderer,
|
||||
camera: &Camera3d,
|
||||
) -> Vec<u8> {
|
||||
let base = vello::Scene::new();
|
||||
renderer
|
||||
.render_to_view(hal, &base, inter_view, W, H, Color::from_rgba8(0, 0, 0, 255))
|
||||
.expect("render base");
|
||||
let mut enc = hal
|
||||
.device
|
||||
.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("voxel-pass") });
|
||||
vr.render(&hal.device, &hal.queue, &mut enc, inter_view, (W, H), camera);
|
||||
hal.queue.submit(std::iter::once(enc.finish()));
|
||||
let _ = hal.device.poll(wgpu::PollType::wait_indefinitely());
|
||||
readback(hal, inter)
|
||||
}
|
||||
|
||||
fn readback(hal: &Hal, target: &wgpu::Texture) -> Vec<u8> {
|
||||
let unpadded = (W * 4) as usize;
|
||||
let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT as usize;
|
||||
let padded = unpadded.div_ceil(align) * align;
|
||||
let buf = hal.device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("readback"),
|
||||
size: (padded * H as usize) as u64,
|
||||
usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
let mut enc = hal
|
||||
.device
|
||||
.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
|
||||
enc.copy_texture_to_buffer(
|
||||
wgpu::TexelCopyTextureInfo {
|
||||
texture: target,
|
||||
mip_level: 0,
|
||||
origin: wgpu::Origin3d::ZERO,
|
||||
aspect: wgpu::TextureAspect::All,
|
||||
},
|
||||
wgpu::TexelCopyBufferInfo {
|
||||
buffer: &buf,
|
||||
layout: wgpu::TexelCopyBufferLayout {
|
||||
offset: 0,
|
||||
bytes_per_row: Some(padded as u32),
|
||||
rows_per_image: Some(H),
|
||||
},
|
||||
},
|
||||
wgpu::Extent3d { width: W, height: H, depth_or_array_layers: 1 },
|
||||
);
|
||||
hal.queue.submit(std::iter::once(enc.finish()));
|
||||
let slice = buf.slice(..);
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
slice.map_async(wgpu::MapMode::Read, move |r| {
|
||||
let _ = tx.send(r);
|
||||
});
|
||||
let _ = hal.device.poll(wgpu::PollType::wait_indefinitely());
|
||||
rx.recv().unwrap().unwrap();
|
||||
let data = slice.get_mapped_range();
|
||||
let mut pixels = Vec::with_capacity((W * H * 4) as usize);
|
||||
for row in 0..H as usize {
|
||||
let s = row * padded;
|
||||
pixels.extend_from_slice(&data[s..s + unpadded]);
|
||||
}
|
||||
drop(data);
|
||||
buf.unmap();
|
||||
pixels
|
||||
}
|
||||
|
||||
fn encode_png(pixels: &[u8], w: u32, h: u32, path: &str) {
|
||||
let file = File::create(path).expect("png");
|
||||
let mut enc = png::Encoder::new(BufWriter::new(file), w, h);
|
||||
enc.set_color(png::ColorType::Rgba);
|
||||
enc.set_depth(png::BitDepth::Eight);
|
||||
let mut wtr = enc.write_header().unwrap();
|
||||
wtr.write_image_data(pixels).unwrap();
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
//! Demo "hero" de M6 — **el mundo completo**: un vuelo por un mundo procedural
|
||||
//! **ilimitado** que combina las dos mitades del frente de streaming:
|
||||
//!
|
||||
//! - **Streaming toroidal** ([`WorldStream`] + `VoxelRenderer::scroll_to`): la
|
||||
//! ventana voxel fina se desliza siguiendo a la cámara, re-subiendo sólo la
|
||||
//! franja que entra (mundo sin fin, sin muro ni repetición).
|
||||
//! - **LOD del horizonte** ([`lod_skirt`]): una malla gruesa del terreno
|
||||
//! circundante, regenerada al recentrar, hace que más allá de la ventana fina
|
||||
//! se vean colinas lejanas (compuesta con los voxels por el depth de
|
||||
//! [`Scene3d`]).
|
||||
//!
|
||||
//! La cámara queda en el centro de la ventana mirando hacia el relieve que viene;
|
||||
//! lo que avanza es el foco de mundo. Cada PNG es terreno nuevo, siempre con
|
||||
//! horizonte.
|
||||
//!
|
||||
//! `cargo run -p llimphi-voxel --example terrain_world --release -- [dim_xz] [seed] [frames]`
|
||||
//! → /tmp/m6_world_##.png
|
||||
|
||||
use std::fs::File;
|
||||
use std::io::BufWriter;
|
||||
|
||||
use llimphi_3d::glam::Vec3;
|
||||
use llimphi_3d::{Atmosphere, Camera3d, Renderer3d, Scene3d, VoxelGrid, VoxelRenderer};
|
||||
use llimphi_hal::{wgpu, Hal};
|
||||
use llimphi_raster::peniko::Color;
|
||||
use llimphi_raster::{vello, Renderer};
|
||||
use llimphi_voxel::{fill_terrain_window, lod_skirt, LodParams, WorldStream};
|
||||
|
||||
const W: u32 = 960;
|
||||
const H: u32 = 540;
|
||||
const FMT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
|
||||
|
||||
fn main() {
|
||||
let dim_xz: u32 = std::env::args().nth(1).and_then(|s| s.parse().ok()).unwrap_or(160);
|
||||
let seed: u32 = std::env::args().nth(2).and_then(|s| s.parse().ok()).unwrap_or(1337);
|
||||
let frames: u32 = std::env::args().nth(3).and_then(|s| s.parse().ok()).unwrap_or(6);
|
||||
let dy: u32 = (dim_xz * 4 / 10).max(48);
|
||||
let dim = [dim_xz, dy, dim_xz];
|
||||
|
||||
let hal = pollster::block_on(Hal::new(None)).expect("hal");
|
||||
let mut renderer = Renderer::new(&hal).expect("renderer");
|
||||
|
||||
let step = llimphi_3d::VOXEL_BRICK;
|
||||
let mut stream = WorldStream::new(dim, seed, 0, 0, step);
|
||||
|
||||
let sun = [0.55, 0.5, 0.32];
|
||||
let atmo = Atmosphere {
|
||||
sky_zenith: [66, 120, 198],
|
||||
sky_horizon: [202, 218, 236],
|
||||
fog_density: 1.0 / dim_xz as f32,
|
||||
};
|
||||
|
||||
// Renderer voxel construido UNA vez desde mundo (0,0) (invariante del ring
|
||||
// buffer); el 1er scroll lo lleva al origen del stream.
|
||||
let mut zero = VoxelGrid::new(dim);
|
||||
fill_terrain_window(&mut zero, [0, 0], seed);
|
||||
let mut vr = VoxelRenderer::new(&hal.device, &hal.queue, FMT, &zero);
|
||||
vr.sun_dir = sun;
|
||||
vr.atmosphere = atmo;
|
||||
|
||||
let mut skirt = Renderer3d::new(&hal.device, FMT);
|
||||
let mut scene = Scene3d::new();
|
||||
|
||||
let inter = make_target(&hal);
|
||||
let view = inter.create_view(&wgpu::TextureViewDescriptor::default());
|
||||
|
||||
for i in 0..frames {
|
||||
// El foco marcha en +Z; cada cuadro entra a mundo nuevo.
|
||||
let focus_z = i as i32 * (dim_xz as i32 / 2);
|
||||
stream.follow(0, focus_z);
|
||||
vr.scroll_to(&hal.device, &hal.queue, stream.origin_voxel(), stream.grid());
|
||||
|
||||
// Falda LOD recentrada en la ventana actual (su hueco = la ventana fina).
|
||||
let [ox, oz] = stream.origin();
|
||||
let center = [ox + dim_xz as i32 / 2, oz + dim_xz as i32 / 2];
|
||||
let p = LodParams {
|
||||
center_xz: center,
|
||||
window_xz: dim_xz,
|
||||
span: dim_xz as i32 * 3,
|
||||
stride: 6,
|
||||
sky_horizon: atmo.sky_horizon,
|
||||
fog_density: atmo.fog_density,
|
||||
sun_dir: sun,
|
||||
};
|
||||
let (verts, indices) = lod_skirt(&p, dim, seed);
|
||||
skirt.set_geometry(&hal.device, &verts, &indices);
|
||||
|
||||
// Cámara: en el centro de la ventana, atrás, mirando +Z hacia el horizonte.
|
||||
let mut hmax = 0u32;
|
||||
for z in (0..dim[2]).step_by(4) {
|
||||
for x in (0..dim[0]).step_by(4) {
|
||||
if let Some(h) = stream.grid().height_at(x, z) {
|
||||
hmax = hmax.max(h);
|
||||
}
|
||||
}
|
||||
}
|
||||
let eye_y = (hmax as f32 - dy as f32 * 0.5) + dy as f32 * 0.22 + 7.0;
|
||||
let camera = Camera3d::fly(Vec3::new(0.0, eye_y, -(dim[2] as f32) * 0.42), 0.0, -0.12);
|
||||
|
||||
// Render base (vello) + escena 3D (voxel fino + falda LOD, depth compartido).
|
||||
renderer
|
||||
.render_to_view(&hal, &vello::Scene::new(), &view, W, H, Color::from_rgba8(0, 0, 0, 255))
|
||||
.expect("base");
|
||||
let mut enc = hal
|
||||
.device
|
||||
.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("world") });
|
||||
scene.render(&hal.device, &hal.queue, &mut enc, &view, (W, H), &camera, Some(&vr), &[&skirt]);
|
||||
hal.queue.submit(std::iter::once(enc.finish()));
|
||||
let _ = hal.device.poll(wgpu::PollType::wait_indefinitely());
|
||||
|
||||
let out = format!("/tmp/m6_world_{i:02}.png");
|
||||
write_png(&readback(&hal, &inter), &out);
|
||||
eprintln!("{out} — foco_z={focus_z}, origen=({ox},{oz}), {} tris de horizonte", indices.len() / 3);
|
||||
}
|
||||
}
|
||||
|
||||
fn make_target(hal: &Hal) -> wgpu::Texture {
|
||||
hal.device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some("inter"),
|
||||
size: wgpu::Extent3d { width: W, height: H, depth_or_array_layers: 1 },
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: FMT,
|
||||
usage: wgpu::TextureUsages::STORAGE_BINDING
|
||||
| wgpu::TextureUsages::TEXTURE_BINDING
|
||||
| wgpu::TextureUsages::RENDER_ATTACHMENT
|
||||
| wgpu::TextureUsages::COPY_SRC,
|
||||
view_formats: &[],
|
||||
})
|
||||
}
|
||||
|
||||
fn readback(hal: &Hal, target: &wgpu::Texture) -> Vec<u8> {
|
||||
let unpadded = (W * 4) as usize;
|
||||
let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT as usize;
|
||||
let padded = unpadded.div_ceil(align) * align;
|
||||
let buf = hal.device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("readback"),
|
||||
size: (padded * H as usize) as u64,
|
||||
usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
let mut enc = hal
|
||||
.device
|
||||
.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
|
||||
enc.copy_texture_to_buffer(
|
||||
wgpu::TexelCopyTextureInfo {
|
||||
texture: target,
|
||||
mip_level: 0,
|
||||
origin: wgpu::Origin3d::ZERO,
|
||||
aspect: wgpu::TextureAspect::All,
|
||||
},
|
||||
wgpu::TexelCopyBufferInfo {
|
||||
buffer: &buf,
|
||||
layout: wgpu::TexelCopyBufferLayout {
|
||||
offset: 0,
|
||||
bytes_per_row: Some(padded as u32),
|
||||
rows_per_image: Some(H),
|
||||
},
|
||||
},
|
||||
wgpu::Extent3d { width: W, height: H, depth_or_array_layers: 1 },
|
||||
);
|
||||
hal.queue.submit(std::iter::once(enc.finish()));
|
||||
let slice = buf.slice(..);
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
slice.map_async(wgpu::MapMode::Read, move |r| {
|
||||
let _ = tx.send(r);
|
||||
});
|
||||
let _ = hal.device.poll(wgpu::PollType::wait_indefinitely());
|
||||
rx.recv().unwrap().unwrap();
|
||||
let data = slice.get_mapped_range();
|
||||
let mut pixels = Vec::with_capacity((W * H * 4) as usize);
|
||||
for row in 0..H as usize {
|
||||
let s = row * padded;
|
||||
pixels.extend_from_slice(&data[s..s + unpadded]);
|
||||
}
|
||||
drop(data);
|
||||
buf.unmap();
|
||||
pixels
|
||||
}
|
||||
|
||||
fn write_png(pixels: &[u8], path: &str) {
|
||||
let file = File::create(path).expect("png");
|
||||
let mut enc = png::Encoder::new(BufWriter::new(file), W, H);
|
||||
enc.set_color(png::ColorType::Rgba);
|
||||
enc.set_depth(png::BitDepth::Eight);
|
||||
let mut wtr = enc.write_header().unwrap();
|
||||
wtr.write_image_data(pixels).unwrap();
|
||||
}
|
||||
Reference in New Issue
Block a user