primitive meshes
This commit is contained in:
@@ -6,6 +6,22 @@ edition = "2024"
|
||||
[lib]
|
||||
path = "src/lib.rs"
|
||||
|
||||
[features]
|
||||
default = ["all-prims"]
|
||||
|
||||
# Primitives procédurales (zéro dep externe)
|
||||
prim-cube = []
|
||||
prim-plane = []
|
||||
prim-sphere = []
|
||||
prim-cylinder = []
|
||||
prim-cone = []
|
||||
prim-torus = []
|
||||
all-prims = ["prim-cube", "prim-plane", "prim-sphere", "prim-cylinder", "prim-cone", "prim-torus"]
|
||||
|
||||
# Import de fichiers
|
||||
import-obj = []
|
||||
import-gltf = []
|
||||
|
||||
[dependencies]
|
||||
wgpu = "30.0.0" # Vérifiez la version la plus récente
|
||||
winit = "0.30.13" # For window management — pinned to match examples
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
use glam::{Quat, Vec3};
|
||||
use wsg_lib::AppHandler;
|
||||
use wsg_lib::app::AppBuilder;
|
||||
use wsg_lib::math::cube;
|
||||
use wsg_lib::mesh::cube;
|
||||
use wsg_lib::resources::Texture;
|
||||
use wsg_lib::utils::WsgError;
|
||||
|
||||
|
||||
@@ -32,7 +32,8 @@ use winit::keyboard::KeyCode;
|
||||
use wsg_lib::AppHandler;
|
||||
use wsg_lib::app::AppBuilder;
|
||||
use wsg_lib::core::ToneMapper;
|
||||
use wsg_lib::math::{Transform, cone, cube, cylinder, icosphere, plane, torus, uv_sphere};
|
||||
use wsg_lib::core::Transform;
|
||||
use wsg_lib::mesh::{cone, cube, cylinder, icosphere, plane, torus, uv_sphere};
|
||||
use wsg_lib::resources::{CameraController, Texture};
|
||||
use wsg_lib::utils::WsgError;
|
||||
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
//! # Example: File Import (OBJ)
|
||||
//!
|
||||
//! Demonstrates loading a Wavefront OBJ file with `wsg_lib::mesh::load_obj`.
|
||||
//! Parses the file and prints geometry statistics.
|
||||
//!
|
||||
//! ## Build & Run
|
||||
//! ```sh
|
||||
//! cargo run -p wsg-lib --example import --features import-obj -- /path/to/model.obj
|
||||
//! ```
|
||||
//!
|
||||
//! Without a file argument, parses a built-in sample triangle.
|
||||
|
||||
use wsg_lib::mesh::import::parse_obj;
|
||||
use wsg_lib::mesh::load_obj;
|
||||
|
||||
fn main() {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
|
||||
let content = if args.len() > 1 {
|
||||
let path = &args[1];
|
||||
eprintln!("Loading: {path}");
|
||||
match load_obj(path) {
|
||||
Ok(geom) => {
|
||||
print_stats(&geom);
|
||||
return;
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Error: {e}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
eprintln!("No file argument — parsing a built-in sample.");
|
||||
eprintln!("Usage: import <model.obj>");
|
||||
// Built-in sample: a simple triangle with UVs and normals
|
||||
"v 0.0 0.0 0.0\nv 1.0 0.0 0.0\nv 0.5 1.0 0.0\nvn 0 0 1\nvt 0.0 0.0\nvt 1.0 0.0\nvt 0.5 1.0\nf 1/1/1 2/2/1 3/3/1\n"
|
||||
};
|
||||
|
||||
let geom = parse_obj(content).expect("sample should parse");
|
||||
print_stats(&geom);
|
||||
}
|
||||
|
||||
fn print_stats(geom: &wsg_lib::Geometry) {
|
||||
println!("\n=== Geometry Statistics ===");
|
||||
println!(" Vertices: {}", geom.positions.len());
|
||||
if let Some(n) = &geom.normals {
|
||||
println!(" Normals: {}", n.len());
|
||||
}
|
||||
if let Some(uv) = &geom.uvs {
|
||||
println!(" UVs: {}", uv.len());
|
||||
}
|
||||
if let Some(idx) = &geom.indices {
|
||||
println!(" Indices: {} ({} triangles)", idx.len(), idx.len() / 3);
|
||||
}
|
||||
if let Err(e) = geom.validate() {
|
||||
println!(" Validation FAILED: {e}");
|
||||
} else {
|
||||
println!(" Validation: OK");
|
||||
}
|
||||
// Bounding box
|
||||
if let Some(bbox) = geom.bbox() {
|
||||
println!(" BBox min: {:?}", bbox.min);
|
||||
println!(" BBox max: {:?}", bbox.max);
|
||||
}
|
||||
println!();
|
||||
}
|
||||
@@ -104,7 +104,7 @@ impl wsg_lib::AppHandler for ShadowTest {
|
||||
.add_entity_with_transform(
|
||||
"ground",
|
||||
"ground_mesh",
|
||||
wsg_lib::math::Transform::identity(),
|
||||
wsg_lib::core::Transform::identity(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
@@ -112,7 +112,7 @@ impl wsg_lib::AppHandler for ShadowTest {
|
||||
app.scene
|
||||
.create_mesh("cube_mesh", box_geometry(0.5, 0.5, 0.5), Some("mat"))
|
||||
.unwrap();
|
||||
let mut cube_tf = wsg_lib::math::Transform::identity();
|
||||
let mut cube_tf = wsg_lib::core::Transform::identity();
|
||||
cube_tf.translation = Vec3::new(0.0, 0.5, 0.0);
|
||||
app.scene
|
||||
.add_entity_with_transform("cube", "cube_mesh", cube_tf)
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
use glam::{Quat, Vec3};
|
||||
use wsg_lib::AppHandler;
|
||||
use wsg_lib::app::AppBuilder;
|
||||
use wsg_lib::math::cube;
|
||||
use wsg_lib::mesh::cube;
|
||||
use wsg_lib::utils::WsgError;
|
||||
|
||||
/// Test handler: cube rotating slowly on two axes, lit **only** by a spot.
|
||||
|
||||
@@ -182,7 +182,7 @@ impl Geometry {
|
||||
/// (normals, UVs, colors, indices) start as `None`.
|
||||
/// Chain builder methods to populate them:
|
||||
/// ```
|
||||
/// # use wsg_lib::math::Geometry;
|
||||
/// # use wsg_lib::core::Geometry;
|
||||
/// let geo = Geometry::new(vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]])
|
||||
/// .with_normals(vec![[0.0, 0.0, 1.0], [0.0, 0.0, 1.0]])
|
||||
/// .with_indices(vec![0, 1]);
|
||||
@@ -1511,7 +1511,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn decimated_icosahedron_stays_closed() {
|
||||
use crate::math::primitives;
|
||||
use crate::mesh::primitives;
|
||||
let ico = primitives::icosphere(1.0, 0); // 12 vertices / 20 faces, closed
|
||||
assert!(is_closed(&ico), "input is closed");
|
||||
let out = ico.decimated(10);
|
||||
@@ -1531,7 +1531,7 @@ mod tests {
|
||||
fn decimated_uv_sphere_keeps_its_caps() {
|
||||
// Regression (user report): the old triangle-removal decimation removed the pole
|
||||
// triangles first (they are the smallest) → missing caps and holes.
|
||||
use crate::math::primitives;
|
||||
use crate::mesh::primitives;
|
||||
let sph = primitives::uv_sphere(1.0, 8, 6);
|
||||
let t = sph.num_triangles() as u32;
|
||||
assert!(is_closed(&sph), "input is closed");
|
||||
@@ -1571,7 +1571,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn decimated_torus_stays_closed() {
|
||||
use crate::math::primitives;
|
||||
use crate::mesh::primitives;
|
||||
let torus = primitives::torus(1.0, 0.4, 16, 12);
|
||||
let t = torus.num_triangles() as u32;
|
||||
assert!(is_closed(&torus), "input is closed");
|
||||
@@ -1635,7 +1635,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn decimated_is_deterministic() {
|
||||
use crate::math::primitives;
|
||||
use crate::mesh::primitives;
|
||||
let ico = primitives::icosphere(1.0, 1); // 80 equal-area faces
|
||||
let a = ico.decimated(30);
|
||||
let b = ico.decimated(30);
|
||||
@@ -1712,7 +1712,7 @@ mod tests {
|
||||
/// the inward-wound faces and pointed INWARD, deviation ≈ 2.0, far LOD looked flat).
|
||||
#[test]
|
||||
fn decimated_sphere_uv_and_normal_error() {
|
||||
use crate::math::primitives;
|
||||
use crate::mesh::primitives;
|
||||
let r = 0.55;
|
||||
let geo = primitives::uv_sphere(r, 32, 20);
|
||||
let levels = geo.generate_lod_levels(3);
|
||||
@@ -1774,7 +1774,7 @@ mod tests {
|
||||
// freezing the apex UV onto base vertices); the cone has TWO charts (side fan +
|
||||
// cap disc) and two slit columns at u = 0 / u = 1, so each decimated vertex is
|
||||
// checked against BOTH analytical maps and must follow at least one.
|
||||
use crate::math::primitives;
|
||||
use crate::mesh::primitives;
|
||||
let cone = primitives::cone(1.0, 1.0, 16);
|
||||
let levels = cone.generate_lod_levels(3);
|
||||
let udist = |a: f32, b: f32| {
|
||||
@@ -1957,7 +1957,7 @@ mod tests {
|
||||
/// duplicates separate, so decimating must never blend the charts together.
|
||||
#[test]
|
||||
fn decimated_cylinder_keeps_charts() {
|
||||
use crate::math::primitives;
|
||||
use crate::mesh::primitives;
|
||||
let cyl = primitives::cylinder(0.4, 0.9, 8);
|
||||
let out = cyl.decimated(16);
|
||||
assert!(out.validate().is_ok());
|
||||
@@ -2064,7 +2064,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn generate_lod_levels_halving_ratios() {
|
||||
use crate::math::primitives;
|
||||
use crate::mesh::primitives;
|
||||
let geo = primitives::icosphere(1.0, 1); // 80 triangles
|
||||
let levels = geo.generate_lod_levels(3);
|
||||
assert_eq!(levels.len(), 3);
|
||||
@@ -2084,7 +2084,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn generate_lod_levels_one_level() {
|
||||
use crate::math::primitives;
|
||||
use crate::mesh::primitives;
|
||||
let geo = primitives::icosphere(1.0, 1);
|
||||
let levels = geo.generate_lod_levels(1);
|
||||
assert_eq!(levels.len(), 1);
|
||||
@@ -11,15 +11,23 @@
|
||||
|
||||
pub mod context;
|
||||
pub mod frame;
|
||||
pub mod frustum;
|
||||
pub mod geometry;
|
||||
pub mod hdr;
|
||||
pub mod input;
|
||||
pub mod lod;
|
||||
pub mod renderer;
|
||||
pub mod shadow;
|
||||
pub mod transform;
|
||||
|
||||
// Re-exports
|
||||
pub use context::Context;
|
||||
pub use frame::Frame;
|
||||
pub use frustum::Frustum;
|
||||
pub use geometry::{BBox, Geometry, GeometryError};
|
||||
pub use hdr::ToneMapper;
|
||||
pub use input::InputState;
|
||||
pub use lod::{lod_level, projected_radius_px};
|
||||
pub use renderer::Renderer;
|
||||
pub use shadow::ShadowConfig;
|
||||
pub use transform::Transform;
|
||||
|
||||
@@ -21,8 +21,8 @@
|
||||
|
||||
use crate::core::Context;
|
||||
use crate::core::Frame;
|
||||
use crate::math::Frustum;
|
||||
use crate::math::lod::{lod_level, projected_radius_px};
|
||||
use crate::core::Frustum;
|
||||
use crate::core::lod::{lod_level, projected_radius_px};
|
||||
use crate::pipeline::{
|
||||
DEPTH_FORMAT, build_shadow_pipeline, create_shadow_map_bind_group_layout,
|
||||
create_shadow_uniform_layout, create_uniform_bind_group_layouts,
|
||||
|
||||
+15
-5
@@ -1,9 +1,9 @@
|
||||
//! # WSG Library Crate Root
|
||||
//!
|
||||
//! The top-level entry point for the wsg-lib crate. Exposes seven public modules organized by architectural responsibility:
|
||||
//! **app** (App facade + AppBuilder), **handler** (AppHandler trait), **core** (Manager + Executor layers),
|
||||
//! **resources** (data types), **pipeline** (shader compilation cache), **scene** (resource graph and entity management),
|
||||
//! and **utils** (configuration and error handling).
|
||||
//! The top-level entry point for the wsg-lib crate. Exposes eight public modules organized by architectural responsibility:
|
||||
//! **app** (App facade + AppBuilder), **handler** (AppHandler trait), **core** (Manager + Executor + geometry types),
|
||||
//! **mesh** (geometry sources: primitives + import), **resources** (data types), **pipeline** (shader compilation cache),
|
||||
//! **scene** (resource graph and entity management), **prelude** (glob re-exports), and **utils** (configuration and error handling).
|
||||
//!
|
||||
//! ## Module Interaction Map
|
||||
//! - `core` consumes resources from `resources`, pipelines from `pipeline`, and errors from `utils`.
|
||||
@@ -31,8 +31,9 @@
|
||||
pub mod app;
|
||||
pub mod core;
|
||||
pub mod handler;
|
||||
pub mod math;
|
||||
pub mod mesh;
|
||||
pub mod pipeline;
|
||||
pub mod prelude;
|
||||
pub mod resources;
|
||||
pub mod scene;
|
||||
pub mod utils;
|
||||
@@ -52,3 +53,12 @@ pub use crate::core::ShadowConfig;
|
||||
/// Re-export of the tone mapping curve selector for convenient top-level access.
|
||||
/// Users enable HDR via `AppBuilder::with_hdr(ToneMapper::Aces)`.
|
||||
pub use crate::core::ToneMapper;
|
||||
|
||||
/// Re-export of the geometry data type (positions, normals, UVs, indices).
|
||||
pub use crate::core::Geometry;
|
||||
|
||||
/// Re-export of the per-entity transform (position + rotation + scale).
|
||||
pub use crate::core::Transform;
|
||||
|
||||
/// Re-export of the axis-aligned bounding box.
|
||||
pub use crate::core::BBox;
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
//! # Math Module — Geometric and Transformation Utilities
|
||||
//!
|
||||
//! Provides core mathematical types and utilities for 3D graphics operations, including:
|
||||
//! - `Transform` for object positioning, rotation, and scaling
|
||||
//! - `Geometry` for mesh vertex data representation
|
||||
//! - `primitives` for procedural mesh generators (cube, plane, sphere, cylinder, cone, torus)
|
||||
//!
|
||||
//! ## Interaction with Other Modules
|
||||
//! - `scene::Scene` uses `Transform` to manage entity positions
|
||||
//! - `renderer::Renderer` uses `Transform` to compute world matrices for shaders
|
||||
//! - `resources::Mesh` stores vertex data in `Geometry` format
|
||||
//! - `resources::Camera` (view/projection matrices) lives in the `resources` module
|
||||
//!
|
||||
//! ## Files
|
||||
//! - `transform.rs`: Defines the `Transform` struct and its conversion to matrix form
|
||||
//! - `geometry.rs`: Defines the `Geometry` struct for mesh data storage
|
||||
//! - `primitives.rs`: Procedural mesh generators (cube, sphere, cylinder, cone, torus…) returning `Geometry`
|
||||
//! - `lod.rs`: Pure LOD level-selection functions (projected radius + hysteresis, Step 19)
|
||||
|
||||
pub mod frustum;
|
||||
pub mod geometry;
|
||||
pub mod lod;
|
||||
pub mod primitives;
|
||||
pub mod transform;
|
||||
|
||||
// Re-exports
|
||||
pub use frustum::Frustum;
|
||||
pub use geometry::{BBox, Geometry, GeometryError};
|
||||
pub use lod::{lod_level, projected_radius_px};
|
||||
pub use primitives::{cone, cube, cylinder, icosphere, plane, torus, uv_sphere};
|
||||
pub use transform::Transform;
|
||||
@@ -1,534 +0,0 @@
|
||||
//! # Primitives Module — Ready-to-use geometry meshes (Step 15, ROADMAP 2.2)
|
||||
//!
|
||||
//! Procedural `Geometry` generators for common 3D shapes, usable directly in WSG
|
||||
//! without importing wgpu: `cube`, `plane`, `uv_sphere`, `icosphere`,
|
||||
//! `cylinder`, `cone` (and `torus` as a bonus).
|
||||
//!
|
||||
//! ## Conventions
|
||||
//! - **Y-up** axis, origin-centered (except `plane`, which lies in the XZ plane around 0).
|
||||
//! - Normals **pointing outward** (meaningful for Phong lighting; culling stays disabled
|
||||
//! by default).
|
||||
//! - UVs in [0,1]², as continuous as possible; `uv_sphere`/`icosphere` project from
|
||||
//! spherical coordinates.
|
||||
//! - Each generator returns a **complete** `Geometry` (positions + normals + UVs +
|
||||
//! indices, no colors → opaque white default via `Geometry::to_vertices`).
|
||||
//!
|
||||
//! ## Invariant
|
||||
//! Every produced geometry passes `Geometry::validate()` without error (checked by the tests).
|
||||
|
||||
use crate::math::Geometry;
|
||||
use glam::Vec3;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Generates a cube centered at the origin, edge `size`, with one normal and UVs per face.
|
||||
/// 24 vertices (4 per face) + 36 indices. Replicates exactly the historical `cube_geometry` of
|
||||
/// the examples (Step 5/10) to guarantee non-regression.
|
||||
pub fn cube(size: f32) -> Geometry {
|
||||
let s = size * 0.5; // half edge
|
||||
let faces: [([f32; 3], [[f32; 3]; 4]); 6] = [
|
||||
(
|
||||
[0.0, 0.0, 1.0],
|
||||
[[-s, -s, s], [s, -s, s], [s, s, s], [-s, s, s]],
|
||||
), // +Z
|
||||
(
|
||||
[0.0, 0.0, -1.0],
|
||||
[[s, -s, -s], [-s, -s, -s], [-s, s, -s], [s, s, -s]],
|
||||
), // -Z
|
||||
(
|
||||
[1.0, 0.0, 0.0],
|
||||
[[s, -s, -s], [s, s, -s], [s, s, s], [s, -s, s]],
|
||||
), // +X
|
||||
(
|
||||
[-1.0, 0.0, 0.0],
|
||||
[[-s, -s, s], [-s, s, s], [-s, s, -s], [-s, -s, -s]],
|
||||
), // -X
|
||||
(
|
||||
[0.0, 1.0, 0.0],
|
||||
[[-s, s, -s], [s, s, -s], [s, s, s], [-s, s, s]],
|
||||
), // +Y
|
||||
(
|
||||
[0.0, -1.0, 0.0],
|
||||
[[-s, -s, s], [s, -s, s], [s, -s, -s], [-s, -s, -s]],
|
||||
), // -Y
|
||||
];
|
||||
|
||||
let mut positions = Vec::with_capacity(24);
|
||||
let mut normals = Vec::with_capacity(24);
|
||||
let mut uvs = Vec::with_capacity(24);
|
||||
let quad_uvs = [[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]];
|
||||
for (normal, corners) in faces {
|
||||
for (i, corner) in corners.iter().enumerate() {
|
||||
positions.push(*corner);
|
||||
normals.push(normal);
|
||||
uvs.push(quad_uvs[i]);
|
||||
}
|
||||
}
|
||||
|
||||
let mut indices = Vec::with_capacity(36);
|
||||
for face in 0..6u16 {
|
||||
let b = face * 4;
|
||||
indices.extend_from_slice(&[b, b + 1, b + 2, b, b + 2, b + 3]);
|
||||
}
|
||||
|
||||
Geometry::new(positions)
|
||||
.with_normals(normals)
|
||||
.with_uvs(uvs)
|
||||
.with_indices(indices)
|
||||
}
|
||||
|
||||
/// Generates a horizontal plane in the XZ plane (normal +Y), centered at (0, 0, 0), with
|
||||
/// `width` × `depth` dimensions, subdivided into `seg_x` × `seg_z` cells. UVs stretched over [0,1]².
|
||||
pub fn plane(width: f32, depth: f32, seg_x: u32, seg_z: u32) -> Geometry {
|
||||
let sx = seg_x.max(1);
|
||||
let sz = seg_z.max(1);
|
||||
let (mut positions, mut normals, mut uvs, mut indices) = (
|
||||
Vec::<[f32; 3]>::new(),
|
||||
Vec::<[f32; 3]>::new(),
|
||||
Vec::<[f32; 2]>::new(),
|
||||
Vec::<u16>::new(),
|
||||
);
|
||||
for z in 0..=sz {
|
||||
let vz = z as f32 / sz as f32;
|
||||
for x in 0..=sx {
|
||||
let vx = x as f32 / sx as f32;
|
||||
positions.push([(vx - 0.5) * width, 0.0, (vz - 0.5) * depth]);
|
||||
normals.push([0.0, 1.0, 0.0]);
|
||||
uvs.push([vx, vz]);
|
||||
}
|
||||
}
|
||||
for z in 0..sz {
|
||||
for x in 0..sx {
|
||||
let a = z * (sx + 1) + x;
|
||||
let b = a + 1;
|
||||
let c = (z + 1) * (sx + 1) + x;
|
||||
let d = c + 1;
|
||||
indices
|
||||
.extend_from_slice(&[a as u16, c as u16, b as u16, b as u16, c as u16, d as u16]);
|
||||
}
|
||||
}
|
||||
Geometry::new(positions)
|
||||
.with_normals(normals)
|
||||
.with_uvs(uvs)
|
||||
.with_indices(indices)
|
||||
}
|
||||
|
||||
/// Generates a UV (latitude/longitude) sphere of radius `radius`, with `sectors` segments around
|
||||
/// and `stacks` vertical rings. Smooth normals = normalized position; spherical UVs.
|
||||
pub fn uv_sphere(radius: f32, sectors: u32, stacks: u32) -> Geometry {
|
||||
let si = sectors.max(3);
|
||||
let st = stacks.max(3);
|
||||
let (mut positions, mut normals, mut uvs, mut indices) = (
|
||||
Vec::<[f32; 3]>::new(),
|
||||
Vec::<[f32; 3]>::new(),
|
||||
Vec::<[f32; 2]>::new(),
|
||||
Vec::<u16>::new(),
|
||||
);
|
||||
for stack in 0..=st {
|
||||
let v = stack as f32 / st as f32;
|
||||
let phi = v * std::f32::consts::PI;
|
||||
for sector in 0..=si {
|
||||
let u = sector as f32 / si as f32;
|
||||
let theta = u * 2.0 * std::f32::consts::PI;
|
||||
let (sin_p, cos_p) = phi.sin_cos();
|
||||
let (sin_t, cos_t) = theta.sin_cos();
|
||||
let pos = Vec3::new(
|
||||
radius * sin_p * cos_t,
|
||||
radius * cos_p,
|
||||
radius * sin_p * sin_t,
|
||||
);
|
||||
positions.push(pos.to_array());
|
||||
normals.push(pos.normalize().to_array());
|
||||
uvs.push([u, v]);
|
||||
}
|
||||
}
|
||||
for stack in 0..st {
|
||||
for sector in 0..si {
|
||||
let k1 = stack * (si + 1) + sector;
|
||||
let k2 = k1 + si + 1;
|
||||
let (k1, k2) = (k1 as u16, k2 as u16);
|
||||
indices.extend_from_slice(&[k1, k2, k1 + 1, k1 + 1, k2, k2 + 1]);
|
||||
}
|
||||
}
|
||||
Geometry::new(positions)
|
||||
.with_normals(normals)
|
||||
.with_uvs(uvs)
|
||||
.with_indices(indices)
|
||||
}
|
||||
|
||||
/// Generates an icosphere (subdivided icosahedron) of radius `radius`. `subdivisions = 0` gives
|
||||
/// an icosahedron (12 vertices / 20 faces / 60 indices); each subdivision refines the faces into 4.
|
||||
/// Smooth normals = position direction; spherical UVs (a seam is unavoidable without a UV
|
||||
/// atlas).
|
||||
pub fn icosphere(radius: f32, subdivisions: u32) -> Geometry {
|
||||
let t = (1.0 + 5.0_f32.sqrt()) * 0.5;
|
||||
// 12 unit vertices (canonical icosahedron).
|
||||
let mut positions: Vec<Vec3> = [
|
||||
[-1.0, t, 0.0],
|
||||
[1.0, t, 0.0],
|
||||
[-1.0, -t, 0.0],
|
||||
[1.0, -t, 0.0],
|
||||
[0.0, -1.0, t],
|
||||
[0.0, 1.0, t],
|
||||
[0.0, -1.0, -t],
|
||||
[0.0, 1.0, -t],
|
||||
[t, 0.0, -1.0],
|
||||
[t, 0.0, 1.0],
|
||||
[-t, 0.0, -1.0],
|
||||
[-t, 0.0, 1.0],
|
||||
]
|
||||
.iter()
|
||||
.map(|v| Vec3::from_array(*v).normalize())
|
||||
.collect();
|
||||
|
||||
let mut faces: Vec<[u32; 3]> = [
|
||||
[0, 11, 5],
|
||||
[0, 5, 1],
|
||||
[0, 1, 7],
|
||||
[0, 7, 10],
|
||||
[0, 10, 11],
|
||||
[1, 5, 9],
|
||||
[5, 11, 4],
|
||||
[11, 10, 2],
|
||||
[10, 7, 6],
|
||||
[7, 1, 8],
|
||||
[3, 9, 4],
|
||||
[3, 4, 2],
|
||||
[3, 2, 6],
|
||||
[3, 6, 8],
|
||||
[3, 8, 9],
|
||||
[4, 9, 5],
|
||||
[2, 4, 11],
|
||||
[6, 2, 10],
|
||||
[8, 6, 7],
|
||||
[9, 8, 1],
|
||||
]
|
||||
.into_iter()
|
||||
.map(|[a, b, c]| [a, b, c])
|
||||
.collect();
|
||||
|
||||
for _ in 0..subdivisions {
|
||||
let mut midpoint = HashMap::new();
|
||||
let old_faces = std::mem::take(&mut faces);
|
||||
for [a, b, c] in old_faces {
|
||||
let ab = subdiv_midpoint(&mut positions, &mut midpoint, a, b);
|
||||
let bc = subdiv_midpoint(&mut positions, &mut midpoint, b, c);
|
||||
let ca = subdiv_midpoint(&mut positions, &mut midpoint, c, a);
|
||||
faces.push([a, ab, ca]);
|
||||
faces.push([ab, b, bc]);
|
||||
faces.push([ca, bc, c]);
|
||||
faces.push([ab, bc, ca]);
|
||||
}
|
||||
}
|
||||
|
||||
// Scale to the radius + normals (unit direction) + spherical UVs.
|
||||
let mut normals = Vec::with_capacity(positions.len());
|
||||
let mut uvs = Vec::with_capacity(positions.len());
|
||||
for p in &positions {
|
||||
let dir = p.normalize();
|
||||
normals.push(dir.to_array());
|
||||
uvs.push(spherical_uv(dir));
|
||||
}
|
||||
let scaled: Vec<[f32; 3]> = positions.iter().map(|p| (*p * radius).to_array()).collect();
|
||||
|
||||
let mut indices = Vec::with_capacity(faces.len() * 3);
|
||||
for [a, b, c] in &faces {
|
||||
indices.extend_from_slice(&[*a as u16, *b as u16, *c as u16]);
|
||||
}
|
||||
Geometry::new(scaled)
|
||||
.with_normals(normals)
|
||||
.with_uvs(uvs)
|
||||
.with_indices(indices)
|
||||
}
|
||||
|
||||
/// Creates (or retrieves) the normalized midpoint between `a` and `b`, pushed onto the unit sphere.
|
||||
fn subdiv_midpoint(
|
||||
positions: &mut Vec<Vec3>,
|
||||
cache: &mut HashMap<(u32, u32), u32>,
|
||||
a: u32,
|
||||
b: u32,
|
||||
) -> u32 {
|
||||
let key = if a < b { (a, b) } else { (b, a) };
|
||||
if let Some(&i) = cache.get(&key) {
|
||||
return i;
|
||||
}
|
||||
let mid = (positions[a as usize] + positions[b as usize]).normalize();
|
||||
positions.push(mid);
|
||||
let i = (positions.len() - 1) as u32;
|
||||
cache.insert(key, i);
|
||||
i
|
||||
}
|
||||
|
||||
/// Spherical UV from a unit direction, in [0,1]².
|
||||
fn spherical_uv(dir: Vec3) -> [f32; 2] {
|
||||
let u = 0.5 + (dir.z.atan2(dir.x) / (2.0 * std::f32::consts::PI));
|
||||
let v = 0.5 - (dir.y.asin() / std::f32::consts::PI);
|
||||
[u, v]
|
||||
}
|
||||
|
||||
/// Generates a cylinder of radius `radius` and height `height` (along Y, centered), with
|
||||
/// `sectors` segments. Parts: side (smooth radial normals), top cap (+Y), bottom base
|
||||
/// (-Y). Side UVs stretched over [0,1]², concentric rings merged on the caps.
|
||||
pub fn cylinder(radius: f32, height: f32, sectors: u32) -> Geometry {
|
||||
let si = sectors.max(3);
|
||||
let h = height * 0.5;
|
||||
let (mut positions, mut normals, mut uvs, mut indices) = (
|
||||
Vec::<[f32; 3]>::new(),
|
||||
Vec::<[f32; 3]>::new(),
|
||||
Vec::<[f32; 2]>::new(),
|
||||
Vec::<u16>::new(),
|
||||
);
|
||||
|
||||
// Side: radial columns × 2 rows (bottom/top).
|
||||
let side_base = 0u16;
|
||||
for row in 0..=1 {
|
||||
let y = if row == 0 { -h } else { h };
|
||||
for s in 0..=si {
|
||||
let u = s as f32 / si as f32;
|
||||
let theta = u * 2.0 * std::f32::consts::PI;
|
||||
let (sin_t, cos_t) = theta.sin_cos();
|
||||
let radial = Vec3::new(cos_t, 0.0, sin_t);
|
||||
positions.push((radial * radius + Vec3::new(0.0, y, 0.0)).to_array());
|
||||
normals.push(radial.to_array());
|
||||
uvs.push([u, row as f32]);
|
||||
}
|
||||
}
|
||||
for s in 0..si {
|
||||
let a = side_base + s as u16;
|
||||
let b = a + 1;
|
||||
let c = side_base + (si as u16) + 1 + s as u16;
|
||||
let d = c + 1;
|
||||
indices.extend_from_slice(&[a, c, b, b, c, d]);
|
||||
}
|
||||
|
||||
// Caps: center + ring at each end.
|
||||
for (y, normal) in [(h, [0.0, 1.0, 0.0]), (-h, [0.0, -1.0, 0.0])] {
|
||||
let center = positions.len() as u16;
|
||||
positions.push([0.0, y, 0.0]);
|
||||
normals.push(normal);
|
||||
uvs.push([0.5, 0.5]);
|
||||
let ring_start = positions.len() as u16;
|
||||
for s in 0..=si {
|
||||
let u = s as f32 / si as f32;
|
||||
let theta = u * 2.0 * std::f32::consts::PI;
|
||||
let (sin_t, cos_t) = theta.sin_cos();
|
||||
positions.push([radius * cos_t, y, radius * sin_t]);
|
||||
normals.push(normal);
|
||||
uvs.push([0.5 + 0.5 * cos_t, 0.5 + 0.5 * sin_t]);
|
||||
}
|
||||
for s in 0..si {
|
||||
let a = ring_start + s as u16;
|
||||
indices.extend_from_slice(&[center, a + 1, a]);
|
||||
}
|
||||
}
|
||||
|
||||
Geometry::new(positions)
|
||||
.with_normals(normals)
|
||||
.with_uvs(uvs)
|
||||
.with_indices(indices)
|
||||
}
|
||||
|
||||
/// Generates a cone of radius `radius` and height `height` (apex at +h/2, base at -h/2), closed by a
|
||||
/// base, with `sectors` segments. Analytical side normals (tilted outward);
|
||||
/// base normal −Y.
|
||||
pub fn cone(radius: f32, height: f32, sectors: u32) -> Geometry {
|
||||
let si = sectors.max(3);
|
||||
let h = height * 0.5;
|
||||
let (mut positions, mut normals, mut uvs, mut indices) = (
|
||||
Vec::<[f32; 3]>::new(),
|
||||
Vec::<[f32; 3]>::new(),
|
||||
Vec::<[f32; 2]>::new(),
|
||||
Vec::<u16>::new(),
|
||||
);
|
||||
|
||||
// Side elements: apex + base ring.
|
||||
let apex = 0u16;
|
||||
positions.push([0.0, h, 0.0]);
|
||||
normals.push([0.0, 1.0, 0.0]); // shared apex; normal close to +Y by default
|
||||
uvs.push([0.5, 1.0]);
|
||||
let base_start = 1u16;
|
||||
for s in 0..=si {
|
||||
let u = s as f32 / si as f32;
|
||||
let theta = u * 2.0 * std::f32::consts::PI;
|
||||
let (sin_t, cos_t) = theta.sin_cos();
|
||||
positions.push([radius * cos_t, -h, radius * sin_t]);
|
||||
// Side normal: normalize(h·cosθ, r, h·sinθ).
|
||||
let n = Vec3::new(h * cos_t, radius, h * sin_t).normalize();
|
||||
normals.push(n.to_array());
|
||||
uvs.push([u, 0.0]);
|
||||
}
|
||||
for s in 0..si {
|
||||
indices.extend_from_slice(&[apex, base_start + s as u16 + 1, base_start + s as u16]);
|
||||
}
|
||||
|
||||
// Closed base (circle at -h/2, normal -Y).
|
||||
let center = positions.len() as u16;
|
||||
positions.push([0.0, -h, 0.0]);
|
||||
normals.push([0.0, -1.0, 0.0]);
|
||||
uvs.push([0.5, 0.5]);
|
||||
let ring = positions.len() as u16;
|
||||
for s in 0..=si {
|
||||
let u = s as f32 / si as f32;
|
||||
let theta = u * 2.0 * std::f32::consts::PI;
|
||||
let (sin_t, cos_t) = theta.sin_cos();
|
||||
positions.push([radius * cos_t, -h, radius * sin_t]);
|
||||
normals.push([0.0, -1.0, 0.0]);
|
||||
uvs.push([0.5 + 0.5 * cos_t, 0.5 + 0.5 * sin_t]);
|
||||
}
|
||||
for s in 0..si {
|
||||
let r = ring + s as u16;
|
||||
indices.extend_from_slice(&[center, r, r + 1]);
|
||||
}
|
||||
|
||||
Geometry::new(positions)
|
||||
.with_normals(normals)
|
||||
.with_uvs(uvs)
|
||||
.with_indices(indices)
|
||||
}
|
||||
|
||||
/// Generates a torus (ring) with major radius `major` (tube center) and minor radius `minor`
|
||||
/// (tube radius), subdivided into `major_segments` × `minor_segments`. Smooth normals (tube
|
||||
/// direction); UVs [0,1]² (seam along the tube meridian and equator).
|
||||
pub fn torus(major: f32, minor: f32, major_segments: u32, minor_segments: u32) -> Geometry {
|
||||
let mj = major_segments.max(3);
|
||||
let mn = minor_segments.max(3);
|
||||
let (mut positions, mut normals, mut uvs, mut indices) = (
|
||||
Vec::<[f32; 3]>::new(),
|
||||
Vec::<[f32; 3]>::new(),
|
||||
Vec::<[f32; 2]>::new(),
|
||||
Vec::<u16>::new(),
|
||||
);
|
||||
for i in 0..=mj {
|
||||
let u = i as f32 / mj as f32;
|
||||
let ua = u * 2.0 * std::f32::consts::PI;
|
||||
let (sin_u, cos_u) = ua.sin_cos();
|
||||
for j in 0..=mn {
|
||||
let v = j as f32 / mn as f32;
|
||||
let va = v * 2.0 * std::f32::consts::PI;
|
||||
let (sin_v, cos_v) = va.sin_cos();
|
||||
let ring = Vec3::new(
|
||||
(major + minor * cos_v) * cos_u,
|
||||
minor * sin_v,
|
||||
(major + minor * cos_v) * sin_u,
|
||||
);
|
||||
positions.push(ring.to_array());
|
||||
let n = Vec3::new(cos_v * cos_u, sin_v, cos_v * sin_u).normalize();
|
||||
normals.push(n.to_array());
|
||||
uvs.push([u, v]);
|
||||
}
|
||||
}
|
||||
for i in 0..mj {
|
||||
for j in 0..mn {
|
||||
let a = i * (mn + 1) + j;
|
||||
let b = a + 1;
|
||||
let c = a + mn + 1;
|
||||
let d = c + 1;
|
||||
// Triangles [a, b, c] / [b, d, c]: on the surface, angle u (major) grows with +u and
|
||||
// angle v (minor) grows with +v; cross(tang_u, tang_v) points OUTWARD from
|
||||
// the tube (= the stored normal), so the winding is CCW seen from outside —
|
||||
// consistent with `front_face: Face::Ccw` (back-face culling).
|
||||
// The original [a, c, b] order was inverted: the external face (CCW seen from outside,
|
||||
// outward normal) was culled and only the inside of the tube, whose normals
|
||||
// point outward, stayed visible — the torus appeared black (N·L ≤ 0).
|
||||
indices
|
||||
.extend_from_slice(&[a as u16, b as u16, c as u16, b as u16, d as u16, c as u16]);
|
||||
}
|
||||
}
|
||||
Geometry::new(positions)
|
||||
.with_normals(normals)
|
||||
.with_uvs(uvs)
|
||||
.with_indices(indices)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn assert_valid(geo: &Geometry) {
|
||||
geo.validate().expect("generated geometry must validate");
|
||||
let positions = &geo.positions;
|
||||
let normals = geo.normals.as_ref().expect("normals present");
|
||||
let uvs = geo.uvs.as_ref().expect("uvs present");
|
||||
let indices = geo.indices.as_ref().expect("indices present");
|
||||
assert_eq!(normals.len(), positions.len(), "normals/positions count");
|
||||
assert_eq!(uvs.len(), positions.len(), "uvs/positions count");
|
||||
for n in normals {
|
||||
let len = Vec3::from_array(*n).length();
|
||||
assert!((len - 1.0).abs() < 1e-3, "unit normal, got {len}");
|
||||
}
|
||||
for &i in indices {
|
||||
assert!((i as usize) < positions.len(), "index {i} in bounds");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cube_counts() {
|
||||
let g = cube(1.0);
|
||||
assert_eq!(g.positions.len(), 24);
|
||||
assert_eq!(g.indices.as_ref().unwrap().len(), 36);
|
||||
assert_valid(&g);
|
||||
let g2 = cube(2.0);
|
||||
assert_eq!(
|
||||
g2.positions,
|
||||
g.positions
|
||||
.iter()
|
||||
.map(|p| [p[0] * 2.0, p[1] * 2.0, p[2] * 2.0])
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plane_counts() {
|
||||
let g = plane(2.0, 3.0, 1, 1);
|
||||
assert_eq!(g.positions.len(), 4);
|
||||
assert_eq!(g.indices.as_ref().unwrap().len(), 6);
|
||||
assert_valid(&g);
|
||||
assert!(g.positions.iter().all(|p| p[1] == 0.0));
|
||||
let g2 = plane(2.0, 3.0, 4, 5);
|
||||
assert_eq!(g2.positions.len(), (4 + 1) * (5 + 1));
|
||||
assert_valid(&g2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn uv_sphere_counts_and_normals() {
|
||||
let g = uv_sphere(1.0, 12, 8);
|
||||
assert_eq!(g.positions.len(), (12 + 1) * (8 + 1));
|
||||
assert_valid(&g);
|
||||
// Normals point outward (position/radius).
|
||||
for (p, n) in g.positions.iter().zip(g.normals.as_ref().unwrap()) {
|
||||
let diff = (Vec3::from_array(*p) / 1.0 - Vec3::from_array(*n)).length();
|
||||
assert!(diff < 1e-4, "normal ~ position/radius, got diff {diff}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn icosphere_grows_with_subdivision() {
|
||||
let base = icosphere(1.0, 0);
|
||||
assert_eq!(base.positions.len(), 12);
|
||||
assert_eq!(base.indices.as_ref().unwrap().len(), 60);
|
||||
assert_valid(&base);
|
||||
let once = icosphere(1.0, 1);
|
||||
assert!(once.positions.len() > base.positions.len());
|
||||
assert_valid(&once);
|
||||
for (p, n) in once.positions.iter().zip(once.normals.as_ref().unwrap()) {
|
||||
let r = Vec3::from_array(*p).length();
|
||||
assert!((r - 1.0).abs() < 1e-3, "on sphere radius, got {r}");
|
||||
let diff = (Vec3::from_array(*p).normalize() - Vec3::from_array(*n)).length();
|
||||
assert!(diff < 1e-4, "normal ~ direction, got {diff}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cylinder_and_cone_validate() {
|
||||
assert_valid(&cylinder(0.5, 1.0, 16));
|
||||
assert_valid(&cone(0.5, 1.0, 16));
|
||||
let c = cylinder(0.5, 1.0, 8);
|
||||
assert!(c.positions.iter().all(|p| p[1].abs() <= 0.5 + 1e-5));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn torus_validate() {
|
||||
let g = torus(1.0, 0.25, 24, 12);
|
||||
assert_valid(&g);
|
||||
assert_eq!(g.positions.len(), (24 + 1) * (12 + 1));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
//! glTF 2.0 / GLB loader.
|
||||
//!
|
||||
//! **Status: stub** — the full implementation requires the `gltf` crate and will
|
||||
//! be added in a follow-up. For now, this module compiles (behind `feature = "import-gltf"`)
|
||||
//! and returns a clear error.
|
||||
|
||||
use crate::core::geometry::Geometry;
|
||||
use crate::mesh::import::MeshImportError;
|
||||
use std::path::Path;
|
||||
|
||||
/// Loads a glTF 2.0 (.gltf JSON) or GLB (.glb binary) file.
|
||||
///
|
||||
/// # Errors
|
||||
/// Always returns [`MeshImportError::Unsupported`] for now (implementation pending).
|
||||
pub fn load_gltf(path: impl AsRef<Path>) -> Result<Vec<Geometry>, MeshImportError> {
|
||||
let _ = path;
|
||||
Err(MeshImportError::Unsupported(
|
||||
"glTF import is not yet implemented (pending gltf crate wrapper)".into(),
|
||||
))
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
//! File import loaders — each behind a feature flag.
|
||||
//!
|
||||
//! | Feature | Function | Format |
|
||||
//!|---------|----------|--------|
|
||||
//!| `import-obj` | `load_obj(path)` | Wavefront OBJ |
|
||||
//!| `import-gltf` | `load_gltf(path)` | glTF 2.0 / GLB |
|
||||
|
||||
#[cfg(feature = "import-obj")]
|
||||
pub mod obj;
|
||||
#[cfg(feature = "import-gltf")]
|
||||
#[path = "gltf.rs"]
|
||||
pub mod gltf_loader;
|
||||
|
||||
#[cfg(feature = "import-obj")]
|
||||
pub use obj::{load_obj, parse_obj};
|
||||
#[cfg(feature = "import-gltf")]
|
||||
pub use gltf_loader::load_gltf;
|
||||
|
||||
/// Error type for mesh file import.
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum MeshImportError {
|
||||
/// The file could not be read (I/O error).
|
||||
#[error("IO error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
/// The file content is malformed or cannot be parsed.
|
||||
#[error("parse error: {0}")]
|
||||
Parse(String),
|
||||
/// The file uses features not supported by this loader.
|
||||
#[error("unsupported format: {0}")]
|
||||
Unsupported(String),
|
||||
}
|
||||
@@ -0,0 +1,311 @@
|
||||
//! Wavefront OBJ parser — minimal, dependency-free.
|
||||
//!
|
||||
//! Supports: `v` (position), `vn` (normal), `vt` (UV), `f` (face, 3-4 verts).
|
||||
//! Quads are split into triangles via fan triangulation.
|
||||
|
||||
use crate::core::geometry::Geometry;
|
||||
use crate::mesh::import::MeshImportError;
|
||||
use std::path::Path;
|
||||
|
||||
/// Parses a Wavefront OBJ file and returns a single [`Geometry`].
|
||||
///
|
||||
/// Supported directives: `v`, `vn`, `vt`, `f` (3 or 4 vertices per face).
|
||||
/// Vertex references in `f` use 1-based indices.
|
||||
/// If no `vn` lines are present, normals are computed (area-weighted face normals).
|
||||
/// If no `vt` lines are present, UVs are omitted.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns [`MeshImportError::Io`] if the file cannot be read,
|
||||
/// or [`MeshImportError::Parse`] on malformed input.
|
||||
pub fn load_obj(path: impl AsRef<Path>) -> Result<Geometry, MeshImportError> {
|
||||
let content = std::fs::read_to_string(path).map_err(MeshImportError::Io)?;
|
||||
parse_obj(&content)
|
||||
}
|
||||
|
||||
/// Parses OBJ content from a string. See [`load_obj`] for supported features.
|
||||
pub fn parse_obj(content: &str) -> Result<Geometry, MeshImportError> {
|
||||
let mut positions: Vec<[f32; 3]> = Vec::new();
|
||||
let mut file_normals: Vec<[f32; 3]> = Vec::new();
|
||||
let mut file_uvs: Vec<[f32; 2]> = Vec::new();
|
||||
|
||||
// Unique vertex table: (pos_idx, opt_uv_idx, opt_norm_idx)
|
||||
let mut vert_table: Vec<(usize, Option<usize>, Option<usize>)> = Vec::new();
|
||||
let mut indices: Vec<u16> = Vec::new();
|
||||
|
||||
for (line_num, raw) in content.lines().enumerate() {
|
||||
let line = raw.trim();
|
||||
if line.is_empty() || line.starts_with('#') {
|
||||
continue;
|
||||
}
|
||||
let parts: Vec<&str> = line.split_whitespace().collect();
|
||||
match parts[0] {
|
||||
"v" => {
|
||||
if parts.len() < 4 {
|
||||
return Err(MeshImportError::Parse(format!(
|
||||
"line {}: 'v' needs 3+ components",
|
||||
line_num + 1
|
||||
)));
|
||||
}
|
||||
let x = parts[1].parse::<f32>().map_err(|_| {
|
||||
MeshImportError::Parse(format!("line {}: bad 'v' x = '{}'", line_num + 1, parts[1]))
|
||||
})?;
|
||||
let y = parts[2].parse::<f32>().map_err(|_| {
|
||||
MeshImportError::Parse(format!("line {}: bad 'v' y = '{}'", line_num + 1, parts[2]))
|
||||
})?;
|
||||
let z = parts[3].parse::<f32>().map_err(|_| {
|
||||
MeshImportError::Parse(format!("line {}: bad 'v' z = '{}'", line_num + 1, parts[3]))
|
||||
})?;
|
||||
positions.push([x, y, z]);
|
||||
}
|
||||
"vn" => {
|
||||
if parts.len() < 4 {
|
||||
return Err(MeshImportError::Parse(format!(
|
||||
"line {}: 'vn' needs 3+ components",
|
||||
line_num + 1
|
||||
)));
|
||||
}
|
||||
let x = parts[1].parse::<f32>().map_err(|_| {
|
||||
MeshImportError::Parse(format!("line {}: bad 'vn' x", line_num + 1))
|
||||
})?;
|
||||
let y = parts[2].parse::<f32>().map_err(|_| {
|
||||
MeshImportError::Parse(format!("line {}: bad 'vn' y", line_num + 1))
|
||||
})?;
|
||||
let z = parts[3].parse::<f32>().map_err(|_| {
|
||||
MeshImportError::Parse(format!("line {}: bad 'vn' z", line_num + 1))
|
||||
})?;
|
||||
file_normals.push([x, y, z]);
|
||||
}
|
||||
"vt" => {
|
||||
if parts.len() < 3 {
|
||||
return Err(MeshImportError::Parse(format!(
|
||||
"line {}: 'vt' needs 2+ components",
|
||||
line_num + 1
|
||||
)));
|
||||
}
|
||||
let u = parts[1].parse::<f32>().map_err(|_| {
|
||||
MeshImportError::Parse(format!("line {}: bad 'vt' u", line_num + 1))
|
||||
})?;
|
||||
let v = parts[2].parse::<f32>().map_err(|_| {
|
||||
MeshImportError::Parse(format!("line {}: bad 'vt' v", line_num + 1))
|
||||
})?;
|
||||
file_uvs.push([u, v]);
|
||||
}
|
||||
"f" => {
|
||||
if parts.len() < 4 {
|
||||
return Err(MeshImportError::Parse(format!(
|
||||
"line {}: 'f' needs 3+ vertices, got {}",
|
||||
line_num + 1,
|
||||
parts.len() - 1
|
||||
)));
|
||||
}
|
||||
// Parse vertex references: "idx" or "idx/uv" or "idx/uv/norm"
|
||||
let face_verts: Vec<(usize, Option<usize>, Option<usize>)> = parts[1..]
|
||||
.iter()
|
||||
.map(|tok| {
|
||||
let mut fields = tok.split('/');
|
||||
let idx_str = fields.next().unwrap_or("0");
|
||||
let uv_str = fields.next();
|
||||
let norm_str = fields.next();
|
||||
|
||||
let idx: usize = idx_str.parse().map_err(|_| {
|
||||
MeshImportError::Parse(format!(
|
||||
"line {}: bad face vertex index '{}'",
|
||||
line_num + 1,
|
||||
tok
|
||||
))
|
||||
})?;
|
||||
if idx == 0 {
|
||||
return Err(MeshImportError::Parse(format!(
|
||||
"line {}: 0-based index not allowed in face",
|
||||
line_num + 1
|
||||
)));
|
||||
}
|
||||
let uv_idx = parse_opt_idx(uv_str, line_num, "uv")?;
|
||||
let norm_idx = parse_opt_idx(norm_str, line_num, "norm")?;
|
||||
Ok((idx - 1, uv_idx, norm_idx))
|
||||
})
|
||||
.collect::<Result<_, _>>()?;
|
||||
|
||||
// Map to unique vertex indices (dedup by pos+uv+norm tuple)
|
||||
let mapped: Vec<u16> = face_verts
|
||||
.iter()
|
||||
.map(|&(pi, uvi, ni)| {
|
||||
// Check if this combo already exists
|
||||
if let Some(pos) = vert_table.iter().position(|&(ep, eu, en)| {
|
||||
ep == pi && eu == uvi && en == ni
|
||||
}) {
|
||||
pos as u16
|
||||
} else {
|
||||
vert_table.push((pi, uvi, ni));
|
||||
(vert_table.len() - 1) as u16
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Fan triangulation
|
||||
if mapped.len() == 3 {
|
||||
indices.extend_from_slice(&mapped);
|
||||
} else if mapped.len() > 3 {
|
||||
for i in 1..mapped.len() - 1 {
|
||||
indices.extend_from_slice(&[mapped[0], mapped[i], mapped[i + 1]]);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {} // Ignore unknown directives
|
||||
}
|
||||
}
|
||||
|
||||
if positions.is_empty() {
|
||||
return Err(MeshImportError::Parse("no vertices found".into()));
|
||||
}
|
||||
if vert_table.is_empty() {
|
||||
return Err(MeshImportError::Parse("no faces found".into()));
|
||||
}
|
||||
|
||||
// Build output vertex arrays from the table
|
||||
let mut out_positions = Vec::with_capacity(vert_table.len());
|
||||
let mut out_normals = Vec::with_capacity(vert_table.len());
|
||||
let mut out_uvs = Vec::with_capacity(vert_table.len());
|
||||
let mut has_any_uv = false;
|
||||
|
||||
for &(pi, uvi, ni) in &vert_table {
|
||||
out_positions.push(positions[pi]);
|
||||
if let Some(ni) = ni {
|
||||
out_normals.push(file_normals[ni]);
|
||||
} else {
|
||||
out_normals.push([0.0, 0.0, 0.0]);
|
||||
}
|
||||
if let Some(uvi) = uvi {
|
||||
out_uvs.push(file_uvs[uvi]);
|
||||
has_any_uv = true;
|
||||
} else {
|
||||
out_uvs.push([0.0, 0.0]);
|
||||
}
|
||||
}
|
||||
|
||||
// Compute normals if file had none
|
||||
if file_normals.is_empty() {
|
||||
compute_normals(&out_positions, &indices, &mut out_normals);
|
||||
}
|
||||
|
||||
let mut geo = Geometry::new(out_positions)
|
||||
.with_normals(out_normals)
|
||||
.with_indices(indices);
|
||||
if has_any_uv {
|
||||
geo = geo.with_uvs(out_uvs);
|
||||
}
|
||||
|
||||
geo.validate()
|
||||
.map_err(|e| MeshImportError::Parse(format!("validation failed: {e}")))?;
|
||||
Ok(geo)
|
||||
}
|
||||
|
||||
fn parse_opt_idx(
|
||||
field: Option<&str>,
|
||||
line_num: usize,
|
||||
what: &str,
|
||||
) -> Result<Option<usize>, MeshImportError> {
|
||||
match field {
|
||||
None | Some("") => Ok(None),
|
||||
Some(s) => {
|
||||
let idx: usize = s.parse().map_err(|_| {
|
||||
MeshImportError::Parse(format!("line {}: bad {what} index '{s}'", line_num + 1))
|
||||
})?;
|
||||
if idx == 0 {
|
||||
return Err(MeshImportError::Parse(format!(
|
||||
"line {}: 0-based {what} index",
|
||||
line_num + 1
|
||||
)));
|
||||
}
|
||||
Ok(Some(idx - 1))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Computes area-weighted vertex normals from triangle faces.
|
||||
fn compute_normals(positions: &[[f32; 3]], indices: &[u16], normals: &mut [[f32; 3]]) {
|
||||
use glam::Vec3;
|
||||
for n in normals.iter_mut() {
|
||||
*n = [0.0, 0.0, 0.0];
|
||||
}
|
||||
for tri in indices.chunks(3) {
|
||||
if tri.len() != 3 {
|
||||
continue;
|
||||
}
|
||||
let (a, b, c) = (tri[0] as usize, tri[1] as usize, tri[2] as usize);
|
||||
let pa = Vec3::from_array(positions[a]);
|
||||
let pb = Vec3::from_array(positions[b]);
|
||||
let pc = Vec3::from_array(positions[c]);
|
||||
let fn_ = (pb - pa).cross(pc - pa);
|
||||
for idx in [a, b, c] {
|
||||
let n = &mut normals[idx];
|
||||
n[0] += fn_.x;
|
||||
n[1] += fn_.y;
|
||||
n[2] += fn_.z;
|
||||
}
|
||||
}
|
||||
for n in normals.iter_mut() {
|
||||
let v = Vec3::from_array(*n);
|
||||
*n = v.normalize().to_array();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parse_simple_triangle() {
|
||||
let content = "v 0 0 0\nv 1 0 0\nv 0 1 0\nf 1 2 3\n";
|
||||
let geo = parse_obj(content).unwrap();
|
||||
assert_eq!(geo.positions.len(), 3);
|
||||
assert_eq!(geo.indices.as_ref().unwrap().len(), 3);
|
||||
geo.validate().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_quad_splits_to_two_tris() {
|
||||
let content = "v 0 0 0\nv 1 0 0\nv 1 1 0\nv 0 1 0\nf 1 2 3 4\n";
|
||||
let geo = parse_obj(content).unwrap();
|
||||
assert_eq!(geo.positions.len(), 4);
|
||||
assert_eq!(geo.indices.as_ref().unwrap().len(), 6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_with_normals_and_uvs() {
|
||||
let content = "v 0 0 0\nv 1 0 0\nv 0 1 0\nvn 0 0 1\nvt 0 0\nvt 1 0\nvt 0 1\nf 1/1/1 2/2/1 3/3/1\n";
|
||||
let geo = parse_obj(content).unwrap();
|
||||
assert!(geo.normals.is_some());
|
||||
assert!(geo.uvs.is_some());
|
||||
let n = geo.normals.as_ref().unwrap();
|
||||
assert_eq!(n[0], [0.0, 0.0, 1.0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_no_normals_computes_them() {
|
||||
let content = "v 0 0 0\nv 1 0 0\nv 0 1 0\nf 1 2 3\n";
|
||||
let geo = parse_obj(content).unwrap();
|
||||
let n = geo.normals.as_ref().unwrap();
|
||||
assert!((n[0][2] - 1.0).abs() < 1e-4, "expected +Z normal, got {:?}", n[0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_empty_fails() {
|
||||
assert!(parse_obj("").is_err());
|
||||
assert!(parse_obj("# just a comment\n").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_malformed_fails() {
|
||||
assert!(parse_obj("v 1 2\nf 1 2 3\n").is_err());
|
||||
assert!(parse_obj("v 0 0 0\nv 1 0 0\nf 1 2\n").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_shared_vertex_dedup() {
|
||||
let content = "v 0 0 0\nv 1 0 0\nv 1 1 0\nv 0 1 0\nf 1 2 3\nf 1 3 4\n";
|
||||
let geo = parse_obj(content).unwrap();
|
||||
assert_eq!(geo.positions.len(), 4);
|
||||
assert_eq!(geo.indices.as_ref().unwrap().len(), 6);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
//! # Mesh module — geometry sources for WSG
|
||||
//!
|
||||
//! This module is the single entry point for **where geometry data comes from**:
|
||||
//!
|
||||
//! - **`primitives`** — procedural generators (cube, sphere, torus, …), each behind a
|
||||
//! feature flag so you only compile what you need.
|
||||
//! - **`import`** — file loaders (OBJ, glTF), each behind a feature flag.
|
||||
//!
|
||||
//! All sources produce a [`Geometry`] (CPU-side vertex data: positions, normals, UVs,
|
||||
//! indices). Turning that into a GPU renderable is the job of [`crate::scene::Scene::add_mesh`].
|
||||
//!
|
||||
//! ## Usage
|
||||
//!
|
||||
//! ```rust
|
||||
//! use wsg_lib::mesh::cube;
|
||||
//!
|
||||
//! // Procedural (feature "prim-cube")
|
||||
//! let geom = cube(2.0);
|
||||
//! assert_eq!(geom.positions.len(), 24);
|
||||
//! ```
|
||||
//!
|
||||
//! ## Features
|
||||
//!
|
||||
//! | Feature | Provides |
|
||||
//! |---------|----------|
|
||||
//! | `prim-cube` | `cube(size)` |
|
||||
//! | `prim-sphere` | `uv_sphere(…)`, `icosphere(…)` |
|
||||
//! | `prim-cylinder` | `cylinder(…)` |
|
||||
//! | `prim-cone` | `cone(…)` |
|
||||
//! | `prim-torus` | `torus(…)` |
|
||||
//! | `prim-plane` | `plane(…)` |
|
||||
//! | `all-prims` | all of the above |
|
||||
//! | `import-obj` | `load_obj(path)` |
|
||||
//! | `import-gltf` | `load_gltf(path)` |
|
||||
|
||||
pub mod primitives;
|
||||
|
||||
#[cfg(feature = "import-obj")]
|
||||
pub mod import;
|
||||
|
||||
// Flat re-exports at the `wsg::mesh` level for convenience.
|
||||
#[cfg(feature = "prim-cube")]
|
||||
pub use primitives::cube;
|
||||
#[cfg(feature = "prim-plane")]
|
||||
pub use primitives::plane;
|
||||
#[cfg(feature = "prim-sphere")]
|
||||
pub use primitives::{icosphere, uv_sphere};
|
||||
#[cfg(feature = "prim-cylinder")]
|
||||
pub use primitives::cylinder;
|
||||
#[cfg(feature = "prim-cone")]
|
||||
pub use primitives::cone;
|
||||
#[cfg(feature = "prim-torus")]
|
||||
pub use primitives::torus;
|
||||
|
||||
#[cfg(feature = "import-obj")]
|
||||
pub use import::load_obj;
|
||||
#[cfg(feature = "import-gltf")]
|
||||
pub use import::load_gltf;
|
||||
@@ -0,0 +1,89 @@
|
||||
//! Cone primitive — side (apex + base ring) + base cap.
|
||||
|
||||
use crate::core::geometry::Geometry;
|
||||
use glam::Vec3;
|
||||
|
||||
/// Generates a cone of radius `radius` and height `height` (apex at +h/2, base at -h/2), closed by a
|
||||
/// base, with `sectors` segments. Analytical side normals (tilted outward);
|
||||
/// base normal −Y.
|
||||
pub fn cone(radius: f32, height: f32, sectors: u32) -> Geometry {
|
||||
let si = sectors.max(3);
|
||||
let h = height * 0.5;
|
||||
let (mut positions, mut normals, mut uvs, mut indices) = (
|
||||
Vec::<[f32; 3]>::new(),
|
||||
Vec::<[f32; 3]>::new(),
|
||||
Vec::<[f32; 2]>::new(),
|
||||
Vec::<u16>::new(),
|
||||
);
|
||||
|
||||
// Side elements: apex + base ring.
|
||||
let apex = 0u16;
|
||||
positions.push([0.0, h, 0.0]);
|
||||
normals.push([0.0, 1.0, 0.0]); // shared apex; normal close to +Y by default
|
||||
uvs.push([0.5, 1.0]);
|
||||
let base_start = 1u16;
|
||||
for s in 0..=si {
|
||||
let u = s as f32 / si as f32;
|
||||
let theta = u * 2.0 * std::f32::consts::PI;
|
||||
let (sin_t, cos_t) = theta.sin_cos();
|
||||
positions.push([radius * cos_t, -h, radius * sin_t]);
|
||||
// Side normal: normalize(h·cosθ, r, h·sinθ).
|
||||
let n = Vec3::new(h * cos_t, radius, h * sin_t).normalize();
|
||||
normals.push(n.to_array());
|
||||
uvs.push([u, 0.0]);
|
||||
}
|
||||
for s in 0..si {
|
||||
indices.extend_from_slice(&[apex, base_start + s as u16 + 1, base_start + s as u16]);
|
||||
}
|
||||
|
||||
// Closed base (circle at -h/2, normal -Y).
|
||||
let center = positions.len() as u16;
|
||||
positions.push([0.0, -h, 0.0]);
|
||||
normals.push([0.0, -1.0, 0.0]);
|
||||
uvs.push([0.5, 0.5]);
|
||||
let ring = positions.len() as u16;
|
||||
for s in 0..=si {
|
||||
let u = s as f32 / si as f32;
|
||||
let theta = u * 2.0 * std::f32::consts::PI;
|
||||
let (sin_t, cos_t) = theta.sin_cos();
|
||||
positions.push([radius * cos_t, -h, radius * sin_t]);
|
||||
normals.push([0.0, -1.0, 0.0]);
|
||||
uvs.push([0.5 + 0.5 * cos_t, 0.5 + 0.5 * sin_t]);
|
||||
}
|
||||
for s in 0..si {
|
||||
let r = ring + s as u16;
|
||||
indices.extend_from_slice(&[center, r, r + 1]);
|
||||
}
|
||||
|
||||
Geometry::new(positions)
|
||||
.with_normals(normals)
|
||||
.with_uvs(uvs)
|
||||
.with_indices(indices)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn assert_valid(geo: &Geometry) {
|
||||
geo.validate().expect("generated geometry must validate");
|
||||
let positions = &geo.positions;
|
||||
let normals = geo.normals.as_ref().expect("normals present");
|
||||
let uvs = geo.uvs.as_ref().expect("uvs present");
|
||||
let indices = geo.indices.as_ref().expect("indices present");
|
||||
assert_eq!(normals.len(), positions.len());
|
||||
assert_eq!(uvs.len(), positions.len());
|
||||
for n in normals {
|
||||
let len = Vec3::from_array(*n).length();
|
||||
assert!((len - 1.0).abs() < 1e-3);
|
||||
}
|
||||
for &i in indices {
|
||||
assert!((i as usize) < positions.len());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cone_validate() {
|
||||
assert_valid(&cone(0.5, 1.0, 16));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
//! Cube primitive — 24 vertices (4 per face) + 36 indices.
|
||||
|
||||
use crate::core::geometry::Geometry;
|
||||
|
||||
/// Generates a cube centered at the origin, edge `size`, with one normal and UVs per face.
|
||||
/// 24 vertices (4 per face) + 36 indices.
|
||||
pub fn cube(size: f32) -> Geometry {
|
||||
let s = size * 0.5;
|
||||
let faces: [([f32; 3], [[f32; 3]; 4]); 6] = [
|
||||
(
|
||||
[0.0, 0.0, 1.0],
|
||||
[[-s, -s, s], [s, -s, s], [s, s, s], [-s, s, s]],
|
||||
),
|
||||
(
|
||||
[0.0, 0.0, -1.0],
|
||||
[[s, -s, -s], [-s, -s, -s], [-s, s, -s], [s, s, -s]],
|
||||
),
|
||||
(
|
||||
[1.0, 0.0, 0.0],
|
||||
[[s, -s, -s], [s, s, -s], [s, s, s], [s, -s, s]],
|
||||
),
|
||||
(
|
||||
[-1.0, 0.0, 0.0],
|
||||
[[-s, -s, s], [-s, s, s], [-s, s, -s], [-s, -s, -s]],
|
||||
),
|
||||
(
|
||||
[0.0, 1.0, 0.0],
|
||||
[[-s, s, -s], [s, s, -s], [s, s, s], [-s, s, s]],
|
||||
),
|
||||
(
|
||||
[0.0, -1.0, 0.0],
|
||||
[[-s, -s, s], [s, -s, s], [s, -s, -s], [-s, -s, -s]],
|
||||
),
|
||||
];
|
||||
|
||||
let mut positions = Vec::with_capacity(24);
|
||||
let mut normals = Vec::with_capacity(24);
|
||||
let mut uvs = Vec::with_capacity(24);
|
||||
let quad_uvs = [[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]];
|
||||
for (normal, corners) in faces {
|
||||
for (i, corner) in corners.iter().enumerate() {
|
||||
positions.push(*corner);
|
||||
normals.push(normal);
|
||||
uvs.push(quad_uvs[i]);
|
||||
}
|
||||
}
|
||||
|
||||
let mut indices = Vec::with_capacity(36);
|
||||
for face in 0..6u16 {
|
||||
let b = face * 4;
|
||||
indices.extend_from_slice(&[b, b + 1, b + 2, b, b + 2, b + 3]);
|
||||
}
|
||||
|
||||
Geometry::new(positions)
|
||||
.with_normals(normals)
|
||||
.with_uvs(uvs)
|
||||
.with_indices(indices)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use glam::Vec3;
|
||||
|
||||
fn assert_valid(geo: &Geometry) {
|
||||
geo.validate().expect("generated geometry must validate");
|
||||
let positions = &geo.positions;
|
||||
let normals = geo.normals.as_ref().expect("normals present");
|
||||
let uvs = geo.uvs.as_ref().expect("uvs present");
|
||||
let indices = geo.indices.as_ref().expect("indices present");
|
||||
assert_eq!(normals.len(), positions.len(), "normals/positions count");
|
||||
assert_eq!(uvs.len(), positions.len(), "uvs/positions count");
|
||||
for n in normals {
|
||||
let len = Vec3::from_array(*n).length();
|
||||
assert!((len - 1.0).abs() < 1e-3, "unit normal, got {len}");
|
||||
}
|
||||
for &i in indices {
|
||||
assert!((i as usize) < positions.len(), "index {i} in bounds");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cube_counts() {
|
||||
let g = cube(1.0);
|
||||
assert_eq!(g.positions.len(), 24);
|
||||
assert_eq!(g.indices.as_ref().unwrap().len(), 36);
|
||||
assert_valid(&g);
|
||||
let g2 = cube(2.0);
|
||||
assert_eq!(
|
||||
g2.positions,
|
||||
g.positions
|
||||
.iter()
|
||||
.map(|p| [p[0] * 2.0, p[1] * 2.0, p[2] * 2.0])
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
//! Cylinder primitive — side + top/bottom caps.
|
||||
|
||||
use crate::core::geometry::Geometry;
|
||||
use glam::Vec3;
|
||||
|
||||
/// Generates a cylinder of radius `radius` and height `height` (along Y, centered), with
|
||||
/// `sectors` segments. Parts: side (smooth radial normals), top cap (+Y), bottom base (−Y).
|
||||
pub fn cylinder(radius: f32, height: f32, sectors: u32) -> Geometry {
|
||||
let si = sectors.max(3);
|
||||
let h = height * 0.5;
|
||||
let (mut positions, mut normals, mut uvs, mut indices) = (
|
||||
Vec::<[f32; 3]>::new(),
|
||||
Vec::<[f32; 3]>::new(),
|
||||
Vec::<[f32; 2]>::new(),
|
||||
Vec::<u16>::new(),
|
||||
);
|
||||
|
||||
// Side: radial columns × 2 rows (bottom/top).
|
||||
let side_base = 0u16;
|
||||
for row in 0..=1 {
|
||||
let y = if row == 0 { -h } else { h };
|
||||
for s in 0..=si {
|
||||
let u = s as f32 / si as f32;
|
||||
let theta = u * 2.0 * std::f32::consts::PI;
|
||||
let (sin_t, cos_t) = theta.sin_cos();
|
||||
let radial = Vec3::new(cos_t, 0.0, sin_t);
|
||||
positions.push((radial * radius + Vec3::new(0.0, y, 0.0)).to_array());
|
||||
normals.push(radial.to_array());
|
||||
uvs.push([u, row as f32]);
|
||||
}
|
||||
}
|
||||
for s in 0..si {
|
||||
let a = side_base + s as u16;
|
||||
let b = a + 1;
|
||||
let c = side_base + (si as u16) + 1 + s as u16;
|
||||
let d = c + 1;
|
||||
indices.extend_from_slice(&[a, c, b, b, c, d]);
|
||||
}
|
||||
|
||||
// Caps: center + ring at each end.
|
||||
for (y, normal) in [(h, [0.0, 1.0, 0.0]), (-h, [0.0, -1.0, 0.0])] {
|
||||
let center = positions.len() as u16;
|
||||
positions.push([0.0, y, 0.0]);
|
||||
normals.push(normal);
|
||||
uvs.push([0.5, 0.5]);
|
||||
let ring_start = positions.len() as u16;
|
||||
for s in 0..=si {
|
||||
let u = s as f32 / si as f32;
|
||||
let theta = u * 2.0 * std::f32::consts::PI;
|
||||
let (sin_t, cos_t) = theta.sin_cos();
|
||||
positions.push([radius * cos_t, y, radius * sin_t]);
|
||||
normals.push(normal);
|
||||
uvs.push([0.5 + 0.5 * cos_t, 0.5 + 0.5 * sin_t]);
|
||||
}
|
||||
for s in 0..si {
|
||||
let a = ring_start + s as u16;
|
||||
indices.extend_from_slice(&[center, a + 1, a]);
|
||||
}
|
||||
}
|
||||
|
||||
Geometry::new(positions)
|
||||
.with_normals(normals)
|
||||
.with_uvs(uvs)
|
||||
.with_indices(indices)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn assert_valid(geo: &Geometry) {
|
||||
geo.validate().expect("generated geometry must validate");
|
||||
let positions = &geo.positions;
|
||||
let normals = geo.normals.as_ref().expect("normals present");
|
||||
let uvs = geo.uvs.as_ref().expect("uvs present");
|
||||
let indices = geo.indices.as_ref().expect("indices present");
|
||||
assert_eq!(normals.len(), positions.len());
|
||||
assert_eq!(uvs.len(), positions.len());
|
||||
for n in normals {
|
||||
let len = Vec3::from_array(*n).length();
|
||||
assert!((len - 1.0).abs() < 1e-3);
|
||||
}
|
||||
for &i in indices {
|
||||
assert!((i as usize) < positions.len());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cylinder_validate() {
|
||||
assert_valid(&cylinder(0.5, 1.0, 16));
|
||||
let c = cylinder(0.5, 1.0, 8);
|
||||
assert!(c.positions.iter().all(|p| p[1].abs() <= 0.5 + 1e-5));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
//! Procedural mesh generators — each behind a feature flag.
|
||||
//!
|
||||
//! Enable features in `Cargo.toml`:
|
||||
//! ```toml
|
||||
//! wsg = { features = ["prim-cube", "prim-sphere"] }
|
||||
//! ```
|
||||
|
||||
#[cfg(feature = "prim-cube")]
|
||||
pub mod cube;
|
||||
#[cfg(feature = "prim-plane")]
|
||||
pub mod plane;
|
||||
#[cfg(feature = "prim-sphere")]
|
||||
pub mod sphere;
|
||||
#[cfg(feature = "prim-cylinder")]
|
||||
pub mod cylinder;
|
||||
#[cfg(feature = "prim-cone")]
|
||||
pub mod cone;
|
||||
#[cfg(feature = "prim-torus")]
|
||||
pub mod torus;
|
||||
|
||||
// Flat re-exports: `use wsg::mesh::primitives::cube` or `use wsg::mesh::cube`
|
||||
#[cfg(feature = "prim-cube")]
|
||||
pub use cube::cube;
|
||||
#[cfg(feature = "prim-plane")]
|
||||
pub use plane::plane;
|
||||
#[cfg(feature = "prim-sphere")]
|
||||
pub use sphere::{icosphere, uv_sphere};
|
||||
#[cfg(feature = "prim-cylinder")]
|
||||
pub use cylinder::cylinder;
|
||||
#[cfg(feature = "prim-cone")]
|
||||
pub use cone::cone;
|
||||
#[cfg(feature = "prim-torus")]
|
||||
pub use torus::torus;
|
||||
@@ -0,0 +1,74 @@
|
||||
//! Plane primitive — horizontal plane in XZ with subdivisions.
|
||||
|
||||
use crate::core::geometry::Geometry;
|
||||
|
||||
/// Generates a horizontal plane in the XZ plane (normal +Y), centered at (0, 0, 0), with
|
||||
/// `width` × `depth` dimensions, subdivided into `seg_x` × `seg_z` cells.
|
||||
pub fn plane(width: f32, depth: f32, seg_x: u32, seg_z: u32) -> Geometry {
|
||||
let sx = seg_x.max(1);
|
||||
let sz = seg_z.max(1);
|
||||
let (mut positions, mut normals, mut uvs, mut indices) = (
|
||||
Vec::<[f32; 3]>::new(),
|
||||
Vec::<[f32; 3]>::new(),
|
||||
Vec::<[f32; 2]>::new(),
|
||||
Vec::<u16>::new(),
|
||||
);
|
||||
for z in 0..=sz {
|
||||
let vz = z as f32 / sz as f32;
|
||||
for x in 0..=sx {
|
||||
let vx = x as f32 / sx as f32;
|
||||
positions.push([(vx - 0.5) * width, 0.0, (vz - 0.5) * depth]);
|
||||
normals.push([0.0, 1.0, 0.0]);
|
||||
uvs.push([vx, vz]);
|
||||
}
|
||||
}
|
||||
for z in 0..sz {
|
||||
for x in 0..sx {
|
||||
let a = z * (sx + 1) + x;
|
||||
let b = a + 1;
|
||||
let c = (z + 1) * (sx + 1) + x;
|
||||
let d = c + 1;
|
||||
indices
|
||||
.extend_from_slice(&[a as u16, c as u16, b as u16, b as u16, c as u16, d as u16]);
|
||||
}
|
||||
}
|
||||
Geometry::new(positions)
|
||||
.with_normals(normals)
|
||||
.with_uvs(uvs)
|
||||
.with_indices(indices)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use glam::Vec3;
|
||||
|
||||
fn assert_valid(geo: &Geometry) {
|
||||
geo.validate().expect("generated geometry must validate");
|
||||
let positions = &geo.positions;
|
||||
let normals = geo.normals.as_ref().expect("normals present");
|
||||
let uvs = geo.uvs.as_ref().expect("uvs present");
|
||||
let indices = geo.indices.as_ref().expect("indices present");
|
||||
assert_eq!(normals.len(), positions.len());
|
||||
assert_eq!(uvs.len(), positions.len());
|
||||
for n in normals {
|
||||
let len = Vec3::from_array(*n).length();
|
||||
assert!((len - 1.0).abs() < 1e-3);
|
||||
}
|
||||
for &i in indices {
|
||||
assert!((i as usize) < positions.len());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plane_counts() {
|
||||
let g = plane(2.0, 3.0, 1, 1);
|
||||
assert_eq!(g.positions.len(), 4);
|
||||
assert_eq!(g.indices.as_ref().unwrap().len(), 6);
|
||||
assert_valid(&g);
|
||||
assert!(g.positions.iter().all(|p| p[1] == 0.0));
|
||||
let g2 = plane(2.0, 3.0, 4, 5);
|
||||
assert_eq!(g2.positions.len(), (4 + 1) * (5 + 1));
|
||||
assert_valid(&g2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
//! Sphere primitives — UV sphere (lat/long) + icosphere (subdivided icosahedron).
|
||||
|
||||
use crate::core::geometry::Geometry;
|
||||
use glam::Vec3;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Generates a UV (latitude/longitude) sphere of radius `radius`, with `sectors` segments around
|
||||
/// and `stacks` vertical rings. Smooth normals = normalized position; spherical UVs.
|
||||
pub fn uv_sphere(radius: f32, sectors: u32, stacks: u32) -> Geometry {
|
||||
let si = sectors.max(3);
|
||||
let st = stacks.max(3);
|
||||
let (mut positions, mut normals, mut uvs, mut indices) = (
|
||||
Vec::<[f32; 3]>::new(),
|
||||
Vec::<[f32; 3]>::new(),
|
||||
Vec::<[f32; 2]>::new(),
|
||||
Vec::<u16>::new(),
|
||||
);
|
||||
for stack in 0..=st {
|
||||
let v = stack as f32 / st as f32;
|
||||
let phi = v * std::f32::consts::PI;
|
||||
for sector in 0..=si {
|
||||
let u = sector as f32 / si as f32;
|
||||
let theta = u * 2.0 * std::f32::consts::PI;
|
||||
let (sin_p, cos_p) = phi.sin_cos();
|
||||
let (sin_t, cos_t) = theta.sin_cos();
|
||||
let pos = Vec3::new(
|
||||
radius * sin_p * cos_t,
|
||||
radius * cos_p,
|
||||
radius * sin_p * sin_t,
|
||||
);
|
||||
positions.push(pos.to_array());
|
||||
normals.push(pos.normalize().to_array());
|
||||
uvs.push([u, v]);
|
||||
}
|
||||
}
|
||||
for stack in 0..st {
|
||||
for sector in 0..si {
|
||||
let k1 = stack * (si + 1) + sector;
|
||||
let k2 = k1 + si + 1;
|
||||
let (k1, k2) = (k1 as u16, k2 as u16);
|
||||
indices.extend_from_slice(&[k1, k2, k1 + 1, k1 + 1, k2, k2 + 1]);
|
||||
}
|
||||
}
|
||||
Geometry::new(positions)
|
||||
.with_normals(normals)
|
||||
.with_uvs(uvs)
|
||||
.with_indices(indices)
|
||||
}
|
||||
|
||||
/// Generates an icosphere (subdivided icosahedron) of radius `radius`.
|
||||
/// `subdivisions = 0` gives an icosahedron (12 verts / 20 faces); each subdivision refines ×4.
|
||||
pub fn icosphere(radius: f32, subdivisions: u32) -> Geometry {
|
||||
let t = (1.0 + 5.0_f32.sqrt()) * 0.5;
|
||||
let mut positions: Vec<Vec3> = [
|
||||
[-1.0, t, 0.0], [1.0, t, 0.0], [-1.0, -t, 0.0], [1.0, -t, 0.0],
|
||||
[0.0, -1.0, t], [0.0, 1.0, t], [0.0, -1.0, -t], [0.0, 1.0, -t],
|
||||
[t, 0.0, -1.0], [t, 0.0, 1.0], [-t, 0.0, -1.0], [-t, 0.0, 1.0],
|
||||
]
|
||||
.iter()
|
||||
.map(|v| Vec3::from_array(*v).normalize())
|
||||
.collect();
|
||||
|
||||
let mut faces: Vec<[u32; 3]> = [
|
||||
[0, 11, 5], [0, 5, 1], [0, 1, 7], [0, 7, 10], [0, 10, 11],
|
||||
[1, 5, 9], [5, 11, 4], [11, 10, 2], [10, 7, 6], [7, 1, 8],
|
||||
[3, 9, 4], [3, 4, 2], [3, 2, 6], [3, 6, 8], [3, 8, 9],
|
||||
[4, 9, 5], [2, 4, 11], [6, 2, 10], [8, 6, 7], [9, 8, 1],
|
||||
]
|
||||
.into_iter()
|
||||
.collect();
|
||||
|
||||
for _ in 0..subdivisions {
|
||||
let mut midpoint = HashMap::new();
|
||||
let old_faces = std::mem::take(&mut faces);
|
||||
for [a, b, c] in old_faces {
|
||||
let ab = subdiv_midpoint(&mut positions, &mut midpoint, a, b);
|
||||
let bc = subdiv_midpoint(&mut positions, &mut midpoint, b, c);
|
||||
let ca = subdiv_midpoint(&mut positions, &mut midpoint, c, a);
|
||||
faces.push([a, ab, ca]);
|
||||
faces.push([ab, b, bc]);
|
||||
faces.push([ca, bc, c]);
|
||||
faces.push([ab, bc, ca]);
|
||||
}
|
||||
}
|
||||
|
||||
let mut normals = Vec::with_capacity(positions.len());
|
||||
let mut uvs = Vec::with_capacity(positions.len());
|
||||
for p in &positions {
|
||||
let dir = p.normalize();
|
||||
normals.push(dir.to_array());
|
||||
uvs.push(spherical_uv(dir));
|
||||
}
|
||||
let scaled: Vec<[f32; 3]> = positions.iter().map(|p| (*p * radius).to_array()).collect();
|
||||
|
||||
let mut indices = Vec::with_capacity(faces.len() * 3);
|
||||
for [a, b, c] in &faces {
|
||||
indices.extend_from_slice(&[*a as u16, *b as u16, *c as u16]);
|
||||
}
|
||||
Geometry::new(scaled)
|
||||
.with_normals(normals)
|
||||
.with_uvs(uvs)
|
||||
.with_indices(indices)
|
||||
}
|
||||
|
||||
fn subdiv_midpoint(
|
||||
positions: &mut Vec<Vec3>,
|
||||
cache: &mut HashMap<(u32, u32), u32>,
|
||||
a: u32,
|
||||
b: u32,
|
||||
) -> u32 {
|
||||
let key = if a < b { (a, b) } else { (b, a) };
|
||||
if let Some(&i) = cache.get(&key) {
|
||||
return i;
|
||||
}
|
||||
let mid = (positions[a as usize] + positions[b as usize]).normalize();
|
||||
positions.push(mid);
|
||||
let i = (positions.len() - 1) as u32;
|
||||
cache.insert(key, i);
|
||||
i
|
||||
}
|
||||
|
||||
fn spherical_uv(dir: Vec3) -> [f32; 2] {
|
||||
let u = 0.5 + (dir.z.atan2(dir.x) / (2.0 * std::f32::consts::PI));
|
||||
let v = 0.5 - (dir.y.asin() / std::f32::consts::PI);
|
||||
[u, v]
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn assert_valid(geo: &Geometry) {
|
||||
geo.validate().expect("generated geometry must validate");
|
||||
let positions = &geo.positions;
|
||||
let normals = geo.normals.as_ref().expect("normals present");
|
||||
let uvs = geo.uvs.as_ref().expect("uvs present");
|
||||
let indices = geo.indices.as_ref().expect("indices present");
|
||||
assert_eq!(normals.len(), positions.len());
|
||||
assert_eq!(uvs.len(), positions.len());
|
||||
for n in normals {
|
||||
let len = Vec3::from_array(*n).length();
|
||||
assert!((len - 1.0).abs() < 1e-3);
|
||||
}
|
||||
for &i in indices {
|
||||
assert!((i as usize) < positions.len());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn uv_sphere_counts_and_normals() {
|
||||
let g = uv_sphere(1.0, 12, 8);
|
||||
assert_eq!(g.positions.len(), (12 + 1) * (8 + 1));
|
||||
assert_valid(&g);
|
||||
for (p, n) in g.positions.iter().zip(g.normals.as_ref().unwrap()) {
|
||||
let diff = (Vec3::from_array(*p) / 1.0 - Vec3::from_array(*n)).length();
|
||||
assert!(diff < 1e-4);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn icosphere_grows_with_subdivision() {
|
||||
let base = icosphere(1.0, 0);
|
||||
assert_eq!(base.positions.len(), 12);
|
||||
assert_eq!(base.indices.as_ref().unwrap().len(), 60);
|
||||
assert_valid(&base);
|
||||
let once = icosphere(1.0, 1);
|
||||
assert!(once.positions.len() > base.positions.len());
|
||||
assert_valid(&once);
|
||||
for (p, n) in once.positions.iter().zip(once.normals.as_ref().unwrap()) {
|
||||
let r = Vec3::from_array(*p).length();
|
||||
assert!((r - 1.0).abs() < 1e-3);
|
||||
let diff = (Vec3::from_array(*p).normalize() - Vec3::from_array(*n)).length();
|
||||
assert!(diff < 1e-4);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
//! Torus primitive — tube around a ring.
|
||||
|
||||
use crate::core::geometry::Geometry;
|
||||
use glam::Vec3;
|
||||
|
||||
/// Generates a torus with major radius `major`, minor radius `minor`, with `major_segments`
|
||||
/// segments around the ring and `minor_segments` around the tube cross-section.
|
||||
pub fn torus(major: f32, minor: f32, major_segments: u32, minor_segments: u32) -> Geometry {
|
||||
let mj = major_segments.max(3);
|
||||
let mn = minor_segments.max(3);
|
||||
let (mut positions, mut normals, mut uvs, mut indices) = (
|
||||
Vec::<[f32; 3]>::new(),
|
||||
Vec::<[f32; 3]>::new(),
|
||||
Vec::<[f32; 2]>::new(),
|
||||
Vec::<u16>::new(),
|
||||
);
|
||||
for i in 0..=mj {
|
||||
let u = i as f32 / mj as f32;
|
||||
let ua = u * 2.0 * std::f32::consts::PI;
|
||||
let (sin_u, cos_u) = ua.sin_cos();
|
||||
for j in 0..=mn {
|
||||
let v = j as f32 / mn as f32;
|
||||
let va = v * 2.0 * std::f32::consts::PI;
|
||||
let (sin_v, cos_v) = va.sin_cos();
|
||||
let ring = Vec3::new(
|
||||
(major + minor * cos_v) * cos_u,
|
||||
minor * sin_v,
|
||||
(major + minor * cos_v) * sin_u,
|
||||
);
|
||||
positions.push(ring.to_array());
|
||||
let n = Vec3::new(cos_v * cos_u, sin_v, cos_v * sin_u).normalize();
|
||||
normals.push(n.to_array());
|
||||
uvs.push([u, v]);
|
||||
}
|
||||
}
|
||||
for i in 0..mj {
|
||||
for j in 0..mn {
|
||||
let a = i * (mn + 1) + j;
|
||||
let b = a + 1;
|
||||
let c = a + mn + 1;
|
||||
let d = c + 1;
|
||||
indices
|
||||
.extend_from_slice(&[a as u16, b as u16, c as u16, b as u16, d as u16, c as u16]);
|
||||
}
|
||||
}
|
||||
Geometry::new(positions)
|
||||
.with_normals(normals)
|
||||
.with_uvs(uvs)
|
||||
.with_indices(indices)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn assert_valid(geo: &Geometry) {
|
||||
geo.validate().expect("generated geometry must validate");
|
||||
let positions = &geo.positions;
|
||||
let normals = geo.normals.as_ref().expect("normals present");
|
||||
let uvs = geo.uvs.as_ref().expect("uvs present");
|
||||
let indices = geo.indices.as_ref().expect("indices present");
|
||||
assert_eq!(normals.len(), positions.len());
|
||||
assert_eq!(uvs.len(), positions.len());
|
||||
for n in normals {
|
||||
let len = Vec3::from_array(*n).length();
|
||||
assert!((len - 1.0).abs() < 1e-3);
|
||||
}
|
||||
for &i in indices {
|
||||
assert!((i as usize) < positions.len());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn torus_validate() {
|
||||
let g = torus(1.0, 0.25, 24, 12);
|
||||
assert_valid(&g);
|
||||
assert_eq!(g.positions.len(), (24 + 1) * (12 + 1));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
//! # WSG Prelude
|
||||
//!
|
||||
//! Re-exports the most commonly used types in a single glob import:
|
||||
//!
|
||||
//! ```rust
|
||||
//! use wsg_lib::prelude::*;
|
||||
//!
|
||||
//! let geom = cube(2.0);
|
||||
//! assert_eq!(geom.positions.len(), 24);
|
||||
//! let tf = Transform::identity();
|
||||
//! ```
|
||||
//!
|
||||
//! This avoids long import paths for the types you touch every day.
|
||||
|
||||
// Core types
|
||||
pub use crate::core::geometry::{BBox, Geometry};
|
||||
pub use crate::core::transform::Transform;
|
||||
pub use crate::core::{ShadowConfig, ToneMapper};
|
||||
|
||||
// App / handler (already at crate root, re-exported here for convenience)
|
||||
pub use crate::app::AppBuilder;
|
||||
pub use crate::handler::AppHandler;
|
||||
|
||||
// Primitives (available when the corresponding feature is enabled)
|
||||
#[cfg(feature = "prim-cube")]
|
||||
pub use crate::mesh::cube;
|
||||
#[cfg(feature = "prim-sphere")]
|
||||
pub use crate::mesh::{icosphere, uv_sphere};
|
||||
#[cfg(feature = "prim-cylinder")]
|
||||
pub use crate::mesh::cylinder;
|
||||
#[cfg(feature = "prim-cone")]
|
||||
pub use crate::mesh::cone;
|
||||
#[cfg(feature = "prim-torus")]
|
||||
pub use crate::mesh::torus;
|
||||
#[cfg(feature = "prim-plane")]
|
||||
pub use crate::mesh::plane;
|
||||
|
||||
// Import (available when the corresponding feature is enabled)
|
||||
#[cfg(feature = "import-obj")]
|
||||
pub use crate::mesh::load_obj;
|
||||
#[cfg(feature = "import-gltf")]
|
||||
pub use crate::mesh::load_gltf;
|
||||
|
||||
// Import error type
|
||||
#[cfg(any(feature = "import-obj", feature = "import-gltf"))]
|
||||
pub use crate::mesh::import::MeshImportError;
|
||||
@@ -25,7 +25,7 @@
|
||||
//! (which took raw `&[Vertex]`) were removed in Step 8: the `Scene` declares meshes from a `Geometry`, and
|
||||
//! `Mesh` derives its interleaved vertices internally via `Geometry::to_vertices()`.
|
||||
|
||||
use crate::math::Geometry;
|
||||
use crate::core::Geometry;
|
||||
use crate::resources::Material;
|
||||
use crate::resources::Vertex;
|
||||
use crate::resources::uniform::LodRow;
|
||||
@@ -272,7 +272,7 @@ impl Mesh {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::math::primitives;
|
||||
use crate::mesh::primitives;
|
||||
|
||||
#[test]
|
||||
fn pack_levels_offsets_and_rebasing() {
|
||||
|
||||
@@ -36,4 +36,4 @@ pub use vertex::Vertex;
|
||||
|
||||
// Convenience re-export of `math::Geometry` (Step 8, D2) so examples can build meshes
|
||||
// from `wsg_lib::resources::Geometry` without importing `math` separately.
|
||||
pub use crate::math::Geometry;
|
||||
pub use crate::core::Geometry;
|
||||
|
||||
@@ -225,10 +225,10 @@ pub struct TransformSlot {
|
||||
}
|
||||
|
||||
impl TransformSlot {
|
||||
/// Builds an active transform slot from a CPU [`crate::math::Transform`] + the mesh's draw
|
||||
/// Builds an active transform slot from a CPU [`crate::core::Transform`] + the mesh's draw
|
||||
/// metadata. `mesh_index` / `draw_count` are packed into `flags`; `active` is 1.
|
||||
pub fn from_transform(
|
||||
t: &crate::math::Transform,
|
||||
t: &crate::core::Transform,
|
||||
mesh_index: u32,
|
||||
draw_count: u32,
|
||||
has_index: bool,
|
||||
@@ -313,8 +313,8 @@ pub struct BBoxSlot {
|
||||
}
|
||||
|
||||
impl BBoxSlot {
|
||||
/// Builds a slot from a CPU [`crate::math::BBox`] (padding zeroed).
|
||||
pub fn from_bbox(b: &crate::math::BBox) -> Self {
|
||||
/// Builds a slot from a CPU [`crate::core::BBox`] (padding zeroed).
|
||||
pub fn from_bbox(b: &crate::core::BBox) -> Self {
|
||||
Self {
|
||||
min: b.min,
|
||||
max: b.max,
|
||||
@@ -377,8 +377,8 @@ impl CullUniforms {
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds the cull block directly from a [`crate::math::Frustum`] (its six unit planes).
|
||||
pub fn from_frustum(f: &crate::math::Frustum, num_slots: u32, culling: bool) -> Self {
|
||||
/// Builds the cull block directly from a [`crate::core::Frustum`] (its six unit planes).
|
||||
pub fn from_frustum(f: &crate::core::Frustum, num_slots: u32, culling: bool) -> Self {
|
||||
Self::new(f.planes, num_slots, culling)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
//! matrix during rendering.
|
||||
//! - `resources::Mesh` is the referenced render resource, resolved by `Scene`; its Material is read by the Renderer.
|
||||
|
||||
use crate::math::Transform;
|
||||
use crate::core::Transform;
|
||||
|
||||
/// A renderable entity: a mesh (with its own material) and a world-space transform.
|
||||
///
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
//! instead of `App`. It can therefore build materials and meshes itself (`add_material_shader`, `create_mesh`) and inject
|
||||
//! a default material for meshes that carry none (`default_material`).
|
||||
|
||||
use crate::math::{Geometry, Transform};
|
||||
use crate::core::{Geometry, Transform};
|
||||
use crate::pipeline::PipelineCache;
|
||||
use crate::resources::{BBoxSlot, Camera, Lights, Material, Mesh, Texture, TransformSlot};
|
||||
use crate::scene::Entity;
|
||||
|
||||
Reference in New Issue
Block a user