mod common;
use std::time::Instant;
use bevy::prelude::*;
use bevy_isomesh::MeshBuilder;
use common::{Capture, CommonPlugin, DemoMesh, DemoStats, OrbitCamera, ViewFlags};
use isomesh::marching_cubes::MarchingCubes;
use isomesh::subgrid::extract::SubgridMarchingTetrahedra;
use isomesh::{RuntimeShape3, Sdf};
const HALF_EXTENT: f32 = 1.6;
const EDGE_SAMPLES: u32 = 16;
const MIN_SAMPLES: u32 = 9;
const MAX_SAMPLES: u32 = 41;
const MIN_THICKNESS: f32 = 0.10;
const MAX_THICKNESS: f32 = 3.0;
#[derive(Resource)]
struct Demo {
samples: u32,
thickness_voxels: f32,
}
#[derive(Component, Clone, Copy, PartialEq, Eq)]
enum Side {
MarchingCubes,
Subgrid,
}
#[derive(Resource)]
struct Materials {
marching_cubes: Handle<StandardMaterial>,
subgrid: Handle<StandardMaterial>,
}
struct Letters {
half_thickness: f32,
centre: f32,
tilt: f32,
}
fn thickness_override() -> Option<f32> {
std::env::var("ISOMESH_THICKNESS")
.ok()
.and_then(|raw| raw.parse::<f32>().ok())
.map(|value| value.clamp(MIN_THICKNESS, MAX_THICKNESS))
}
fn segment_distance(p: [f32; 2], a: [f32; 2], b: [f32; 2]) -> f32 {
let pa = [p[0] - a[0], p[1] - a[1]];
let ba = [b[0] - a[0], b[1] - a[1]];
let denominator = ba[0] * ba[0] + ba[1] * ba[1];
let t = if denominator > 0.0 {
((pa[0] * ba[0] + pa[1] * ba[1]) / denominator).clamp(0.0, 1.0)
} else {
0.0
};
let d = [pa[0] - ba[0] * t, pa[1] - ba[1] * t];
(d[0] * d[0] + d[1] * d[1]).sqrt()
}
const STROKES: [[[f32; 2]; 2]; 10] = [
[[-0.95, -0.6], [-0.95, 0.6]],
[[-0.5, 0.6], [0.1, 0.6]],
[[-0.5, 0.6], [-0.5, 0.0]],
[[-0.5, 0.0], [0.1, 0.0]],
[[0.1, 0.0], [0.1, -0.6]],
[[-0.5, -0.6], [0.1, -0.6]],
[[0.5, -0.6], [0.5, 0.6]],
[[1.1, -0.6], [1.1, 0.6]],
[[0.5, 0.6], [1.1, 0.6]],
[[0.5, -0.6], [1.1, -0.6]],
];
const STROKE_HALF_WIDTH: f32 = 0.08;
impl Sdf for Letters {
type Scalar = f32;
fn sample(&self, p: [f32; 3]) -> f32 {
let flat = [p[0], p[1]];
let mut nearest = f32::INFINITY;
for [a, b] in STROKES {
nearest = nearest.min(segment_distance(flat, a, b));
}
let in_plane = nearest - STROKE_HALF_WIDTH;
let mid_plane = self.centre + self.tilt * p[1];
let out_of_plane = (p[2] - mid_plane).abs() - self.half_thickness;
in_plane.max(out_of_plane)
}
}
fn main() {
App::new()
.add_plugins(DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window {
title: "isomesh — E-108 subgrid features".into(),
..default()
}),
..default()
}))
.add_plugins(CommonPlugin)
.insert_resource(Demo {
samples: common::samples_override().unwrap_or(25),
thickness_voxels: thickness_override().unwrap_or(0.35),
})
.add_systems(Startup, setup)
.add_systems(Update, (controls, remesh))
.run();
}
fn setup(
mut commands: Commands,
mut materials: ResMut<Assets<StandardMaterial>>,
mut camera: Query<&mut OrbitCamera>,
) {
for mut orbit in &mut camera {
orbit.yaw = std::f32::consts::FRAC_PI_2;
orbit.pitch = 0.55;
orbit.radius = 7.5;
}
commands.insert_resource(Materials {
marching_cubes: materials.add(StandardMaterial {
base_color: Color::srgb(0.86, 0.74, 0.58),
perceptual_roughness: 0.55,
double_sided: true,
cull_mode: None,
..default()
}),
subgrid: materials.add(StandardMaterial {
base_color: Color::srgb(0.72, 0.80, 0.86),
perceptual_roughness: 0.55,
double_sided: true,
cull_mode: None,
..default()
}),
});
}
fn controls(
keys: Res<ButtonInput<KeyCode>>,
mut demo: ResMut<Demo>,
capture: Res<Capture>,
mut flags: ResMut<ViewFlags>,
) {
if capture.is_active() {
const LOW: f32 = 0.2;
const HIGH: f32 = 1.6;
const STEPS: u32 = 28;
let phase = capture.taken % (STEPS * 2);
let step = if phase < STEPS {
phase
} else {
STEPS * 2 - phase - 1
};
demo.thickness_voxels = HIGH - (HIGH - LOW) * (step as f32 / (STEPS - 1) as f32);
return;
}
if keys.just_pressed(KeyCode::BracketRight) {
demo.samples = (demo.samples + 4).min(MAX_SAMPLES);
}
if keys.just_pressed(KeyCode::BracketLeft) {
demo.samples = demo.samples.saturating_sub(4).max(MIN_SAMPLES);
}
if keys.just_pressed(KeyCode::Equal) {
demo.thickness_voxels = (demo.thickness_voxels + 0.15).min(MAX_THICKNESS);
}
if keys.just_pressed(KeyCode::Minus) {
demo.thickness_voxels = (demo.thickness_voxels - 0.15).max(MIN_THICKNESS);
}
if keys.just_pressed(KeyCode::KeyR) {
flags.remesh_requested = true;
}
}
struct Extraction {
builder: MeshBuilder,
millis: f64,
}
fn extract_pair(samples: u32, thickness_voxels: f32) -> Option<(Extraction, Extraction)> {
let min = [-HALF_EXTENT; 3];
let cell_size = (2.0 * HALF_EXTENT) / (samples - 1) as f32;
let shape = RuntimeShape3::new([samples; 3]).ok()?;
let field = Letters {
half_thickness: 0.5 * thickness_voxels * cell_size,
centre: 0.41 * cell_size,
tilt: 0.03,
};
let mut mc = MeshBuilder::new();
let started = Instant::now();
MarchingCubes::<f32>::new()
.extract(&field, &shape, min, cell_size, &mut mc)
.ok()?;
let mc_millis = started.elapsed().as_secs_f64() * 1000.0;
let mut sub = MeshBuilder::new();
let started = Instant::now();
SubgridMarchingTetrahedra::<f32>::new(EDGE_SAMPLES)
.ok()?
.extract(&field, &shape, min, cell_size, &mut sub)
.ok()?;
let sub_millis = started.elapsed().as_secs_f64() * 1000.0;
Some((
Extraction {
builder: mc,
millis: mc_millis,
},
Extraction {
builder: sub,
millis: sub_millis,
},
))
}
#[allow(clippy::too_many_arguments)]
fn remesh(
demo: Res<Demo>,
mut stats: ResMut<DemoStats>,
mut meshes: ResMut<Assets<Mesh>>,
materials: Res<Materials>,
mut commands: Commands,
mut query: Query<(&mut Mesh3d, &mut Transform, &Side)>,
mut flags: ResMut<ViewFlags>,
mut last: Local<Option<(u32, u32)>>,
) {
let key = (demo.samples, (demo.thickness_voxels * 100.0) as u32);
if *last == Some(key) && !flags.remesh_requested {
return;
}
*last = Some(key);
flags.remesh_requested = false;
let Some((mc, sub)) = extract_pair(demo.samples, demo.thickness_voxels) else {
return;
};
stats.title = format!(
"E-108 marching cubes | subgrid marching tetrahedra letters {:.2} voxels thick \
[-/= thickness, [ ] resolution]",
demo.thickness_voxels
);
stats.vertices = mc.builder.vertex_count() + sub.builder.vertex_count();
stats.triangles = mc.builder.triangle_count() + sub.builder.triangle_count();
let verdict = if mc.builder.triangle_count() == 0 {
"gone -- no grid edge straddles the sheet"
} else if sub.builder.triangle_count() > 4 * mc.builder.triangle_count().max(1) {
"aliasing -- a holey remnant, not the letters (M-72)"
} else {
"both resolve it; press - to thin the letters"
};
stats.extra = vec![
format!(
"{} samples/axis, {EDGE_SAMPLES} per tet edge [ and ] to change",
demo.samples
),
String::new(),
format!("{:<12} {:>12} {:>12}", "", "cubes", "subgrid"),
format!(
"{:<12} {:>12} {:>12}",
"vertices",
mc.builder.vertex_count(),
sub.builder.vertex_count()
),
format!(
"{:<12} {:>12} {:>12}",
"triangles",
mc.builder.triangle_count(),
sub.builder.triangle_count()
),
format!(
"{:<12} {:>12.2} {:>12.2}",
"extract ms", mc.millis, sub.millis
),
String::new(),
format!("left panel: {verdict}"),
String::new(),
"a sign test gets one bit per edge; subgrid marching gets every zero".into(),
"along it. M-67: a sign test cannot tell apart 95.6% of the".into(),
"configurations a tetrahedron can be in.".into(),
String::new(),
"the cost is real -- M-98 measured 70x classic marching tetrahedra --".into(),
"but the honest comparison is against whatever resolution would".into(),
"resolve the same feature, and below one voxel there is none.".into(),
];
let offset = HALF_EXTENT * 1.25;
let pairs = [
(Side::MarchingCubes, mc.builder, -offset),
(Side::Subgrid, sub.builder, offset),
];
if query.is_empty() {
for (side, builder, x) in pairs {
let material = match side {
Side::MarchingCubes => materials.marching_cubes.clone(),
Side::Subgrid => materials.subgrid.clone(),
};
commands.spawn((
Mesh3d(meshes.add(builder.into_mesh())),
MeshMaterial3d(material),
Transform::from_xyz(x, 0.0, 0.0),
DemoMesh,
side,
));
}
} else {
let built: Vec<(Side, Handle<Mesh>, f32)> = pairs
.into_iter()
.map(|(side, builder, x)| (side, meshes.add(builder.into_mesh()), x))
.collect();
for (mut mesh, mut transform, side) in &mut query {
if let Some((_, handle, x)) = built.iter().find(|(s, _, _)| s == side) {
mesh.0 = handle.clone();
transform.translation.x = *x;
}
}
}
}