effectcraft/crates/model/src/prim.rs

236 lines
8.8 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! Parametric primitives (Layer ▸ New ▸ Cube/Sphere/Plane/Torus/Cone/Cylinder).
//!
//! Meshes are built in model space (+Y up, counter-clockwise front faces, like glTF), centred
//! on the origin, in pixels, with normals, UVs (0..1 across each face/around each surface) and
//! tangents.
use std::f32::consts::{PI, TAU};
use serde::{Deserialize, Serialize};
use crate::{Material, Mesh, Model, Primitive};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum PrimitiveKind {
#[default]
Cube,
Sphere,
Plane,
Torus,
Cone,
Cylinder,
}
impl PrimitiveKind {
pub const ALL: [PrimitiveKind; 6] =
[PrimitiveKind::Cube, PrimitiveKind::Sphere, PrimitiveKind::Plane, PrimitiveKind::Torus, PrimitiveKind::Cone, PrimitiveKind::Cylinder];
pub fn label(self) -> &'static str {
match self {
PrimitiveKind::Cube => "Cube",
PrimitiveKind::Sphere => "Sphere",
PrimitiveKind::Plane => "Plane",
PrimitiveKind::Torus => "Torus",
PrimitiveKind::Cone => "Cone",
PrimitiveKind::Cylinder => "Cylinder",
}
}
pub fn parse(s: &str) -> Option<PrimitiveKind> {
PrimitiveKind::ALL.into_iter().find(|k| k.label().eq_ignore_ascii_case(s.trim()))
}
}
/// Primitive dimensions (pixels) and tessellation.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct PrimParams {
/// Width, height, depth (cube, plane: width × height).
pub size: [f32; 3],
/// Sphere/cone/cylinder radius; torus ring radius.
pub radius: f32,
/// Torus tube radius.
pub tube: f32,
/// Cone/cylinder height.
pub height: f32,
/// Segments around.
pub segments: u32,
/// Sphere rings / torus tube sides.
pub rings: u32,
}
impl Default for PrimParams {
fn default() -> Self {
PrimParams { size: [300.0; 3], radius: 150.0, tube: 50.0, height: 300.0, segments: 48, rings: 24 }
}
}
struct B {
p: Primitive,
}
impl B {
fn new() -> B {
B { p: Primitive { material: Some(0), ..Default::default() } }
}
fn v(&mut self, pos: [f32; 3], n: [f32; 3], uv: [f32; 2]) -> u32 {
self.p.positions.push(pos);
self.p.normals.push(n);
self.p.uvs.push(uv);
(self.p.positions.len() - 1) as u32
}
fn tri(&mut self, a: u32, b: u32, c: u32) {
self.p.indices.extend([a, b, c]);
}
fn quad(&mut self, a: u32, b: u32, c: u32, d: u32) {
// a-b-c-d counter-clockwise.
self.tri(a, b, c);
self.tri(a, c, d);
}
}
/// The mesh of a primitive.
pub fn mesh(kind: PrimitiveKind, pr: &PrimParams) -> Primitive {
let seg = pr.segments.clamp(3, 512);
let rings = pr.rings.clamp(2, 512);
let mut b = B::new();
match kind {
PrimitiveKind::Cube => {
let [w, h, d] = pr.size.map(|v| v.abs() * 0.5);
// (normal, u axis, v axis) per face; v runs down the texture.
let faces: [([f32; 3], [f32; 3], [f32; 3]); 6] = [
([0.0, 0.0, 1.0], [1.0, 0.0, 0.0], [0.0, -1.0, 0.0]),
([0.0, 0.0, -1.0], [-1.0, 0.0, 0.0], [0.0, -1.0, 0.0]),
([1.0, 0.0, 0.0], [0.0, 0.0, -1.0], [0.0, -1.0, 0.0]),
([-1.0, 0.0, 0.0], [0.0, 0.0, 1.0], [0.0, -1.0, 0.0]),
([0.0, 1.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0]),
([0.0, -1.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, -1.0]),
];
let half = [w, h, d];
for (n, u, v) in faces {
let c = |su: f32, sv: f32| -> [f32; 3] { [0, 1, 2].map(|k| (n[k] + u[k] * su + v[k] * sv) * half[k]) };
let i0 = b.v(c(-1.0, -1.0), n, [0.0, 0.0]);
let i1 = b.v(c(1.0, -1.0), n, [1.0, 0.0]);
let i2 = b.v(c(1.0, 1.0), n, [1.0, 1.0]);
let i3 = b.v(c(-1.0, 1.0), n, [0.0, 1.0]);
// u × v points along -n for these axes (v down): wind so the face is CCW from outside.
b.quad(i0, i3, i2, i1);
}
}
PrimitiveKind::Plane => {
let (w, h) = (pr.size[0].abs() * 0.5, pr.size[1].abs() * 0.5);
let n = [0.0, 0.0, 1.0];
let i0 = b.v([-w, h, 0.0], n, [0.0, 0.0]);
let i1 = b.v([w, h, 0.0], n, [1.0, 0.0]);
let i2 = b.v([w, -h, 0.0], n, [1.0, 1.0]);
let i3 = b.v([-w, -h, 0.0], n, [0.0, 1.0]);
b.quad(i0, i3, i2, i1);
}
PrimitiveKind::Sphere => {
let r = pr.radius.abs();
for j in 0..=rings {
let th = PI * j as f32 / rings as f32;
// Exact poles and seam (welded positions).
let (st, ct) = if j == 0 {
(0.0, 1.0)
} else if j == rings {
(0.0, -1.0)
} else {
th.sin_cos()
};
for i in 0..=seg {
let ph = TAU * (i % seg) as f32 / seg as f32;
let n = [st * ph.sin(), ct, st * ph.cos()];
b.v([n[0] * r, n[1] * r, n[2] * r], n, [i as f32 / seg as f32, j as f32 / rings as f32]);
}
}
let row = seg + 1;
for j in 0..rings {
for i in 0..seg {
let a = j * row + i;
let (b0, c, d) = (a + row, a + row + 1, a + 1);
if j != 0 {
b.tri(a, b0, d);
}
if j != rings - 1 {
b.tri(d, b0, c);
}
}
}
}
PrimitiveKind::Torus => {
let (rr, tr) = (pr.radius.abs(), pr.tube.abs());
for j in 0..=rings {
let v = TAU * (j % rings) as f32 / rings as f32;
for i in 0..=seg {
let u = TAU * (i % seg) as f32 / seg as f32;
// Ring in the XZ plane.
let (cu, su, cv, sv) = (u.cos(), u.sin(), v.cos(), v.sin());
let n = [cv * su, sv, cv * cu];
let p = [(rr + tr * cv) * su, tr * sv, (rr + tr * cv) * cu];
b.v(p, n, [i as f32 / seg as f32, j as f32 / rings as f32]);
}
}
let row = seg + 1;
for j in 0..rings {
for i in 0..seg {
let a = j * row + i;
b.quad(a, a + 1, a + row + 1, a + row);
}
}
}
PrimitiveKind::Cylinder | PrimitiveKind::Cone => {
let r = pr.radius.abs();
let h = pr.height.abs() * 0.5;
let top_r = if kind == PrimitiveKind::Cone { 0.0 } else { r };
// Side normal tilt for the cone (slope).
let (ny, nk) = {
let s = (r - top_r) / (2.0 * h).max(1e-6);
let l = (1.0 + s * s).sqrt();
(s / l, 1.0 / l)
};
let base = b.p.positions.len() as u32;
for i in 0..=seg {
let a = TAU * (i % seg) as f32 / seg as f32;
let (s, c) = a.sin_cos();
let n = [s * nk, ny, c * nk];
let u = i as f32 / seg as f32;
b.v([s * top_r, h, c * top_r], n, [u, 0.0]);
b.v([s * r, -h, c * r], n, [u, 1.0]);
}
for i in 0..seg {
let t0 = base + 2 * i;
let (b0, t1, b1) = (t0 + 1, t0 + 2, t0 + 3);
if kind == PrimitiveKind::Cone {
b.tri(t0, b0, b1);
} else {
b.quad(t0, b0, b1, t1);
}
}
let caps: &[(f32, f32)] = if kind == PrimitiveKind::Cone { &[(-1.0, r)] } else { &[(1.0, r), (-1.0, r)] };
for &(side, rad) in caps {
let n = [0.0, side, 0.0];
let c = b.v([0.0, h * side, 0.0], n, [0.5, 0.5]);
let first = b.p.positions.len() as u32;
for i in 0..=seg {
let a = TAU * (i % seg) as f32 / seg as f32;
let (s, co) = a.sin_cos();
b.v([s * rad, h * side, co * rad], n, [0.5 + 0.5 * s, 0.5 - 0.5 * co * side]);
}
for i in 0..seg {
let (p0, p1) = (first + i, first + i + 1);
if side > 0.0 { b.tri(c, p0, p1) } else { b.tri(c, p1, p0) }
}
}
}
}
b.p.compute_tangents();
b.p
}
/// A primitive as a one-mesh model with one material.
pub fn model(kind: PrimitiveKind, pr: &PrimParams, material: Material) -> Model {
let mut material = material;
if kind == PrimitiveKind::Plane {
material.double_sided = true;
}
Model::single(Mesh { name: kind.label().to_string(), primitives: vec![mesh(kind, pr)] }, vec![material])
}