mod common;
use bevy::prelude::*;
use bevy_isomesh::to_bevy_mesh;
use common::{Capture, CommonPlugin, DemoMesh, DemoStats, OrbitCamera, ViewFlags};
use isomesh::fields::{BoxExact, ReferenceField, Sphere, ThinPlate, Torus, csg_difference};
use isomesh::marching_cubes::MarchingCubes;
use isomesh::normals::{NormalStrategy, recompute};
use isomesh::{MeshBuffer, RuntimeShape3, Sdf};
#[derive(Resource)]
struct Demo {
field: usize,
samples: u32,
}
const FIELDS: [&str; 5] = [
"sphere",
"box_exact",
"torus",
"csg_difference",
"thin_plate",
];
const MIN_SAMPLES: u32 = 5;
const MAX_SAMPLES: u32 = 97;
#[derive(Component, Clone, Copy, PartialEq, Eq)]
enum Panel {
Analytic,
CentralDifference,
AreaWeighted,
}
impl Panel {
const ALL: [Self; 3] = [Self::Analytic, Self::CentralDifference, Self::AreaWeighted];
fn label(self) -> &'static str {
match self {
Self::Analytic => "analytic gradient",
Self::CentralDifference => "central diff (step = h)",
Self::AreaWeighted => "area-weighted faces",
}
}
}
fn main() {
App::new()
.add_plugins(DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window {
title: "isomesh — E-113 normal estimation".into(),
..default()
}),
..default()
}))
.add_plugins(CommonPlugin)
.insert_resource(Demo {
field: 3,
samples: common::samples_override().unwrap_or(25),
})
.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.30;
orbit.radius = 9.5;
}
commands.insert_resource(PanelMaterial(materials.add(StandardMaterial {
base_color: Color::srgb(0.87, 0.72, 0.56),
perceptual_roughness: 0.20,
metallic: 0.02,
reflectance: 0.35,
..default()
})));
}
#[derive(Resource)]
struct PanelMaterial(Handle<StandardMaterial>);
fn controls(
keys: Res<ButtonInput<KeyCode>>,
mut demo: ResMut<Demo>,
capture: Res<Capture>,
mut flags: ResMut<ViewFlags>,
) {
if capture.is_active() {
return;
}
for (key, index) in [
(KeyCode::Digit1, 0),
(KeyCode::Digit2, 1),
(KeyCode::Digit3, 2),
(KeyCode::Digit4, 3),
(KeyCode::Digit5, 4),
] {
if keys.just_pressed(key) {
demo.field = index;
}
}
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::KeyR) {
flags.remesh_requested = true;
}
}
struct Panelled {
buffer: MeshBuffer<f32>,
worst: f32,
mean: f32,
}
fn build<F: Sdf<Scalar = f32> + ReferenceField>(
field: &F,
samples: u32,
) -> Option<(Vec<(Panel, Panelled)>, f32)> {
let (min, max) = field.domain();
let cell_size = (max[0] - min[0]) / (samples - 1) as f32;
let shape = RuntimeShape3::new([samples; 3]).ok()?;
let mut base = MeshBuffer::<f32>::new();
MarchingCubes::<f32>::new()
.extract(field, &shape, min, cell_size, &mut base)
.ok()?;
if base.is_empty() {
return None;
}
let reference = base.normals.clone();
let mut out = Vec::new();
for panel in Panel::ALL {
let strategy = match panel {
Panel::Analytic => NormalStrategy::AnalyticGradient,
Panel::CentralDifference => NormalStrategy::CentralDifference { step: cell_size },
Panel::AreaWeighted => NormalStrategy::AreaWeightedFaces,
};
let mut buffer = base.clone();
if let Err(error) = recompute(&mut buffer, field, strategy) {
error!("{}: {error}", panel.label());
return None;
}
let (worst, mean) = deviation(&reference, &buffer.normals);
out.push((
panel,
Panelled {
buffer,
worst,
mean,
},
));
}
Some((out, max[0] - min[0]))
}
fn deviation(a: &[[f32; 3]], b: &[[f32; 3]]) -> (f32, f32) {
let mut worst = 0.0f32;
let mut total = 0.0f32;
for (x, y) in a.iter().zip(b) {
let dot = (x[0] * y[0] + x[1] * y[1] + x[2] * y[2]).clamp(-1.0, 1.0);
let angle = dot.acos().to_degrees();
worst = worst.max(angle);
total += angle;
}
let count = a.len().max(1) as f32;
(worst, total / count)
}
#[allow(clippy::too_many_arguments)]
fn remesh(
demo: Res<Demo>,
mut stats: ResMut<DemoStats>,
mut meshes: ResMut<Assets<Mesh>>,
material: Res<PanelMaterial>,
mut commands: Commands,
mut query: Query<(&mut Mesh3d, &mut Transform, &Panel)>,
mut camera: Query<&mut OrbitCamera>,
mut flags: ResMut<ViewFlags>,
mut last: Local<Option<(usize, u32)>>,
) {
let key = (demo.field, demo.samples);
if *last == Some(key) && !flags.remesh_requested {
return;
}
*last = Some(key);
flags.remesh_requested = false;
let built = match demo.field {
0 => build(&Sphere::<f32>::canonical(), demo.samples),
1 => build(&BoxExact::<f32>::canonical(), demo.samples),
2 => build(&Torus::<f32>::canonical(), demo.samples),
3 => build(&csg_difference::<f32>(), demo.samples),
_ => build(&ThinPlate::<f32>::canonical(), demo.samples),
};
let Some((panels, width)) = built else {
return;
};
let vertices = panels.first().map_or(0, |(_, p)| p.buffer.vertex_count());
let triangles = panels.first().map_or(0, |(_, p)| p.buffer.triangle_count());
stats.title = format!(
"E-113 normal estimation field {} ({}) [1-5] field, [ ] resolution",
demo.field + 1,
FIELDS[demo.field]
);
stats.vertices = vertices;
stats.triangles = triangles;
let mut lines = vec![
format!("{} samples/axis [ and ] to change", demo.samples),
"geometry is identical in all three panels; only the shading differs".into(),
String::new(),
format!("{:<26} {:>9} {:>9}", "strategy", "worst", "mean"),
];
for (panel, p) in &panels {
lines.push(format!(
"{:<26} {:>8.3}d {:>8.3}d",
panel.label(),
p.worst,
p.mean
));
}
lines.push(String::new());
lines.push("degrees from the analytic gradient, left panel to each".into());
stats.extra = lines;
let offset = width * 0.60;
for mut orbit in &mut camera {
orbit.radius = width * 2.35;
}
let placed: Vec<(Panel, Handle<Mesh>, f32)> = panels
.iter()
.enumerate()
.map(|(i, (panel, p))| {
let x = (i as f32 - 1.0) * offset;
(*panel, meshes.add(to_bevy_mesh(&p.buffer)), x)
})
.collect();
if query.is_empty() {
for (panel, handle, x) in placed {
commands.spawn((
Mesh3d(handle),
MeshMaterial3d(material.0.clone()),
Transform::from_xyz(x, 0.0, 0.0),
DemoMesh,
panel,
));
}
} else {
for (mut mesh, mut transform, panel) in &mut query {
if let Some((_, handle, x)) = placed.iter().find(|(p, _, _)| p == panel) {
mesh.0 = handle.clone();
transform.translation.x = *x;
}
}
}
}