diff --git a/examples/hello/hello.fs b/examples/hello/hello.fs index 771b913..1571407 100644 --- a/examples/hello/hello.fs +++ b/examples/hello/hello.fs @@ -90,21 +90,22 @@ let init (_args: array) = // let barrelModel = Model.file("ExplodingBarrel.glb"); // let renderModel = (Graphics.Scene3D.model barrelModel) |> Transform.scale 1f; // let renderModel = Model.file ("ExplodingBarrel.glb") |> Graphics.Scene3D.model |> Transform.scale 0.5f; - let modify = Model.modify (MeshSelector.all ()) (MeshOverride.material (textureMaterial)); - let renderModel = Model.file ("vr_glove_model2.glb") |> modify |> Graphics.Scene3D.model |> Transform.scale 5f; + // let modify = Model.modify (MeshSelector.all ()) (MeshOverride.material (textureMaterial)); + // let renderModel = Model.file ("vr_glove_model2.glb") |> modify |> Graphics.Scene3D.model |> Transform.scale 5f; - // let renderModel = - // "shark.glb" - // |> Model.file - // |> Graphics.Scene3D.model - // |> Transform.scale 0.001f; + let renderModel = + "shark.glb" + |> Model.file + |> Graphics.Scene3D.model + |> Transform.translateZ 10.0f + |> Transform.scale 0.004f; group([| material (textureMaterial, [| cylinder() |> Transform.translateY -1.0f; renderModel - |> Transform.rotateY (Math.Angle.degrees (frameTime.tts * 20.0f)) - |> Transform.rotateZ (Math.Angle.degrees (frameTime.tts * 40.0f)) + |> Transform.rotateY (Math.Angle.degrees (180.0f + 10.0f * frameTime.tts * 0.5f)) + |> Transform.rotateX (Math.Angle.degrees (0.0f * sin frameTime.tts * 2.0f)) |]) |> Transform.translateZ ((sin (frameTime.tts * 5.0f)) * 1.0f) |]) diff --git a/runtime/functor-runtime-common/src/animation.rs b/runtime/functor-runtime-common/src/animation.rs new file mode 100644 index 0000000..795d3a7 --- /dev/null +++ b/runtime/functor-runtime-common/src/animation.rs @@ -0,0 +1,36 @@ +use cgmath::{Quaternion, Vector3}; + +pub struct Animation { + pub name: String, + pub channels: Vec, + pub duration: f32, +} + +pub enum AnimationProperty { + Translation, + Rotation, + Scale, + // TODO: Morph target + Weights, +} + +pub struct AnimationChannel { + pub target_node_index: usize, + pub target_property: AnimationProperty, + pub keyframes: Vec, + // interpolation: TODO? +} + +#[derive(Clone)] +pub struct Keyframe { + pub time: f32, + pub value: AnimationValue, +} + +#[derive(Clone)] +pub enum AnimationValue { + Translation(Vector3), + Rotation(Quaternion), + Scale(Vector3), + Weights(Vec), +} diff --git a/runtime/functor-runtime-common/src/asset/pipelines/model_pipeline.rs b/runtime/functor-runtime-common/src/asset/pipelines/model_pipeline.rs index b4a31a6..a3bd9fe 100644 --- a/runtime/functor-runtime-common/src/asset/pipelines/model_pipeline.rs +++ b/runtime/functor-runtime-common/src/asset/pipelines/model_pipeline.rs @@ -1,9 +1,11 @@ use std::io::Cursor; -use cgmath::{vec2, vec3, Matrix4}; +use cgmath::num_traits::ToPrimitive; +use cgmath::{vec2, vec3, Matrix4, Quaternion}; use gltf::{buffer::Source as BufferSource, image::Source as ImageSource}; -use crate::model::{Model, ModelMesh}; +use crate::animation::{Animation, AnimationChannel, AnimationProperty, AnimationValue, Keyframe}; +use crate::model::{Model, ModelMesh, Skeleton, SkeletonBuilder}; use crate::{ asset::{AssetCache, AssetPipeline}, geometry::IndexedMesh, @@ -67,19 +69,39 @@ impl AssetPipeline for ModelPipeline { let mut meshes = Vec::new(); + let mut maybe_skeleton: Option = None; + for scene in document.scenes() { println!("Scene {}", scene.index()); for node in scene.nodes() { println!("- Node: {:?}", node.name()); - process_node(&node, &buffers_data, &images_data, &mut meshes); + process_node( + &node, + &buffers_data, + &images_data, + &mut meshes, + &mut maybe_skeleton, + ); } } - Model { meshes } + let animations = process_animations(&document, &buffers_data); + + let skeleton = maybe_skeleton.unwrap_or(Skeleton::empty()); + + Model { + meshes, + skeleton, + animations, + } } fn unloaded_asset(&self, _context: crate::asset::AssetPipelineContext) -> Model { - Model { meshes: vec![] } + Model { + meshes: vec![], + skeleton: Skeleton::empty(), + animations: vec![], + } } } @@ -88,6 +110,7 @@ fn process_node( buffers: &[gltf::buffer::Data], images: &[TextureData], meshes: &mut Vec, + maybe_skeleton: &mut Option, ) { if let Some(mesh) = node.mesh() { let transform_array = node.transform().matrix(); @@ -115,6 +138,16 @@ fn process_node( let scale = 1.0; + let joints = reader + .read_joints(0) + .map(|v| v.into_u16().collect::>()) + .unwrap_or_default(); + + let weights = reader + .read_weights(0) + .map(|v| v.into_f32().collect::>()) + .unwrap_or_default(); + let vertices: Vec = positions .iter() .zip(tex_coords.into_iter()) @@ -124,10 +157,12 @@ fn process_node( }) .collect(); println!( - "-- Mesh: {:?} vertices: {} indices: {}", + "-- Mesh: {:?} vertices: {} indices: {} joints: {} weights: {}", mesh.name(), vertices.len(), - indices.len() + indices.len(), + joints.len(), + weights.len(), ); // Parse material @@ -167,7 +202,152 @@ fn process_node( meshes.push(model_mesh); } } + + // Process skinning data + if let Some(skin) = node.skin() { + let reader = skin.reader(|buffer| Some(&buffers[buffer.index()])); + + // TODO: Save inverse bind matrices with model + let _inverse_bind_matrices = reader + .read_inverse_bind_matrices() + .map(|v| v.collect::>()) + .unwrap_or_default(); + + let mut skeleton_builder = SkeletonBuilder::create(); + + let maybe_root = skin.skeleton(); + + if let Some(root) = maybe_root { + process_joints(&root, None, &mut skeleton_builder); + } + + *maybe_skeleton = Some(skeleton_builder.build()); + } + for child in node.children() { - process_node(&child, buffers, images, meshes); + process_node(&child, buffers, images, meshes, maybe_skeleton); + } +} + +fn process_joints( + node: &gltf::Node, + parent_id: Option, + skeleton_builder: &mut SkeletonBuilder, +) { + let id = node.index().to_i32().unwrap(); + let name = node.name().unwrap_or("None"); + let transform = node.transform().matrix().into(); + skeleton_builder.add_joint(id, name.to_owned(), parent_id, transform); + + for node in node.children() { + process_joints(&node, Some(id), skeleton_builder); + } +} + +fn process_animations(document: &gltf::Document, buffers: &[gltf::buffer::Data]) -> Vec { + let mut animations = Vec::new(); + // Load animations + // From: https://whoisryosuke.com/blog/2022/importing-gltf-with-wgpu-and-rust + let len = document.animations().len(); + for animation in document.animations() { + let animation_name = animation.name().unwrap_or("Unnamed Animation").to_owned(); + let mut channels = Vec::new(); + let mut max_time = 0.0; + + for channel in animation.channels() { + let sampler = channel.sampler(); + let target = channel.target(); + let node_index = target.node().index(); + let property = match target.property() { + gltf::animation::Property::Translation => AnimationProperty::Translation, + gltf::animation::Property::Rotation => AnimationProperty::Rotation, + gltf::animation::Property::Scale => AnimationProperty::Scale, + gltf::animation::Property::MorphTargetWeights => AnimationProperty::Weights, + }; + + let reader = channel.reader(|buffer| Some(&buffers[buffer.index()])); + + let input_times: Vec = reader + .read_inputs() + .expect("Failed to read animation input") + .collect(); + + let output_values = reader + .read_outputs() + .expect("Failed to read animation output"); + + let interpolation = sampler.interpolation(); + + let mut keyframes = Vec::new(); + max_time = input_times + .iter() + .cloned() + .fold(max_time, |a: f32, b: f32| a.max(b)); + + match output_values { + gltf::animation::util::ReadOutputs::Translations(translations) => { + for (i, translation) in translations.enumerate() { + let time = input_times[i]; + keyframes.push(Keyframe { + time, + value: AnimationValue::Translation(vec3( + translation[0], + translation[1], + translation[2], + )), + }); + } + } + gltf::animation::util::ReadOutputs::Rotations(rotations) => { + for (i, rotation) in rotations.into_f32().enumerate() { + let time = input_times[i]; + keyframes.push(Keyframe { + time, + // TODO: Does w come first or last? + value: AnimationValue::Rotation(Quaternion { + v: vec3(rotation[0], rotation[1], rotation[2]), + s: rotation[3], + }), + }); + } + } + gltf::animation::util::ReadOutputs::Scales(scales) => { + for (i, scale) in scales.enumerate() { + let time = input_times[i]; + keyframes.push(Keyframe { + time, + value: AnimationValue::Scale(vec3(scale[0], scale[1], scale[2])), + }); + } + } + gltf::animation::util::ReadOutputs::MorphTargetWeights(weights) => { + // TODO: + println!("WARN: ignoring morph target weights; not implemented"); + // for (i, weight) in weights.enumerate() { + // let time = input_times[i]; + // keyframes.push(Keyframe { + // time, + // value: AnimationValue::Weights(weight.to_vec()), + // }); + // } + } + } + + channels.push(AnimationChannel { + target_node_index: node_index, + target_property: property, + keyframes, + // TODO: + //interpolation, + }); + } + + animations.push(Animation { + name: animation_name, + channels, + duration: max_time, + }); } + // panic!("animations"); + animations } diff --git a/runtime/functor-runtime-common/src/lib.rs b/runtime/functor-runtime-common/src/lib.rs index b2200f0..b2876c5 100644 --- a/runtime/functor-runtime-common/src/lib.rs +++ b/runtime/functor-runtime-common/src/lib.rs @@ -48,6 +48,7 @@ impl OpaqueState { } } +pub mod animation; pub mod asset; mod frame_time; pub mod geometry; diff --git a/runtime/functor-runtime-common/src/model/mod.rs b/runtime/functor-runtime-common/src/model/mod.rs index a4239dc..8dc3d2a 100644 --- a/runtime/functor-runtime-common/src/model/mod.rs +++ b/runtime/functor-runtime-common/src/model/mod.rs @@ -1,6 +1,12 @@ +mod skeleton; + +use crate::{ + animation::Animation, geometry::IndexedMesh, render::vertex::VertexPositionTexture, + texture::Texture2D, +}; use cgmath::Matrix4; -use crate::{geometry::IndexedMesh, render::vertex::VertexPositionTexture, texture::Texture2D}; +pub use skeleton::*; pub struct ModelMesh { // Material info @@ -13,4 +19,8 @@ pub struct ModelMesh { pub struct Model { pub meshes: Vec, + + pub skeleton: Skeleton, + + pub animations: Vec, } diff --git a/runtime/functor-runtime-common/src/model/skeleton.rs b/runtime/functor-runtime-common/src/model/skeleton.rs new file mode 100644 index 0000000..8ca0913 --- /dev/null +++ b/runtime/functor-runtime-common/src/model/skeleton.rs @@ -0,0 +1,348 @@ +use std::collections::{HashMap, HashSet}; + +use cgmath::{ + Decomposed, InnerSpace, Matrix3, Matrix4, Quaternion, SquareMatrix, Vector3, VectorSpace, +}; + +use crate::animation::{Animation, AnimationValue, Keyframe}; + +#[derive(Clone, Debug)] +pub struct Joint { + pub name: String, + pub transform: Matrix4, + pub parent: Option, +} + +#[derive(Clone, Debug)] +pub struct Skeleton { + num_joints: i32, + + // Use HashMap for joints because they could be sparse + joint_info: HashMap, + joint_absolute_transform: HashMap>, +} + +impl Skeleton { + pub fn empty() -> Skeleton { + Skeleton { + num_joints: 0, + joint_info: HashMap::new(), + joint_absolute_transform: HashMap::new(), + } + } + + pub fn animate(skeleton: &Skeleton, animation: &Animation, time: f32) -> Skeleton { + // Start by cloning the existing skeleton + let mut new_skeleton = skeleton.clone(); + + // For each joint, store the base T, R, S, and the animated T, R, S + let mut joint_base_TRS = HashMap::new(); + let mut joint_animated_TRS = HashMap::new(); + + // First, for each joint, extract base T, R, S + for (&joint_index, joint) in &skeleton.joint_info { + // Extract translation + let base_T = joint.transform.w.truncate(); // last column + + // Extract the upper-left 3x3 matrix + let m = Matrix3::from_cols( + joint.transform.x.truncate(), + joint.transform.y.truncate(), + joint.transform.z.truncate(), + ); + + // Extract scale factors + let scale_x = m.x.magnitude(); + let scale_y = m.y.magnitude(); + let scale_z = m.z.magnitude(); + let base_S = Vector3::new(scale_x, scale_y, scale_z); + + // Normalize the columns to get the rotation matrix + let rotation_matrix = Matrix3::from_cols(m.x / scale_x, m.y / scale_y, m.z / scale_z); + + // Convert rotation matrix to Quaternion + let base_R = Quaternion::from(rotation_matrix); + + joint_base_TRS.insert(joint_index, (base_T, base_R, base_S)); + // Initialize animated T, R, S to base T, R, S + joint_animated_TRS.insert(joint_index, (base_T, base_R, base_S)); + } + + // For each animation channel + for channel in &animation.channels { + let target_node_index = channel.target_node_index as i32; + // Get the animated value at time t + if let Some(value) = interpolate_keyframes(&channel.keyframes, time) { + // Update the animated T, R, S for the joint + if let Some((animated_T, animated_R, animated_S)) = + joint_animated_TRS.get_mut(&target_node_index) + { + match value { + AnimationValue::Translation(translation) => { + *animated_T = translation; + } + AnimationValue::Rotation(rotation) => { + *animated_R = rotation; + } + AnimationValue::Scale(scale) => { + *animated_S = scale; + } + _ => {} + } + } + } + } + + // For each joint, reconstruct the animated transform + for (&joint_index, joint) in new_skeleton.joint_info.iter_mut() { + if let Some((animated_T, animated_R, animated_S)) = joint_animated_TRS.get(&joint_index) + { + // Construct the transform from animated_T, animated_R, animated_S + let transform = Matrix4::from_translation(*animated_T) + * Matrix4::from(*animated_R) + * Matrix4::from_nonuniform_scale(animated_S.x, animated_S.y, animated_S.z); + joint.transform = transform; + } + } + + // Recompute absolute transforms + new_skeleton.joint_absolute_transform = + compute_absolute_transforms(&new_skeleton.joint_info); + + // Return the new skeleton + new_skeleton + } + + pub fn get_joint_count(&self) -> i32 { + self.num_joints + } + + pub fn get_joint_name(&self, idx: i32) -> Option<&str> { + self.joint_info.get(&idx).map(|m| m.name.as_str()) + } + + pub fn get_joint_relative_transform(&self, idx: i32) -> Matrix4 { + self.joint_info + .get(&idx) + .map(|m| m.transform) + .unwrap_or(Matrix4::identity()) + } + + pub fn get_joint_absolute_transform(&self, idx: i32) -> Matrix4 { + self.joint_absolute_transform + .get(&idx) + .map(|m| *m) + .unwrap_or(Matrix4::identity()) + } + + pub fn get_transforms(&self) -> Vec> { + let mut vec = Vec::new(); + for i in 0..self.num_joints { + vec.push(self.get_joint_absolute_transform(i)) + } + vec + } +} + +pub struct SkeletonBuilder { + skeleton: Skeleton, +} + +impl SkeletonBuilder { + pub fn create() -> SkeletonBuilder { + SkeletonBuilder { + skeleton: Skeleton { + num_joints: 0, + joint_info: HashMap::new(), + joint_absolute_transform: HashMap::new(), + }, + } + } + + pub fn add_joint( + &mut self, + joint_index: i32, + name: String, + parent_index: Option, + transform: Matrix4, + ) { + let joint = Joint { + name, + transform, + parent: parent_index, + }; + self.skeleton.joint_info.insert(joint_index, joint); + } + + pub fn build(mut self) -> Skeleton { + // Compute absolute transforms + let joint_absolute_transform = compute_absolute_transforms(&self.skeleton.joint_info); + + // Update the skeleton with the computed absolute transforms + self.skeleton.joint_absolute_transform = joint_absolute_transform; + + let num_joints = self.skeleton.joint_info.keys().max(); + + // Return the built skeleton + Skeleton { + num_joints: num_joints.map(|n| n + 1).unwrap_or(0), + ..self.skeleton + } + } +} + +fn compute_absolute_transforms(joint_info: &HashMap) -> HashMap> { + let mut joint_absolute_transform = HashMap::new(); + + for &joint_index in joint_info.keys() { + compute_joint_absolute_transform( + joint_index, + joint_info, + &mut joint_absolute_transform, + &mut HashSet::new(), // For cycle detection + ); + } + + joint_absolute_transform +} + +fn compute_joint_absolute_transform( + joint_index: i32, + joint_info: &HashMap, + joint_absolute_transform: &mut HashMap>, + visited: &mut HashSet, +) -> Matrix4 { + // Check for cycles + if !visited.insert(joint_index) { + panic!("Cycle detected in joint hierarchy at joint {}", joint_index); + } + + // Return cached value if already computed + if let Some(&abs_transform) = joint_absolute_transform.get(&joint_index) { + visited.remove(&joint_index); + return abs_transform; + } + + // Get the joint + let joint = joint_info.get(&joint_index).expect("Joint not found"); + + // Compute the absolute transform + let abs_transform = if let Some(parent_index) = joint.parent { + let parent_abs_transform = compute_joint_absolute_transform( + parent_index, + joint_info, + joint_absolute_transform, + visited, + ); + parent_abs_transform * joint.transform + } else { + // No parent, so absolute transform is the joint's transform + joint.transform + }; + + // Store the computed absolute transform + joint_absolute_transform.insert(joint_index, abs_transform); + + // Remove from visited set + visited.remove(&joint_index); + + abs_transform +} + +fn interpolate_keyframes(keyframes: &Vec, time: f32) -> Option { + if keyframes.is_empty() { + return None; + } + // If time is before the first keyframe, return the first value + if time <= keyframes[0].time { + return Some(keyframes[0].value.clone()); + } + // If time is after the last keyframe, return the last value + if time >= keyframes[keyframes.len() - 1].time { + return Some(keyframes[keyframes.len() - 1].value.clone()); + } + // Find the keyframes surrounding the given time + for i in 0..keyframes.len() - 1 { + let kf0 = &keyframes[i]; + let kf1 = &keyframes[i + 1]; + if time >= kf0.time && time <= kf1.time { + let t = (time - kf0.time) / (kf1.time - kf0.time); + return Some(interpolate_values(&kf0.value, &kf1.value, t)); + } + } + None +} +fn interpolate_values(v0: &AnimationValue, v1: &AnimationValue, t: f32) -> AnimationValue { + match (v0, v1) { + (AnimationValue::Translation(tr0), AnimationValue::Translation(tr1)) => { + let value = tr0.lerp(*tr1, t); + AnimationValue::Translation(value) + } + (AnimationValue::Rotation(r0), AnimationValue::Rotation(r1)) => { + let value = r0.slerp(*r1, t); + AnimationValue::Rotation(value) + } + (AnimationValue::Scale(s0), AnimationValue::Scale(s1)) => { + let value = s0.lerp(*s1, t); + AnimationValue::Scale(value) + } + _ => { + // Unsupported interpolation + v0.clone() + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use cgmath::{Matrix4, Vector3}; + + #[test] + fn test_absolute_transforms() { + // Define the transforms + let transform_joint_0 = Matrix4::from_translation(Vector3::new(1.0, 0.0, 0.0)); + let transform_joint_1 = Matrix4::from_translation(Vector3::new(0.0, 2.0, 0.0)); + let transform_joint_2 = Matrix4::from_translation(Vector3::new(0.0, 0.0, 3.0)); + + // Create the SkinBuilder + let mut skin_builder = SkeletonBuilder::create(); + + skin_builder.add_joint(0, "Joint0".to_string(), None, transform_joint_0); + skin_builder.add_joint(1, "Joint1".to_string(), Some(0), transform_joint_1); + skin_builder.add_joint(2, "Joint2".to_string(), Some(1), transform_joint_2); + + // Build the Skin + let skin = skin_builder.build(); + + // Expected absolute transforms + let expected_abs_transform_joint_0 = transform_joint_0; + let expected_abs_transform_joint_1 = + expected_abs_transform_joint_0 * transform_joint_1 * transform_joint_2; + let expected_abs_transform_joint_2 = expected_abs_transform_joint_1 * transform_joint_2; + + // Retrieve computed absolute transforms + let abs_transform_joint_0 = skin.get_joint_absolute_transform(0); + let abs_transform_joint_1 = skin.get_joint_absolute_transform(1); + let abs_transform_joint_2 = skin.get_joint_absolute_transform(2); + + // Helper function to compare matrices + fn matrices_approx_equal(a: &Matrix4, b: &Matrix4) -> bool { + a.eq(b) + } + + // Assert that computed transforms match expected transforms + assert!( + matrices_approx_equal(&abs_transform_joint_0, &expected_abs_transform_joint_0), + "Joint 0 absolute transform does not match expected value." + ); + assert!( + matrices_approx_equal(&abs_transform_joint_1, &expected_abs_transform_joint_1), + "Joint 1 absolute transform does not match expected value." + ); + assert!( + matrices_approx_equal(&abs_transform_joint_2, &expected_abs_transform_joint_2), + "Joint 2 absolute transform does not match expected value." + ); + } +} diff --git a/runtime/functor-runtime-common/src/render_context.rs b/runtime/functor-runtime-common/src/render_context.rs index 49c7f90..a9afcd1 100644 --- a/runtime/functor-runtime-common/src/render_context.rs +++ b/runtime/functor-runtime-common/src/render_context.rs @@ -1,9 +1,10 @@ use std::sync::Arc; -use crate::asset::AssetCache; +use crate::{asset::AssetCache, FrameTime}; pub struct RenderContext<'a> { pub gl: &'a glow::Context, pub shader_version: &'a str, pub asset_cache: Arc, + pub frame_time: FrameTime, } diff --git a/runtime/functor-runtime-common/src/scene3d/mod.rs b/runtime/functor-runtime-common/src/scene3d/mod.rs index 47f9462..365d0a2 100644 --- a/runtime/functor-runtime-common/src/scene3d/mod.rs +++ b/runtime/functor-runtime-common/src/scene3d/mod.rs @@ -1,6 +1,7 @@ use std::{cell::RefCell, sync::Arc}; -use cgmath::{vec3, Matrix4, SquareMatrix}; +use cgmath::{point3, vec3, vec4, Matrix4, SquareMatrix, Transform}; +use gltf::json::extensions::scene; use serde::{Deserialize, Serialize}; use fable_library_rust::NativeArray_::Array; @@ -12,9 +13,9 @@ use crate::{ AssetHandle, BuiltAssetPipeline, }, geometry::{self, Geometry, Mesh}, - material::{BasicMaterial, Material}, + material::{BasicMaterial, ColorMaterial, Material}, math::Angle, - model::Model, + model::{Model, Skeleton}, texture::{RuntimeTexture, Texture2D}, RenderContext, }; @@ -179,6 +180,8 @@ impl Scene3D { let matrix = world_matrix * self.xform; + // println!("SKELETON: {:#?}", hydrated_model.skeleton); + for mesh in hydrated_model.meshes.iter() { // Go through selectors, and adjust // let override_material_description = Some(MaterialDescription::Texture( @@ -224,8 +227,45 @@ impl Scene3D { ); }; + // TODO: Bring back drawing mesh.mesh.draw(&render_context.gl) } + + // TEMPORARY: Render joints + // let maybe_animation = hydrated_model.animations.get(0); + // if let Some(animation) = maybe_animation { + // let time = render_context.frame_time.tts % animation.duration; + // let animated_skeleton = + // Skeleton::animate(&hydrated_model.skeleton, animation, time); + + // println!( + // "Animating {} {} {}", + // animation.name, animation.duration, time, + // ); + + // let joints = animated_skeleton.get_transforms(); + // for joint_transform in joints { + // let mut color_material = + // ColorMaterial::create(vec4(0.0, 1.0, 0.0, 1.0)); + // color_material.initialize(&render_context); + + // let xform = &(matrix * joint_transform); + // let point = xform.transform_point(point3(0.0, 0.0, 0.0)); + // let xform2 = + // Matrix4::from_translation(vec3(point.x, point.y, point.z)) + // * Matrix4::from_scale(0.1); + + // color_material.draw_opaque( + // &render_context, + // projection_matrix, + // view_matrix, + // &xform2, + // &[], + // ); + + // scene_context.sphere.borrow().draw(render_context.gl); + // } + // } } } } diff --git a/runtime/functor-runtime-desktop/src/main.rs b/runtime/functor-runtime-desktop/src/main.rs index 3773d8a..e34b610 100644 --- a/runtime/functor-runtime-desktop/src/main.rs +++ b/runtime/functor-runtime-desktop/src/main.rs @@ -150,6 +150,7 @@ pub async fn main() { gl: &gl, shader_version, asset_cache: asset_cache.clone(), + frame_time: time.clone(), }; let scene = game.render(time.clone());