From 21e015f75a22420d5fad52245b9d185c17a5b41e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?charlotte=20=F0=9F=8C=B8?= Date: Tue, 24 Feb 2026 18:01:53 -0800 Subject: [PATCH 1/3] Add Gltf support. --- Cargo.lock | 1 + Cargo.toml | 4 + assets/gltf/Duck.glb | Bin 0 -> 120656 bytes crates/processing_ffi/src/lib.rs | 11 +- crates/processing_pyo3/examples/gltf_load.py | 46 ++ crates/processing_pyo3/src/gltf.rs | 57 +++ crates/processing_pyo3/src/graphics.rs | 18 +- crates/processing_pyo3/src/lib.rs | 4 + crates/processing_render/Cargo.toml | 1 + crates/processing_render/src/error.rs | 2 + crates/processing_render/src/gltf.rs | 426 +++++++++++++++++++ crates/processing_render/src/lib.rs | 72 ++++ examples/gltf_load.rs | 75 ++++ 13 files changed, 711 insertions(+), 6 deletions(-) create mode 100644 assets/gltf/Duck.glb create mode 100644 crates/processing_pyo3/examples/gltf_load.py create mode 100644 crates/processing_pyo3/src/gltf.rs create mode 100644 crates/processing_render/src/gltf.rs create mode 100644 examples/gltf_load.rs diff --git a/Cargo.lock b/Cargo.lock index 33b5d77..0043a02 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4748,6 +4748,7 @@ version = "0.1.0" dependencies = [ "bevy", "crossbeam-channel", + "gltf", "half", "js-sys", "lyon", diff --git a/Cargo.toml b/Cargo.toml index 05eddaf..fc8c994 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -82,6 +82,10 @@ path = "examples/pbr.rs" name = "midi" path = "examples/midi.rs" +[[example]] +name = "gltf_load" +path = "examples/gltf_load.rs" + [profile.wasm-release] inherits = "release" opt-level = "z" diff --git a/assets/gltf/Duck.glb b/assets/gltf/Duck.glb new file mode 100644 index 0000000000000000000000000000000000000000..39d3fed50e94a9bef9e64f0985975efe68fdc717 GIT binary patch literal 120656 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 PyResult { + let entity = gltf_geometry(self.entity, name) + .map_err(|e| PyRuntimeError::new_err(format!("{e}")))?; + Ok(Geometry { entity }) + } + + pub fn material(&self, name: &str) -> PyResult { + let entity = gltf_material(self.entity, name) + .map_err(|e| PyRuntimeError::new_err(format!("{e}")))?; + Ok(Material { entity }) + } + + pub fn mesh_names(&self) -> PyResult> { + gltf_mesh_names(self.entity).map_err(|e| PyRuntimeError::new_err(format!("{e}"))) + } + + pub fn material_names(&self) -> PyResult> { + gltf_material_names(self.entity).map_err(|e| PyRuntimeError::new_err(format!("{e}"))) + } + + pub fn camera(&self, index: usize) -> PyResult<()> { + gltf_camera(self.entity, self.graphics_entity, index) + .map_err(|e| PyRuntimeError::new_err(format!("{e}"))) + } + + pub fn light(&self, index: usize) -> PyResult { + let entity = gltf_light(self.entity, self.graphics_entity, index) + .map_err(|e| PyRuntimeError::new_err(format!("{e}")))?; + Ok(Light { entity }) + } +} + +#[pyfunction] +#[pyo3(pass_module)] +pub fn load_gltf(module: &Bound<'_, PyModule>, path: &str) -> PyResult { + let graphics = get_graphics(module)?; + let entity = gltf_load(path).map_err(|e| PyRuntimeError::new_err(format!("{e}")))?; + Ok(Gltf { + entity, + graphics_entity: graphics.entity, + }) +} diff --git a/crates/processing_pyo3/src/graphics.rs b/crates/processing_pyo3/src/graphics.rs index a14f6db..89a2c39 100644 --- a/crates/processing_pyo3/src/graphics.rs +++ b/crates/processing_pyo3/src/graphics.rs @@ -1,3 +1,4 @@ +use bevy::math::Vec4; use bevy::prelude::Entity; use processing::prelude::*; use pyo3::{exceptions::PyRuntimeError, prelude::*, types::PyDict}; @@ -26,7 +27,7 @@ impl Drop for Surface { #[pyclass] #[derive(Debug)] pub struct Light { - entity: Entity, + pub(crate) entity: Entity, } #[pymethods] @@ -62,7 +63,7 @@ impl Drop for Image { #[pyclass(unsendable)] pub struct Geometry { - entity: Entity, + pub(crate) entity: Entity, } #[pyclass] @@ -132,7 +133,7 @@ impl Geometry { #[pyclass(unsendable)] pub struct Graphics { - entity: Entity, + pub(crate) entity: Entity, pub surface: Surface, } @@ -357,8 +358,15 @@ impl Graphics { } pub fn perspective(&self, fov: f32, aspect: f32, near: f32, far: f32) -> PyResult<()> { - graphics_perspective(self.entity, fov, aspect, near, far) - .map_err(|e| PyRuntimeError::new_err(format!("{e}"))) + graphics_perspective( + self.entity, + fov, + aspect, + near, + far, + Vec4::new(0.0, 0.0, -1.0, -near), + ) + .map_err(|e| PyRuntimeError::new_err(format!("{e}"))) } #[allow(clippy::too_many_arguments)] diff --git a/crates/processing_pyo3/src/lib.rs b/crates/processing_pyo3/src/lib.rs index 74e9212..5116932 100644 --- a/crates/processing_pyo3/src/lib.rs +++ b/crates/processing_pyo3/src/lib.rs @@ -9,6 +9,7 @@ //! To allow Python users to create a similar experience, we provide module-level //! functions that forward to a singleton Graphics object pub(crate) behind the scenes. mod glfw; +mod gltf; mod graphics; pub(crate) mod material; @@ -21,6 +22,7 @@ use pyo3::{ }; use std::ffi::{CStr, CString}; +use gltf::Gltf; use std::env; #[pymodule] @@ -30,6 +32,8 @@ fn processing(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(gltf::load_gltf, m)?)?; m.add_function(wrap_pyfunction!(size, m)?)?; m.add_function(wrap_pyfunction!(run, m)?)?; m.add_function(wrap_pyfunction!(mode_3d, m)?)?; diff --git a/crates/processing_render/Cargo.toml b/crates/processing_render/Cargo.toml index 8999a2c..68977b8 100644 --- a/crates/processing_render/Cargo.toml +++ b/crates/processing_render/Cargo.toml @@ -13,6 +13,7 @@ x11 = ["bevy/x11"] [dependencies] bevy = { workspace = true } +gltf = { version = "1.4.0", default-features = false, features = ["KHR_lights_punctual", "names"] } lyon = "1.0" raw-window-handle = "0.6" thiserror = "2" diff --git a/crates/processing_render/src/error.rs b/crates/processing_render/src/error.rs index 91eec57..9a3ab90 100644 --- a/crates/processing_render/src/error.rs +++ b/crates/processing_render/src/error.rs @@ -34,4 +34,6 @@ pub enum ProcessingError { MaterialNotFound, #[error("Unknown material property: {0}")] UnknownMaterialProperty(String), + #[error("GLTF load error: {0}")] + GltfLoadError(String), } diff --git a/crates/processing_render/src/gltf.rs b/crates/processing_render/src/gltf.rs new file mode 100644 index 0000000..c33a38d --- /dev/null +++ b/crates/processing_render/src/gltf.rs @@ -0,0 +1,426 @@ +//! Load and query GLTF files, providing name-based lookup for meshes, +//! materials, cameras, and lights. + +use bevy::{ + asset::{ + AssetPath, LoadState, handle_internal_asset_events, + io::{AssetSourceId, embedded::GetAssetServer}, + }, + ecs::system::RunSystemOnce, + gltf::{Gltf, GltfLoaderSettings, GltfMesh}, + prelude::*, +}; + +use crate::config::{Config, ConfigKey}; +use crate::error::{ProcessingError, Result}; +use crate::geometry::{BuiltinAttributes, Geometry, layout::VertexLayout}; +use crate::graphics; +use crate::light; +use crate::render::material::UntypedMaterial; + +fn resolve_asset_path(config: &Config, path: &str) -> AssetPath<'static> { + let asset_path = AssetPath::parse(path).into_owned(); + match config.get(ConfigKey::AssetRootPath) { + Some(_) => asset_path.with_source(AssetSourceId::from("assets_directory")), + None => asset_path, + } +} + +/// Block until an asset handle loads, returning an error if loading fails. +fn block_on_load(world: &mut World, load_state: impl Fn(&World) -> LoadState) -> Result<()> { + loop { + match load_state(world) { + LoadState::Loading => { + world.run_system_once(handle_internal_asset_events).unwrap(); + } + LoadState::Loaded => return Ok(()), + LoadState::Failed(err) => { + return Err(ProcessingError::GltfLoadError(format!( + "Asset failed to load: {err}" + ))); + } + LoadState::NotLoaded => { + return Err(ProcessingError::GltfLoadError( + "Asset not loaded".to_string(), + )); + } + } + } +} + +/// Load the root GLTF asset, blocking until fully loaded. +fn load_gltf_root(world: &mut World, config: &Config, path: &str) -> Result> { + let base_path = match path.find('#') { + Some(idx) => &path[..idx], + None => path, + }; + let asset_path = resolve_asset_path(config, base_path); + let handle: Handle = + world + .get_asset_server() + .load_with_settings(asset_path, |s: &mut GltfLoaderSettings| { + s.include_source = true; + }); + block_on_load(world, |w| w.get_asset_server().load_state(&handle))?; + Ok(handle) +} + +#[derive(Component)] +pub struct GltfHandle { + handle: Handle, + base_path: String, +} + +pub fn load(In(path): In, world: &mut World) -> Result { + let config = world.resource::().clone(); + let handle = load_gltf_root(world, &config, &path)?; + let base_path = match path.find('#') { + Some(idx) => path[..idx].to_string(), + None => path, + }; + let entity = world.spawn(GltfHandle { handle, base_path }).id(); + Ok(entity) +} + +pub fn geometry( + In((gltf_entity, name)): In<(Entity, String)>, + world: &mut World, +) -> Result { + let handle = world + .get::(gltf_entity) + .ok_or(ProcessingError::InvalidEntity)?; + let gltf_handle = handle.handle.clone(); + + let mesh_handle = { + let gltf_assets = world.resource::>(); + let gltf = gltf_assets + .get(&gltf_handle) + .ok_or_else(|| ProcessingError::GltfLoadError("GLTF asset not found".into()))?; + let gltf_mesh_handle = gltf.named_meshes.get(name.as_str()).ok_or_else(|| { + ProcessingError::GltfLoadError(format!("Mesh '{}' not found in GLTF", name)) + })?; + let gltf_mesh_assets = world.resource::>(); + let gltf_mesh = gltf_mesh_assets + .get(gltf_mesh_handle) + .ok_or_else(|| ProcessingError::GltfLoadError("GltfMesh asset not found".into()))?; + // TODO: a mesh could have multiple primitives, but for simplicity we'll just take the + // first one here. we could extend the API later to allow users to specify a primitive index + // if needed, or support mesh hierachies via parent/child ptrs on the python classes. + let prim = gltf_mesh.primitives.first().ok_or_else(|| { + ProcessingError::GltfLoadError(format!("Mesh '{}' has no primitives", name)) + })?; + prim.mesh.clone() + }; + + let builtins = world.resource::(); + let attrs = vec![ + builtins.position, + builtins.normal, + builtins.color, + builtins.uv, + ]; + let layout_entity = world.spawn(VertexLayout::with_attributes(attrs)).id(); + let entity = world.spawn(Geometry::new(mesh_handle, layout_entity)).id(); + Ok(entity) +} + +pub fn material( + In((gltf_entity, name)): In<(Entity, String)>, + world: &mut World, +) -> Result { + let handle = world + .get::(gltf_entity) + .ok_or(ProcessingError::InvalidEntity)?; + let gltf_handle = handle.handle.clone(); + let base_path = handle.base_path.clone(); + + let material_index = { + let gltf_assets = world.resource::>(); + let gltf = gltf_assets + .get(&gltf_handle) + .ok_or_else(|| ProcessingError::GltfLoadError("GLTF asset not found".into()))?; + let named_handle = gltf.named_materials.get(name.as_str()).ok_or_else(|| { + ProcessingError::GltfLoadError(format!("Material '{}' not found in GLTF", name)) + })?; + gltf.materials + .iter() + .position(|h| h.id() == named_handle.id()) + .ok_or_else(|| { + ProcessingError::GltfLoadError(format!( + "Material '{}' not found in materials list", + name + )) + })? + }; + + let config = world.resource::().clone(); + // this is a bit hacky but we can leverage the fact that the GLTF loader creates standard + // material assets with predictable labels based on the GLTF material index. we just need to + // construct the correct path to look up the asset handle, then we can spawn an entity with an + // UntypedMaterial component referencing that handle. + let std_path = format!("{}#Material{}/std", base_path, material_index); + let asset_path = resolve_asset_path(&config, &std_path); + let handle: Handle = world.get_asset_server().load(asset_path); + block_on_load(world, |w| w.get_asset_server().load_state(&handle))?; + let entity = world.spawn(UntypedMaterial(handle.untyped())).id(); + Ok(entity) +} + +pub fn mesh_names(In(gltf_entity): In, world: &mut World) -> Result> { + let handle = world + .get::(gltf_entity) + .ok_or(ProcessingError::InvalidEntity)?; + let gltf_handle = handle.handle.clone(); + + let gltf_assets = world.resource::>(); + let gltf = gltf_assets + .get(&gltf_handle) + .ok_or_else(|| ProcessingError::GltfLoadError("GLTF asset not found".into()))?; + Ok(gltf.named_meshes.keys().map(|k| k.to_string()).collect()) +} + +pub fn material_names(In(gltf_entity): In, world: &mut World) -> Result> { + let handle = world + .get::(gltf_entity) + .ok_or(ProcessingError::InvalidEntity)?; + let gltf_handle = handle.handle.clone(); + + let gltf_assets = world.resource::>(); + let gltf = gltf_assets + .get(&gltf_handle) + .ok_or_else(|| ProcessingError::GltfLoadError("GLTF asset not found".into()))?; + Ok(gltf.named_materials.keys().map(|k| k.to_string()).collect()) +} + +fn node_transform(node: &gltf::Node) -> Transform { + let (translation, rotation, scale) = node.transform().decomposed(); + Transform { + translation: Vec3::from(translation), + rotation: Quat::from_array(rotation), + scale: Vec3::from(scale), + } +} + +fn find_node_transform( + source: &gltf::Gltf, + predicate: impl Fn(&gltf::Node) -> bool, +) -> Option { + fn walk(node: gltf::Node, predicate: &impl Fn(&gltf::Node) -> bool) -> Option { + if predicate(&node) { + return Some(node_transform(&node)); + } + for child in node.children() { + if let Some(t) = walk(child, predicate) { + return Some(t); + } + } + None + } + + for scene in source.scenes() { + for node in scene.nodes() { + if let Some(t) = walk(node, &predicate) { + return Some(t); + } + } + } + None +} + +enum CameraProjection { + Perspective { + fov: f32, + aspect_ratio: f32, + near: f32, + far: f32, + }, + Orthographic { + xmag: f32, + ymag: f32, + near: f32, + far: f32, + }, +} + +pub fn camera( + In((gltf_entity, graphics_entity, index)): In<(Entity, Entity, usize)>, + world: &mut World, +) -> Result<()> { + let handle = world + .get::(gltf_entity) + .ok_or(ProcessingError::InvalidEntity)?; + let gltf_handle = handle.handle.clone(); + + let (projection, node_xform) = { + let gltf_assets = world.resource::>(); + let gltf = gltf_assets + .get(&gltf_handle) + .ok_or_else(|| ProcessingError::GltfLoadError("GLTF asset not found".into()))?; + let source = gltf + .source + .as_ref() + .ok_or_else(|| ProcessingError::GltfLoadError("GLTF source not loaded".into()))?; + + let gltf_camera = source.cameras().nth(index).ok_or_else(|| { + ProcessingError::GltfLoadError(format!("Camera index {} not found", index)) + })?; + + let projection = match gltf_camera.projection() { + gltf::camera::Projection::Perspective(p) => CameraProjection::Perspective { + fov: p.yfov(), + aspect_ratio: p.aspect_ratio().unwrap_or(1.0), + near: p.znear(), + far: p.zfar().unwrap_or(10_000.0), + }, + gltf::camera::Projection::Orthographic(o) => CameraProjection::Orthographic { + xmag: o.xmag(), + ymag: o.ymag(), + near: o.znear(), + far: o.zfar(), + }, + }; + + let node_xform = + find_node_transform(source, |n| n.camera().map(|c| c.index()) == Some(index)); + + (projection, node_xform) + }; + + match projection { + CameraProjection::Perspective { + fov, + aspect_ratio, + near, + far, + } => { + world + .run_system_cached_with( + graphics::perspective, + ( + graphics_entity, + PerspectiveProjection { + fov, + aspect_ratio, + near, + far, + near_clip_plane: Vec4::new(0.0, 0.0, -1.0, -near), + }, + ), + ) + .unwrap()?; + } + CameraProjection::Orthographic { + xmag, + ymag, + near, + far, + } => { + world + .run_system_cached_with( + graphics::ortho, + ( + graphics_entity, + graphics::OrthoArgs { + left: -xmag, + right: xmag, + bottom: -ymag, + top: ymag, + near, + far, + }, + ), + ) + .unwrap()?; + } + } + + if let Some(t) = node_xform { + let mut transform = world + .get_mut::(graphics_entity) + .ok_or(ProcessingError::GraphicsNotFound)?; + *transform = t; + } + + Ok(()) +} + +pub fn light( + In((gltf_entity, graphics_entity, index)): In<(Entity, Entity, usize)>, + world: &mut World, +) -> Result { + let handle = world + .get::(gltf_entity) + .ok_or(ProcessingError::InvalidEntity)?; + let gltf_handle = handle.handle.clone(); + + let (light_entity, node_xform) = { + let gltf_assets = world.resource::>(); + let gltf = gltf_assets + .get(&gltf_handle) + .ok_or_else(|| ProcessingError::GltfLoadError("GLTF asset not found".into()))?; + let source = gltf + .source + .as_ref() + .ok_or_else(|| ProcessingError::GltfLoadError("GLTF source not loaded".into()))?; + + let gltf_light = source + .lights() + .and_then(|mut lights| lights.nth(index)) + .ok_or_else(|| { + ProcessingError::GltfLoadError(format!("Light index {} not found", index)) + })?; + + let color = Color::srgb_from_array(gltf_light.color()); + let node_xform = + find_node_transform(source, |n| n.light().map(|l| l.index()) == Some(index)); + + let light_entity = match gltf_light.kind() { + gltf::khr_lights_punctual::Kind::Directional => world + .run_system_cached_with( + light::create_directional, + (graphics_entity, color, gltf_light.intensity()), + ) + .unwrap()?, + gltf::khr_lights_punctual::Kind::Point => world + .run_system_cached_with( + light::create_point, + ( + graphics_entity, + color, + gltf_light.intensity() * core::f32::consts::PI * 4.0, + gltf_light.range().unwrap_or(20.0), + 0.0, + ), + ) + .unwrap()?, + gltf::khr_lights_punctual::Kind::Spot { + inner_cone_angle, + outer_cone_angle, + } => world + .run_system_cached_with( + light::create_spot, + ( + graphics_entity, + color, + gltf_light.intensity() * core::f32::consts::PI * 4.0, + gltf_light.range().unwrap_or(20.0), + 0.0, + inner_cone_angle, + outer_cone_angle, + ), + ) + .unwrap()?, + }; + + (light_entity, node_xform) + }; + + if let Some(t) = node_xform { + let mut transform = world + .get_mut::(light_entity) + .ok_or(ProcessingError::TransformNotFound)?; + *transform = t; + } + + Ok(light_entity) +} diff --git a/crates/processing_render/src/lib.rs b/crates/processing_render/src/lib.rs index 960c9e2..6f73223 100644 --- a/crates/processing_render/src/lib.rs +++ b/crates/processing_render/src/lib.rs @@ -1,6 +1,7 @@ pub mod config; pub mod error; pub mod geometry; +pub mod gltf; mod graphics; pub mod image; pub mod light; @@ -1278,3 +1279,74 @@ pub fn material_destroy(entity: Entity) -> error::Result<()> { .unwrap() }) } + +#[cfg(not(target_arch = "wasm32"))] +pub fn gltf_load(path: &str) -> error::Result { + app_mut(|app| { + app.world_mut() + .run_system_cached_with(gltf::load, path.to_string()) + .unwrap() + }) +} + +#[cfg(not(target_arch = "wasm32"))] +pub fn gltf_geometry(gltf_entity: Entity, name: &str) -> error::Result { + app_mut(|app| { + app.world_mut() + .run_system_cached_with(gltf::geometry, (gltf_entity, name.to_string())) + .unwrap() + }) +} + +#[cfg(not(target_arch = "wasm32"))] +pub fn gltf_material(gltf_entity: Entity, name: &str) -> error::Result { + app_mut(|app| { + app.world_mut() + .run_system_cached_with(gltf::material, (gltf_entity, name.to_string())) + .unwrap() + }) +} + +#[cfg(not(target_arch = "wasm32"))] +pub fn gltf_mesh_names(gltf_entity: Entity) -> error::Result> { + app_mut(|app| { + app.world_mut() + .run_system_cached_with(gltf::mesh_names, gltf_entity) + .unwrap() + }) +} + +#[cfg(not(target_arch = "wasm32"))] +pub fn gltf_material_names(gltf_entity: Entity) -> error::Result> { + app_mut(|app| { + app.world_mut() + .run_system_cached_with(gltf::material_names, gltf_entity) + .unwrap() + }) +} + +#[cfg(not(target_arch = "wasm32"))] +pub fn gltf_camera( + gltf_entity: Entity, + graphics_entity: Entity, + index: usize, +) -> error::Result<()> { + app_mut(|app| { + app.world_mut() + .run_system_cached_with(gltf::camera, (gltf_entity, graphics_entity, index)) + .unwrap() + }) +} + +#[cfg(not(target_arch = "wasm32"))] +pub fn gltf_light( + gltf_entity: Entity, + graphics_entity: Entity, + index: usize, +) -> error::Result { + app_mut(|app| { + app.world_mut() + .run_system_cached_with(gltf::light, (gltf_entity, graphics_entity, index)) + .unwrap() + }) +} diff --git a/examples/gltf_load.rs b/examples/gltf_load.rs new file mode 100644 index 0000000..d325be6 --- /dev/null +++ b/examples/gltf_load.rs @@ -0,0 +1,75 @@ +mod glfw; + +use glfw::GlfwContext; +use processing::prelude::*; +use processing_render::material::MaterialValue; +use processing_render::render::command::DrawCommand; + +fn main() { + match sketch() { + Ok(_) => { + eprintln!("Sketch completed successfully"); + exit(0).unwrap(); + } + Err(e) => { + eprintln!("Sketch error: {:?}", e); + exit(1).unwrap(); + } + }; +} + +fn sketch() -> error::Result<()> { + let width = 800; + let height = 600; + let mut glfw_ctx = GlfwContext::new(width, height)?; + init(Config::default())?; + + let surface = glfw_ctx.create_surface(width, height, 1.0)?; + let graphics = graphics_create(surface, width, height)?; + + let gltf = gltf_load("gltf/Duck.glb")?; + let duck = gltf_geometry(gltf, "LOD3spShape")?; + let duck_mat = gltf_material(gltf, "blinn3-fx")?; + + graphics_mode_3d(graphics)?; + gltf_camera(gltf, graphics, 0)?; + let light = gltf_light(gltf, graphics, 0)?; + + let mut frame: u64 = 0; + + while glfw_ctx.poll_events() { + let t = frame as f32 * 0.02; + + let radius = 150.0; + let lx = t.cos() * radius; + let ly = 150.0; + let lz = t.sin() * radius; + transform_set_position(light, lx, ly, lz)?; + transform_look_at(light, 0.0, 80.0, 0.0)?; + + let r = (t * 0.7).sin() * 0.5 + 0.5; + let g = (t * 0.7 + 2.0).sin() * 0.5 + 0.5; + let b = (t * 0.7 + 4.0).sin() * 0.5 + 0.5; + material_set( + duck_mat, + "base_color", + MaterialValue::Float4([r, g, b, 1.0]), + )?; + + graphics_begin_draw(graphics)?; + + graphics_record_command( + graphics, + DrawCommand::BackgroundColor(bevy::color::Color::srgb(0.1, 0.1, 0.12)), + )?; + + graphics_record_command(graphics, DrawCommand::Material(duck_mat))?; + graphics_record_command(graphics, DrawCommand::Geometry(duck))?; + + graphics_end_draw(graphics)?; + + frame += 1; + } + + Ok(()) +} From 285cd46853311c25b1cafaa697dd3b122c745639 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?charlotte=20=F0=9F=8C=B8?= Date: Tue, 24 Feb 2026 22:29:44 -0800 Subject: [PATCH 2/3] Change approach, find entities in scene. --- Cargo.lock | 1 - assets/gltf/Duck.glb | Bin 120656 -> 120588 bytes crates/processing_pyo3/src/gltf.rs | 13 +- crates/processing_render/Cargo.toml | 1 - crates/processing_render/src/gltf.rs | 417 +++++++++------------ crates/processing_render/src/lib.rs | 20 +- crates/processing_render/src/render/mod.rs | 14 +- examples/gltf_load.rs | 18 +- 8 files changed, 210 insertions(+), 274 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0043a02..33b5d77 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4748,7 +4748,6 @@ version = "0.1.0" dependencies = [ "bevy", "crossbeam-channel", - "gltf", "half", "js-sys", "lyon", diff --git a/assets/gltf/Duck.glb b/assets/gltf/Duck.glb index 39d3fed50e94a9bef9e64f0985975efe68fdc717..7cb0cc15815662d2787563b6cfb6bd56e6b8e8e5 100644 GIT binary patch delta 560 zcmZvZy^0h;5XX1jh2z8nF)`3US})j4lxjE-^O{6Y&9j5o`7o6zrs{{?*m>tEz7oTkkHmc4qTud)G#z(TyMD(TDZU z^Y#1t{pI*e|8sKdOh|E9zN*?%QIPHK-3=^XPop(^j zRK;bTRw5_h!%@L2n=vqLoJ~3d)-hJ?<0e**;%v@Q9*`8xn{*}K{Hh_i7Z&V*!Kh+){mPPf=F z_zZ%@HWoJf2qJcNK7o&*;F+XY(bHVc|Mz|8o?Rs0E|UG}?8#xT)9D<2?{bP#cM`HAFVRhR0gvMU@rHyjrP|Wr~sP&=VOZ z8?D!BIWH?UT1#1xEe^^edr(YgnHtG(3{n-1cY%VjR;%(=l{Fvw*A6X2=D_G9k_kYy zR@G`QUAu8wEz)8tt_RSnNME*@$Fk^AK7COQ0Z`TBy579~a%UpRMJnsA>Lyu{9qhu* zdH2S1Z-S#pft>KO#F5a3=pY)fEoE>)@SsBjNY?qbjFEuBurzrKU`2;YFfC_km6wI6 zX9O8~(DIIJy>5Ob_iqV`TA2KcS2eeLH^;&fEwYa-hyduKccM8*_7XwERiSj}t>)QcInO3lej=)^oA-%&xH>LR)Ch$sA4kcy{l{dJ?qoQ-n+fMdN3Hgx$J+w?01j8{sG7~tfc?| diff --git a/crates/processing_pyo3/src/gltf.rs b/crates/processing_pyo3/src/gltf.rs index 9b7bf0b..be8a9aa 100644 --- a/crates/processing_pyo3/src/gltf.rs +++ b/crates/processing_pyo3/src/gltf.rs @@ -8,7 +8,6 @@ use crate::material::Material; #[pyclass(unsendable)] pub struct Gltf { entity: Entity, - graphics_entity: Entity, } #[pymethods] @@ -34,12 +33,12 @@ impl Gltf { } pub fn camera(&self, index: usize) -> PyResult<()> { - gltf_camera(self.entity, self.graphics_entity, index) + gltf_camera(self.entity, index) .map_err(|e| PyRuntimeError::new_err(format!("{e}"))) } pub fn light(&self, index: usize) -> PyResult { - let entity = gltf_light(self.entity, self.graphics_entity, index) + let entity = gltf_light(self.entity, index) .map_err(|e| PyRuntimeError::new_err(format!("{e}")))?; Ok(Light { entity }) } @@ -49,9 +48,7 @@ impl Gltf { #[pyo3(pass_module)] pub fn load_gltf(module: &Bound<'_, PyModule>, path: &str) -> PyResult { let graphics = get_graphics(module)?; - let entity = gltf_load(path).map_err(|e| PyRuntimeError::new_err(format!("{e}")))?; - Ok(Gltf { - entity, - graphics_entity: graphics.entity, - }) + let entity = gltf_load(graphics.entity, path) + .map_err(|e| PyRuntimeError::new_err(format!("{e}")))?; + Ok(Gltf { entity }) } diff --git a/crates/processing_render/Cargo.toml b/crates/processing_render/Cargo.toml index 68977b8..8999a2c 100644 --- a/crates/processing_render/Cargo.toml +++ b/crates/processing_render/Cargo.toml @@ -13,7 +13,6 @@ x11 = ["bevy/x11"] [dependencies] bevy = { workspace = true } -gltf = { version = "1.4.0", default-features = false, features = ["KHR_lights_punctual", "names"] } lyon = "1.0" raw-window-handle = "0.6" thiserror = "2" diff --git a/crates/processing_render/src/gltf.rs b/crates/processing_render/src/gltf.rs index c33a38d..59477f9 100644 --- a/crates/processing_render/src/gltf.rs +++ b/crates/processing_render/src/gltf.rs @@ -7,17 +7,21 @@ use bevy::{ io::{AssetSourceId, embedded::GetAssetServer}, }, ecs::system::RunSystemOnce, - gltf::{Gltf, GltfLoaderSettings, GltfMesh}, + gltf::{Gltf, GltfMeshName}, prelude::*, + camera::visibility::RenderLayers, + scene::SceneSpawner, }; use crate::config::{Config, ConfigKey}; use crate::error::{ProcessingError, Result}; use crate::geometry::{BuiltinAttributes, Geometry, layout::VertexLayout}; use crate::graphics; -use crate::light; use crate::render::material::UntypedMaterial; +#[derive(Component)] +pub struct GltfNodeTransform(pub Transform); + fn resolve_asset_path(config: &Config, path: &str) -> AssetPath<'static> { let asset_path = AssetPath::parse(path).into_owned(); match config.get(ConfigKey::AssetRootPath) { @@ -26,7 +30,6 @@ fn resolve_asset_path(config: &Config, path: &str) -> AssetPath<'static> { } } -/// Block until an asset handle loads, returning an error if loading fails. fn block_on_load(world: &mut World, load_state: impl Fn(&World) -> LoadState) -> Result<()> { loop { match load_state(world) { @@ -48,37 +51,85 @@ fn block_on_load(world: &mut World, load_state: impl Fn(&World) -> LoadState) -> } } -/// Load the root GLTF asset, blocking until fully loaded. -fn load_gltf_root(world: &mut World, config: &Config, path: &str) -> Result> { - let base_path = match path.find('#') { - Some(idx) => &path[..idx], - None => path, - }; - let asset_path = resolve_asset_path(config, base_path); - let handle: Handle = - world - .get_asset_server() - .load_with_settings(asset_path, |s: &mut GltfLoaderSettings| { - s.include_source = true; - }); - block_on_load(world, |w| w.get_asset_server().load_state(&handle))?; - Ok(handle) +fn compute_global_transform(world: &World, entity: Entity) -> Transform { + let local = world + .get::(entity) + .copied() + .unwrap_or_default(); + match world.get::(entity) { + Some(child_of) => { + let parent_global = compute_global_transform(world, child_of.parent()); + Transform::from_matrix(parent_global.to_matrix() * local.to_matrix()) + } + None => local, + } } #[derive(Component)] pub struct GltfHandle { handle: Handle, + instance_id: bevy::scene::InstanceId, + graphics_entity: Entity, base_path: String, } -pub fn load(In(path): In, world: &mut World) -> Result { +pub fn load( + In((graphics_entity, path)): In<(Entity, String)>, + world: &mut World, +) -> Result { let config = world.resource::().clone(); - let handle = load_gltf_root(world, &config, &path)?; let base_path = match path.find('#') { Some(idx) => path[..idx].to_string(), - None => path, + None => path.clone(), }; - let entity = world.spawn(GltfHandle { handle, base_path }).id(); + let asset_path = resolve_asset_path(&config, &base_path); + let handle: Handle = world.get_asset_server().load(asset_path); + block_on_load(world, |w| w.get_asset_server().load_state(&handle))?; + + let scene_handle = { + let gltf_assets = world.resource::>(); + let gltf = gltf_assets + .get(&handle) + .ok_or_else(|| ProcessingError::GltfLoadError("GLTF asset not found".into()))?; + gltf.default_scene + .clone() + .or_else(|| gltf.scenes.first().cloned()) + .ok_or_else(|| ProcessingError::GltfLoadError("GLTF has no scenes".into()))? + }; + + // we spawn the scene in to the world in a blocking fashion so that bevy runs all + // its hooks for the gltf, ex creating standard material instances + let instance_id = + world.resource_scope(|world, mut spawner: Mut| { + spawner + .spawn_sync(world, &scene_handle) + .map_err(|e| ProcessingError::GltfLoadError(format!("Scene spawn failed: {e}"))) + })?; + + + // we have to remove the existing cameras from the scene -- the user can request to set *this* + // graphics to a camera, but the scenes cameras should not exist + { + let spawner = world.resource::(); + let cam_entities: Vec = spawner + .iter_instance_entities(instance_id) + .filter(|&e| world.get::(e).is_some()) + .collect(); + for e in cam_entities { + // gltf is weird -- cameras can exist on any node. we remove just the camera component rather + // than despawn in order to be safe + world.entity_mut(e).remove::(); + } + } + + let entity = world + .spawn(GltfHandle { + handle, + instance_id, + graphics_entity, + base_path, + }) + .id(); Ok(entity) } @@ -86,30 +137,37 @@ pub fn geometry( In((gltf_entity, name)): In<(Entity, String)>, world: &mut World, ) -> Result { - let handle = world + let gltf_handle = world .get::(gltf_entity) .ok_or(ProcessingError::InvalidEntity)?; - let gltf_handle = handle.handle.clone(); + let instance_id = gltf_handle.instance_id; + + let (mesh_handle, global_transform) = { + let spawner = world.resource::(); + + // find the mesh with the given name component that bevy added post-spawn + // name is derived from gltf node or computed + let mesh_entity = spawner + .iter_instance_entities(instance_id) + .find(|&e| { + world + .get::(e) + .map(|n| n.0 == name) + .unwrap_or(false) + }) + .ok_or_else(|| { + ProcessingError::GltfLoadError(format!("Mesh '{}' not found in GLTF scene", name)) + })?; - let mesh_handle = { - let gltf_assets = world.resource::>(); - let gltf = gltf_assets - .get(&gltf_handle) - .ok_or_else(|| ProcessingError::GltfLoadError("GLTF asset not found".into()))?; - let gltf_mesh_handle = gltf.named_meshes.get(name.as_str()).ok_or_else(|| { - ProcessingError::GltfLoadError(format!("Mesh '{}' not found in GLTF", name)) + let mesh3d = world.get::(mesh_entity).ok_or_else(|| { + ProcessingError::GltfLoadError(format!( + "Mesh '{}' scene entity has no Mesh3d component", + name + )) })?; - let gltf_mesh_assets = world.resource::>(); - let gltf_mesh = gltf_mesh_assets - .get(gltf_mesh_handle) - .ok_or_else(|| ProcessingError::GltfLoadError("GltfMesh asset not found".into()))?; - // TODO: a mesh could have multiple primitives, but for simplicity we'll just take the - // first one here. we could extend the API later to allow users to specify a primitive index - // if needed, or support mesh hierachies via parent/child ptrs on the python classes. - let prim = gltf_mesh.primitives.first().ok_or_else(|| { - ProcessingError::GltfLoadError(format!("Mesh '{}' has no primitives", name)) - })?; - prim.mesh.clone() + let handle = mesh3d.0.clone(); + let transform = compute_global_transform(world, mesh_entity); + (handle, transform) }; let builtins = world.resource::(); @@ -120,7 +178,12 @@ pub fn geometry( builtins.uv, ]; let layout_entity = world.spawn(VertexLayout::with_attributes(attrs)).id(); - let entity = world.spawn(Geometry::new(mesh_handle, layout_entity)).id(); + let entity = world + .spawn(( + Geometry::new(mesh_handle, layout_entity), + GltfNodeTransform(global_transform), + )) + .id(); Ok(entity) } @@ -154,10 +217,6 @@ pub fn material( }; let config = world.resource::().clone(); - // this is a bit hacky but we can leverage the fact that the GLTF loader creates standard - // material assets with predictable labels based on the GLTF material index. we just need to - // construct the correct path to look up the asset handle, then we can spawn an entity with an - // UntypedMaterial component referencing that handle. let std_path = format!("{}#Material{}/std", base_path, material_index); let asset_path = resolve_asset_path(&config, &std_path); let handle: Handle = world.get_asset_server().load(asset_path); @@ -189,238 +248,118 @@ pub fn material_names(In(gltf_entity): In, world: &mut World) -> Result< let gltf = gltf_assets .get(&gltf_handle) .ok_or_else(|| ProcessingError::GltfLoadError("GLTF asset not found".into()))?; - Ok(gltf.named_materials.keys().map(|k| k.to_string()).collect()) -} - -fn node_transform(node: &gltf::Node) -> Transform { - let (translation, rotation, scale) = node.transform().decomposed(); - Transform { - translation: Vec3::from(translation), - rotation: Quat::from_array(rotation), - scale: Vec3::from(scale), - } -} - -fn find_node_transform( - source: &gltf::Gltf, - predicate: impl Fn(&gltf::Node) -> bool, -) -> Option { - fn walk(node: gltf::Node, predicate: &impl Fn(&gltf::Node) -> bool) -> Option { - if predicate(&node) { - return Some(node_transform(&node)); - } - for child in node.children() { - if let Some(t) = walk(child, predicate) { - return Some(t); - } - } - None - } - - for scene in source.scenes() { - for node in scene.nodes() { - if let Some(t) = walk(node, &predicate) { - return Some(t); - } - } - } - None -} - -enum CameraProjection { - Perspective { - fov: f32, - aspect_ratio: f32, - near: f32, - far: f32, - }, - Orthographic { - xmag: f32, - ymag: f32, - near: f32, - far: f32, - }, + Ok(gltf + .named_materials + .keys() + .map(|k| k.to_string()) + .collect()) } pub fn camera( - In((gltf_entity, graphics_entity, index)): In<(Entity, Entity, usize)>, + In((gltf_entity, index)): In<(Entity, usize)>, world: &mut World, ) -> Result<()> { - let handle = world + let gltf_handle = world .get::(gltf_entity) .ok_or(ProcessingError::InvalidEntity)?; - let gltf_handle = handle.handle.clone(); + let instance_id = gltf_handle.instance_id; + let graphics_entity = gltf_handle.graphics_entity; let (projection, node_xform) = { - let gltf_assets = world.resource::>(); - let gltf = gltf_assets - .get(&gltf_handle) - .ok_or_else(|| ProcessingError::GltfLoadError("GLTF asset not found".into()))?; - let source = gltf - .source - .as_ref() - .ok_or_else(|| ProcessingError::GltfLoadError("GLTF source not loaded".into()))?; - - let gltf_camera = source.cameras().nth(index).ok_or_else(|| { - ProcessingError::GltfLoadError(format!("Camera index {} not found", index)) - })?; + let spawner = world.resource::(); + let camera_entity = spawner + .iter_instance_entities(instance_id) + .filter(|&e| world.get::(e).is_some()) + .nth(index) + .ok_or_else(|| { + ProcessingError::GltfLoadError(format!("Camera index {} not found", index)) + })?; - let projection = match gltf_camera.projection() { - gltf::camera::Projection::Perspective(p) => CameraProjection::Perspective { - fov: p.yfov(), - aspect_ratio: p.aspect_ratio().unwrap_or(1.0), - near: p.znear(), - far: p.zfar().unwrap_or(10_000.0), - }, - gltf::camera::Projection::Orthographic(o) => CameraProjection::Orthographic { - xmag: o.xmag(), - ymag: o.ymag(), - near: o.znear(), - far: o.zfar(), - }, - }; - - let node_xform = - find_node_transform(source, |n| n.camera().map(|c| c.index()) == Some(index)); - - (projection, node_xform) + let projection = world + .get::(camera_entity) + .ok_or_else(|| { + ProcessingError::GltfLoadError("Camera entity has no Projection component".into()) + })? + .clone(); + let transform = compute_global_transform(world, camera_entity); + (projection, transform) }; match projection { - CameraProjection::Perspective { - fov, - aspect_ratio, - near, - far, - } => { + Projection::Perspective(p) => { world - .run_system_cached_with( - graphics::perspective, - ( - graphics_entity, - PerspectiveProjection { - fov, - aspect_ratio, - near, - far, - near_clip_plane: Vec4::new(0.0, 0.0, -1.0, -near), - }, - ), - ) + .run_system_cached_with(graphics::perspective, (graphics_entity, p)) .unwrap()?; } - CameraProjection::Orthographic { - xmag, - ymag, - near, - far, - } => { + Projection::Orthographic(o) => { world .run_system_cached_with( graphics::ortho, ( graphics_entity, graphics::OrthoArgs { - left: -xmag, - right: xmag, - bottom: -ymag, - top: ymag, - near, - far, + left: o.area.min.x, + right: o.area.max.x, + bottom: o.area.min.y, + top: o.area.max.y, + near: o.near, + far: o.far, }, ), ) .unwrap()?; } + Projection::Custom(_) => { + return Err(ProcessingError::GltfLoadError( + "Custom projections are not supported".into(), + )); + } } - if let Some(t) = node_xform { - let mut transform = world - .get_mut::(graphics_entity) - .ok_or(ProcessingError::GraphicsNotFound)?; - *transform = t; - } + let mut transform = world + .get_mut::(graphics_entity) + .ok_or(ProcessingError::GraphicsNotFound)?; + *transform = node_xform; Ok(()) } pub fn light( - In((gltf_entity, graphics_entity, index)): In<(Entity, Entity, usize)>, + In((gltf_entity, index)): In<(Entity, usize)>, world: &mut World, ) -> Result { - let handle = world + let gltf_handle = world .get::(gltf_entity) .ok_or(ProcessingError::InvalidEntity)?; - let gltf_handle = handle.handle.clone(); + let instance_id = gltf_handle.instance_id; + let graphics_entity = gltf_handle.graphics_entity; + + let light_entities: Vec = { + let spawner = world.resource::(); + spawner + .iter_instance_entities(instance_id) + .filter(|&e| { + world.get::(e).is_some() + || world.get::(e).is_some() + || world.get::(e).is_some() + }) + .collect() + }; - let (light_entity, node_xform) = { - let gltf_assets = world.resource::>(); - let gltf = gltf_assets - .get(&gltf_handle) - .ok_or_else(|| ProcessingError::GltfLoadError("GLTF asset not found".into()))?; - let source = gltf - .source - .as_ref() - .ok_or_else(|| ProcessingError::GltfLoadError("GLTF source not loaded".into()))?; - - let gltf_light = source - .lights() - .and_then(|mut lights| lights.nth(index)) - .ok_or_else(|| { - ProcessingError::GltfLoadError(format!("Light index {} not found", index)) - })?; + let scene_light_entity = *light_entities.get(index).ok_or_else(|| { + ProcessingError::GltfLoadError(format!("Light index {} not found", index)) + })?; - let color = Color::srgb_from_array(gltf_light.color()); - let node_xform = - find_node_transform(source, |n| n.light().map(|l| l.index()) == Some(index)); + let render_layers = world + .get::(graphics_entity) + .ok_or(ProcessingError::GraphicsNotFound)? + .clone(); + world.entity_mut(scene_light_entity).insert(render_layers); - let light_entity = match gltf_light.kind() { - gltf::khr_lights_punctual::Kind::Directional => world - .run_system_cached_with( - light::create_directional, - (graphics_entity, color, gltf_light.intensity()), - ) - .unwrap()?, - gltf::khr_lights_punctual::Kind::Point => world - .run_system_cached_with( - light::create_point, - ( - graphics_entity, - color, - gltf_light.intensity() * core::f32::consts::PI * 4.0, - gltf_light.range().unwrap_or(20.0), - 0.0, - ), - ) - .unwrap()?, - gltf::khr_lights_punctual::Kind::Spot { - inner_cone_angle, - outer_cone_angle, - } => world - .run_system_cached_with( - light::create_spot, - ( - graphics_entity, - color, - gltf_light.intensity() * core::f32::consts::PI * 4.0, - gltf_light.range().unwrap_or(20.0), - 0.0, - inner_cone_angle, - outer_cone_angle, - ), - ) - .unwrap()?, - }; - - (light_entity, node_xform) - }; - - if let Some(t) = node_xform { - let mut transform = world - .get_mut::(light_entity) - .ok_or(ProcessingError::TransformNotFound)?; - *transform = t; - } + let global = compute_global_transform(world, scene_light_entity); + *world + .get_mut::(scene_light_entity) + .ok_or(ProcessingError::GraphicsNotFound)? = global; - Ok(light_entity) + Ok(scene_light_entity) } diff --git a/crates/processing_render/src/lib.rs b/crates/processing_render/src/lib.rs index 6f73223..2af81d8 100644 --- a/crates/processing_render/src/lib.rs +++ b/crates/processing_render/src/lib.rs @@ -1281,10 +1281,10 @@ pub fn material_destroy(entity: Entity) -> error::Result<()> { } #[cfg(not(target_arch = "wasm32"))] -pub fn gltf_load(path: &str) -> error::Result { +pub fn gltf_load(graphics_entity: Entity, path: &str) -> error::Result { app_mut(|app| { app.world_mut() - .run_system_cached_with(gltf::load, path.to_string()) + .run_system_cached_with(gltf::load, (graphics_entity, path.to_string())) .unwrap() }) } @@ -1326,27 +1326,19 @@ pub fn gltf_material_names(gltf_entity: Entity) -> error::Result> { } #[cfg(not(target_arch = "wasm32"))] -pub fn gltf_camera( - gltf_entity: Entity, - graphics_entity: Entity, - index: usize, -) -> error::Result<()> { +pub fn gltf_camera(gltf_entity: Entity, index: usize) -> error::Result<()> { app_mut(|app| { app.world_mut() - .run_system_cached_with(gltf::camera, (gltf_entity, graphics_entity, index)) + .run_system_cached_with(gltf::camera, (gltf_entity, index)) .unwrap() }) } #[cfg(not(target_arch = "wasm32"))] -pub fn gltf_light( - gltf_entity: Entity, - graphics_entity: Entity, - index: usize, -) -> error::Result { +pub fn gltf_light(gltf_entity: Entity, index: usize) -> error::Result { app_mut(|app| { app.world_mut() - .run_system_cached_with(gltf::light, (gltf_entity, graphics_entity, index)) + .run_system_cached_with(gltf::light, (gltf_entity, index)) .unwrap() }) } diff --git a/crates/processing_render/src/render/mod.rs b/crates/processing_render/src/render/mod.rs index 5e8471f..17144fe 100644 --- a/crates/processing_render/src/render/mod.rs +++ b/crates/processing_render/src/render/mod.rs @@ -18,6 +18,7 @@ use transform::TransformStack; use crate::{ Flush, geometry::Geometry, + gltf::GltfNodeTransform, image::Image, render::{material::UntypedMaterial, primitive::rect}, }; @@ -122,7 +123,7 @@ pub fn flush_draw_commands( With, >, p_images: Query<&Image>, - p_geometries: Query<&Geometry>, + p_geometries: Query<(&Geometry, Option<&GltfNodeTransform>)>, p_material_handles: Query<&UntypedMaterial>, ) { for (graphics_entity, mut cmd_buffer, mut state, render_layers, projection, camera_transform) in @@ -297,7 +298,7 @@ pub fn flush_draw_commands( DrawCommand::ShearX { angle } => state.transform.shear_x(angle), DrawCommand::ShearY { angle } => state.transform.shear_y(angle), DrawCommand::Geometry(entity) => { - let Some(geometry) = p_geometries.get(entity).ok() else { + let Some((geometry, node_transform)) = p_geometries.get(entity).ok() else { warn!("Could not find Geometry for entity {:?}", entity); continue; }; @@ -318,6 +319,15 @@ pub fn flush_draw_commands( let z_offset = -(batch.draw_index as f32 * 0.001); let mut transform = state.transform.to_bevy_transform(); + + // if the "source" geometry was parented in a gltf scene, we need to make sure that + // we apply the parent transform here to ensure the correct final transform + // TODO: think about how hierarchies should work, especially for retained + if let Some(nt) = node_transform { + transform = Transform::from_matrix( + transform.to_matrix() * nt.0.to_matrix(), + ); + } transform.translation.z += z_offset; res.commands.spawn(( diff --git a/examples/gltf_load.rs b/examples/gltf_load.rs index d325be6..8abf3f4 100644 --- a/examples/gltf_load.rs +++ b/examples/gltf_load.rs @@ -27,29 +27,29 @@ fn sketch() -> error::Result<()> { let surface = glfw_ctx.create_surface(width, height, 1.0)?; let graphics = graphics_create(surface, width, height)?; - let gltf = gltf_load("gltf/Duck.glb")?; + let gltf = gltf_load(graphics, "gltf/Duck.glb")?; let duck = gltf_geometry(gltf, "LOD3spShape")?; let duck_mat = gltf_material(gltf, "blinn3-fx")?; graphics_mode_3d(graphics)?; - gltf_camera(gltf, graphics, 0)?; - let light = gltf_light(gltf, graphics, 0)?; + gltf_camera(gltf, 0)?; + let light = gltf_light(gltf, 0)?; let mut frame: u64 = 0; while glfw_ctx.poll_events() { let t = frame as f32 * 0.02; - let radius = 150.0; + let radius = 1.5; let lx = t.cos() * radius; - let ly = 150.0; + let ly = 1.5; let lz = t.sin() * radius; transform_set_position(light, lx, ly, lz)?; - transform_look_at(light, 0.0, 80.0, 0.0)?; + transform_look_at(light, 0.0, 0.8, 0.0)?; - let r = (t * 0.7).sin() * 0.5 + 0.5; - let g = (t * 0.7 + 2.0).sin() * 0.5 + 0.5; - let b = (t * 0.7 + 4.0).sin() * 0.5 + 0.5; + let r = (t * 8.0).sin() * 0.5 + 0.5; + let g = (t * 8.0 + 2.0).sin() * 0.5 + 0.5; + let b = (t * 8.0 + 4.0).sin() * 0.5 + 0.5; material_set( duck_mat, "base_color", From 4e6abbb02b6b603081a5710080777fdc144cbcae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?charlotte=20=F0=9F=8C=B8?= Date: Tue, 24 Feb 2026 22:31:54 -0800 Subject: [PATCH 3/3] Fmt. --- crates/processing_pyo3/examples/gltf_load.py | 12 +++---- crates/processing_pyo3/src/gltf.rs | 11 +++--- crates/processing_render/src/gltf.rs | 37 ++++++-------------- crates/processing_render/src/render/mod.rs | 7 ++-- 4 files changed, 25 insertions(+), 42 deletions(-) diff --git a/crates/processing_pyo3/examples/gltf_load.py b/crates/processing_pyo3/examples/gltf_load.py index affb9c3..9f8e0ff 100644 --- a/crates/processing_pyo3/examples/gltf_load.py +++ b/crates/processing_pyo3/examples/gltf_load.py @@ -23,16 +23,16 @@ def draw(): global frame t = frame * 0.02 - radius = 150.0 + radius = 1.5 lx = math.cos(t) * radius - ly = 150.0 + ly = 1.5 lz = math.sin(t) * radius light.position(lx, ly, lz) - light.look_at(0.0, 80.0, 0.0) + light.look_at(0.0, 0.8, 0.0) - r = math.sin(t * 0.7) * 0.5 + 0.5 - g = math.sin(t * 0.7 + 2.0) * 0.5 + 0.5 - b = math.sin(t * 0.7 + 4.0) * 0.5 + 0.5 + r = math.sin(t * 8.0) * 0.5 + 0.5 + g = math.sin(t * 8.0 + 2.0) * 0.5 + 0.5 + b = math.sin(t * 8.0 + 4.0) * 0.5 + 0.5 duck_mat.set_float4("base_color", r, g, b, 1.0) background(25) diff --git a/crates/processing_pyo3/src/gltf.rs b/crates/processing_pyo3/src/gltf.rs index be8a9aa..93dd6b3 100644 --- a/crates/processing_pyo3/src/gltf.rs +++ b/crates/processing_pyo3/src/gltf.rs @@ -33,13 +33,12 @@ impl Gltf { } pub fn camera(&self, index: usize) -> PyResult<()> { - gltf_camera(self.entity, index) - .map_err(|e| PyRuntimeError::new_err(format!("{e}"))) + gltf_camera(self.entity, index).map_err(|e| PyRuntimeError::new_err(format!("{e}"))) } pub fn light(&self, index: usize) -> PyResult { - let entity = gltf_light(self.entity, index) - .map_err(|e| PyRuntimeError::new_err(format!("{e}")))?; + let entity = + gltf_light(self.entity, index).map_err(|e| PyRuntimeError::new_err(format!("{e}")))?; Ok(Light { entity }) } } @@ -48,7 +47,7 @@ impl Gltf { #[pyo3(pass_module)] pub fn load_gltf(module: &Bound<'_, PyModule>, path: &str) -> PyResult { let graphics = get_graphics(module)?; - let entity = gltf_load(graphics.entity, path) - .map_err(|e| PyRuntimeError::new_err(format!("{e}")))?; + let entity = + gltf_load(graphics.entity, path).map_err(|e| PyRuntimeError::new_err(format!("{e}")))?; Ok(Gltf { entity }) } diff --git a/crates/processing_render/src/gltf.rs b/crates/processing_render/src/gltf.rs index 59477f9..47056cc 100644 --- a/crates/processing_render/src/gltf.rs +++ b/crates/processing_render/src/gltf.rs @@ -6,10 +6,10 @@ use bevy::{ AssetPath, LoadState, handle_internal_asset_events, io::{AssetSourceId, embedded::GetAssetServer}, }, + camera::visibility::RenderLayers, ecs::system::RunSystemOnce, gltf::{Gltf, GltfMeshName}, prelude::*, - camera::visibility::RenderLayers, scene::SceneSpawner, }; @@ -52,10 +52,7 @@ fn block_on_load(world: &mut World, load_state: impl Fn(&World) -> LoadState) -> } fn compute_global_transform(world: &World, entity: Entity) -> Transform { - let local = world - .get::(entity) - .copied() - .unwrap_or_default(); + let local = world.get::(entity).copied().unwrap_or_default(); match world.get::(entity) { Some(child_of) => { let parent_global = compute_global_transform(world, child_of.parent()); @@ -99,14 +96,12 @@ pub fn load( // we spawn the scene in to the world in a blocking fashion so that bevy runs all // its hooks for the gltf, ex creating standard material instances - let instance_id = - world.resource_scope(|world, mut spawner: Mut| { - spawner - .spawn_sync(world, &scene_handle) - .map_err(|e| ProcessingError::GltfLoadError(format!("Scene spawn failed: {e}"))) - })?; + let instance_id = world.resource_scope(|world, mut spawner: Mut| { + spawner + .spawn_sync(world, &scene_handle) + .map_err(|e| ProcessingError::GltfLoadError(format!("Scene spawn failed: {e}"))) + })?; - // we have to remove the existing cameras from the scene -- the user can request to set *this* // graphics to a camera, but the scenes cameras should not exist { @@ -144,7 +139,7 @@ pub fn geometry( let (mesh_handle, global_transform) = { let spawner = world.resource::(); - + // find the mesh with the given name component that bevy added post-spawn // name is derived from gltf node or computed let mesh_entity = spawner @@ -248,17 +243,10 @@ pub fn material_names(In(gltf_entity): In, world: &mut World) -> Result< let gltf = gltf_assets .get(&gltf_handle) .ok_or_else(|| ProcessingError::GltfLoadError("GLTF asset not found".into()))?; - Ok(gltf - .named_materials - .keys() - .map(|k| k.to_string()) - .collect()) + Ok(gltf.named_materials.keys().map(|k| k.to_string()).collect()) } -pub fn camera( - In((gltf_entity, index)): In<(Entity, usize)>, - world: &mut World, -) -> Result<()> { +pub fn camera(In((gltf_entity, index)): In<(Entity, usize)>, world: &mut World) -> Result<()> { let gltf_handle = world .get::(gltf_entity) .ok_or(ProcessingError::InvalidEntity)?; @@ -324,10 +312,7 @@ pub fn camera( Ok(()) } -pub fn light( - In((gltf_entity, index)): In<(Entity, usize)>, - world: &mut World, -) -> Result { +pub fn light(In((gltf_entity, index)): In<(Entity, usize)>, world: &mut World) -> Result { let gltf_handle = world .get::(gltf_entity) .ok_or(ProcessingError::InvalidEntity)?; diff --git a/crates/processing_render/src/render/mod.rs b/crates/processing_render/src/render/mod.rs index 17144fe..eb8cc0b 100644 --- a/crates/processing_render/src/render/mod.rs +++ b/crates/processing_render/src/render/mod.rs @@ -319,14 +319,13 @@ pub fn flush_draw_commands( let z_offset = -(batch.draw_index as f32 * 0.001); let mut transform = state.transform.to_bevy_transform(); - + // if the "source" geometry was parented in a gltf scene, we need to make sure that // we apply the parent transform here to ensure the correct final transform // TODO: think about how hierarchies should work, especially for retained if let Some(nt) = node_transform { - transform = Transform::from_matrix( - transform.to_matrix() * nt.0.to_matrix(), - ); + transform = + Transform::from_matrix(transform.to_matrix() * nt.0.to_matrix()); } transform.translation.z += z_offset;