From c1e6d141bf9e211bda2fe7e1730b58b8a9cb2bac Mon Sep 17 00:00:00 2001 From: Max Richter Date: Thu, 25 Apr 2024 13:15:24 +0200 Subject: [PATCH] feat: add gravity node --- Cargo.lock | 16 +++ nodes/max/plantarium/branch/src/input.json | 8 ++ nodes/max/plantarium/branch/src/lib.rs | 41 +++---- nodes/max/plantarium/gravity/.gitignore | 6 + nodes/max/plantarium/gravity/Cargo.toml | 30 +++++ nodes/max/plantarium/gravity/package.json | 6 + nodes/max/plantarium/gravity/src/input.json | 29 +++++ nodes/max/plantarium/gravity/src/lib.rs | 129 ++++++++++++++++++++ nodes/max/plantarium/gravity/tests/web.rs | 13 ++ nodes/max/plantarium/noise/src/input.json | 2 +- packages/utils/src/geometry/extrude_path.rs | 2 +- packages/utils/src/geometry/math.rs | 21 ++++ packages/utils/src/geometry/mod.rs | 2 + packages/utils/src/geometry/path_data.rs | 111 +++++++++++++++++ 14 files changed, 390 insertions(+), 26 deletions(-) create mode 100644 nodes/max/plantarium/gravity/.gitignore create mode 100644 nodes/max/plantarium/gravity/Cargo.toml create mode 100644 nodes/max/plantarium/gravity/package.json create mode 100644 nodes/max/plantarium/gravity/src/input.json create mode 100644 nodes/max/plantarium/gravity/src/lib.rs create mode 100644 nodes/max/plantarium/gravity/tests/web.rs create mode 100644 packages/utils/src/geometry/math.rs diff --git a/Cargo.lock b/Cargo.lock index 7e07be8..011463d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -78,6 +78,22 @@ version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e05e7e6723e3455f4818c7b26e855439f7546cf617ef669d1adedb8669e5cb9" +[[package]] +name = "gravity" +version = "0.1.0" +dependencies = [ + "console_error_panic_hook", + "glam", + "macros", + "noise", + "serde", + "serde-wasm-bindgen", + "utils", + "wasm-bindgen", + "wasm-bindgen-test", + "web-sys", +] + [[package]] name = "itoa" version = "1.0.11" diff --git a/nodes/max/plantarium/branch/src/input.json b/nodes/max/plantarium/branch/src/input.json index 74cc454..b42e9cf 100644 --- a/nodes/max/plantarium/branch/src/input.json +++ b/nodes/max/plantarium/branch/src/input.json @@ -65,6 +65,14 @@ "min": 3, "max": 64, "setting": "resolution.curve" + }, + "rotation": { + "type": "float", + "hidden": true, + "min": 0, + "max": 360, + "step": 0.01, + "value": 0 } } } diff --git a/nodes/max/plantarium/branch/src/lib.rs b/nodes/max/plantarium/branch/src/lib.rs index 30b5b46..f1de805 100644 --- a/nodes/max/plantarium/branch/src/lib.rs +++ b/nodes/max/plantarium/branch/src/lib.rs @@ -1,10 +1,10 @@ -use std::f32::consts::PI; - -use glam::Vec3; use macros::include_definition_file; +use std::f32::consts::PI; use utils::{ concat_arg_vecs, evaluate_float, evaluate_int, - geometry::{create_path, get_direction_at_path, get_point_at_path, wrap_path, wrap_path_mut}, + geometry::{ + create_path, interpolate_along_path, rotate_vector_by_angle, wrap_path, wrap_path_mut, + }, log, set_panic_hook, split_args, }; use wasm_bindgen::prelude::*; @@ -51,42 +51,35 @@ pub fn execute(input: &[i32]) -> Vec { let length = evaluate_float(args[1]); let thickness = evaluate_float(args[2]); - let offset_single = evaluate_float(args[3]); + let offset_single = if i % 2 == 0 { + evaluate_float(args[3]) + } else { + 0.0 + }; - // log!("a: {}, length: {}, thickness: {}, offset_single: {}, lowest_branch: {}, highest_branch: {}", a, length, thickness, offset_single, lowest_branch, highest_branch); - - // log!("a: {}, length: {}, thickness: {}, offset_single: {}, lowest_branch: {}, highest_branch: {}", a, length, thickness, offset_single, lowest_branch, highest_branch); let root_alpha = (a * (highest_branch - lowest_branch) + lowest_branch) .min(1.0) .max(0.0); - let is_left = i % 2 == 0; + let (branch_origin, orthogonal, direction) = interpolate_along_path( + path.points, + root_alpha + (offset_single - 0.5) * 6.0 / resolution as f32, + ); - let branch_origin = get_point_at_path(path.points, root_alpha); - //const [_vx, , _vz] = interpolateSkeletonVec(stem.skeleton, a); - let direction_slice = get_direction_at_path(path.points, root_alpha); - let direction = Vec3::from_slice(&direction_slice).normalize(); - - let rotation_angle = if is_left { PI } else { -PI }; + let rotation_angle = (evaluate_float(args[9]) * PI / 180.0) * i as f32; // check if diration contains NaN - if direction[0].is_nan() || direction[1].is_nan() || direction[2].is_nan() { + if orthogonal[0].is_nan() || orthogonal[1].is_nan() || orthogonal[2].is_nan() { log!( - "BRANCH direction contains NaN: {:?}, slice: {:?} branch_origin: {:?}, branch: {}", + "BRANCH direction contains NaN: {:?}, branch_origin: {:?}, branch: {}", direction, - direction_slice, branch_origin, i ); continue; } - let branch_direction = Vec3::from_slice(&[ - direction[0] * rotation_angle.cos() - direction[2] * rotation_angle.sin(), - 0.0, - direction[0] * rotation_angle.sin() + direction[2] * rotation_angle.cos(), - ]) - .normalize(); + let branch_direction = rotate_vector_by_angle(orthogonal, direction, rotation_angle); log!( "BRANCH depth: {}, branch_origin: {:?}, direction_at: {:?}, branch_direction: {:?}", diff --git a/nodes/max/plantarium/gravity/.gitignore b/nodes/max/plantarium/gravity/.gitignore new file mode 100644 index 0000000..4e30131 --- /dev/null +++ b/nodes/max/plantarium/gravity/.gitignore @@ -0,0 +1,6 @@ +/target +**/*.rs.bk +Cargo.lock +bin/ +pkg/ +wasm-pack.log diff --git a/nodes/max/plantarium/gravity/Cargo.toml b/nodes/max/plantarium/gravity/Cargo.toml new file mode 100644 index 0000000..81378fe --- /dev/null +++ b/nodes/max/plantarium/gravity/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "gravity" +version = "0.1.0" +authors = ["Max Richter "] +edition = "2018" + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +default = ["console_error_panic_hook"] + +[dependencies] +wasm-bindgen = "0.2.84" + +# The `console_error_panic_hook` crate provides better debugging of panics by +# logging them with `console.error`. This is great for development, but requires +# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for +# code size when deploying. +utils = { version = "0.1.0", path = "../../../../packages/utils" } +macros = { version = "0.1.0", path = "../../../../packages/macros" } +serde = { version = "1.0", features = ["derive"] } +serde-wasm-bindgen = "0.4" +console_error_panic_hook = { version = "0.1.7", optional = true } +web-sys = { version = "0.3.69", features = ["console"] } +noise = "0.9.0" +glam = "0.27.0" + +[dev-dependencies] +wasm-bindgen-test = "0.3.34" diff --git a/nodes/max/plantarium/gravity/package.json b/nodes/max/plantarium/gravity/package.json new file mode 100644 index 0000000..86916c9 --- /dev/null +++ b/nodes/max/plantarium/gravity/package.json @@ -0,0 +1,6 @@ +{ + "scripts": { + "build": "wasm-pack build --release --out-name index --no-default-features", + "dev": "cargo watch -s 'wasm-pack build --dev --out-name index --no-default-features'" + } +} diff --git a/nodes/max/plantarium/gravity/src/input.json b/nodes/max/plantarium/gravity/src/input.json new file mode 100644 index 0000000..7908b38 --- /dev/null +++ b/nodes/max/plantarium/gravity/src/input.json @@ -0,0 +1,29 @@ +{ + "id": "max/plantarium/gravity", + "outputs": [ + "path" + ], + "inputs": { + "plant": { + "type": "path" + }, + "strength": { + "type": "float", + "min": 0.1, + "max": 1 + }, + "curviness": { + "type": "float", + "hidden": true, + "min": 0.1, + "max": 1 + }, + "depth": { + "type": "integer", + "min": 1, + "max": 10, + "value": 1, + "hidden": true + } + } +} diff --git a/nodes/max/plantarium/gravity/src/lib.rs b/nodes/max/plantarium/gravity/src/lib.rs new file mode 100644 index 0000000..8a61249 --- /dev/null +++ b/nodes/max/plantarium/gravity/src/lib.rs @@ -0,0 +1,129 @@ +use glam::Vec3; +use macros::include_definition_file; +use utils::{ + concat_args, evaluate_float, evaluate_int, geometry::wrap_path_mut, log, reset_call_count, + set_panic_hook, split_args, +}; +use wasm_bindgen::prelude::*; + +include_definition_file!("src/input.json"); + +fn lerp_vec3(a: Vec3, b: Vec3, t: f32) -> Vec3 { + a + (b - a) * t +} + +#[wasm_bindgen] +pub fn execute(input: &[i32]) -> Vec { + set_panic_hook(); + + reset_call_count(); + + let args = split_args(input); + + let plants = split_args(args[0]); + let depth = evaluate_int(args[3]); + + let mut max_depth = 0; + for path_data in plants.iter() { + if path_data[2] != 0 { + continue; + } + max_depth = max_depth.max(path_data[3]); + } + + let output: Vec> = plants + .iter() + .map(|_path_data| { + let mut path_data = _path_data.to_vec(); + if path_data[2] != 0 || path_data[3] < (max_depth - depth + 1) { + return path_data; + } + + let path = wrap_path_mut(&mut path_data); + + let mut offset_vec = Vec3::ZERO; + + for i in 1..path.length { + // let alpha = i as f32 / (path.length - 1) as f32; + let start_index = (i - 1) * 4; + let end_index = start_index + 4; + + let start_point = Vec3::from_slice(&path.points[start_index..start_index + 3]); + let end_point = Vec3::from_slice(&path.points[end_index..end_index + 3]); + log!("--------------------------------"); + + log!( + "start_index: {:?} end_index: {:?} length:{}", + start_index, + end_index, + path.points.len() + ); + if start_point[0].is_nan() { + log!("start_point is nan {:?}", path.points); + continue; + } + log!("start_point: {:?}", start_point); + log!("end_point: {:?}", end_point); + + let length = (end_point - start_point).length(); + + let normalised = (end_point - start_point).normalize(); + + if normalised[0].is_nan() { + log!("normalised is nan {:?}", normalised); + continue; + } + + let strength = evaluate_float(args[1]); + let down_point = Vec3::new(0.0, -length * strength, 0.0); + + if down_point[0].is_nan() { + log!("down_point is nan {:?}", down_point); + continue; + } + + let curviness = evaluate_float(args[2]); + + let mut mid_point = lerp_vec3( + normalised, + down_point, + curviness * (i as f32 / path.length as f32).sqrt(), + ); + + if mid_point[0].is_nan() { + log!("mid_point is nan {:?}", mid_point); + log!("normalised: {:?}", normalised); + log!("curviness: {:?}", curviness); + continue; + } + + if mid_point[0] == 0.0 && mid_point[2] == 0.0 { + mid_point[0] += 0.0001; + mid_point[2] += 0.0001; + } + + mid_point = mid_point.normalize(); + + mid_point *= length; + + let final_end_point = start_point + mid_point; + let offset_end_point = end_point + offset_vec; + + if offset_end_point[0].is_nan() { + log!("offset_end_point is nan {:?}", offset_end_point); + continue; + } + + path.points[end_index] = offset_end_point[0]; + path.points[end_index + 1] = offset_end_point[1]; + path.points[end_index + 2] = offset_end_point[2]; + + let offset = final_end_point - end_point; + offset_vec += offset; + } + path_data + }) + .collect(); + + concat_args(output.iter().map(|x| x.as_slice()).collect()) +} diff --git a/nodes/max/plantarium/gravity/tests/web.rs b/nodes/max/plantarium/gravity/tests/web.rs new file mode 100644 index 0000000..de5c1da --- /dev/null +++ b/nodes/max/plantarium/gravity/tests/web.rs @@ -0,0 +1,13 @@ +//! Test suite for the Web and headless browsers. + +#![cfg(target_arch = "wasm32")] + +extern crate wasm_bindgen_test; +use wasm_bindgen_test::*; + +wasm_bindgen_test_configure!(run_in_browser); + +#[wasm_bindgen_test] +fn pass() { + assert_eq!(1 + 1, 2); +} diff --git a/nodes/max/plantarium/noise/src/input.json b/nodes/max/plantarium/noise/src/input.json index bfb88d4..a91f483 100644 --- a/nodes/max/plantarium/noise/src/input.json +++ b/nodes/max/plantarium/noise/src/input.json @@ -21,7 +21,7 @@ "type": "float", "label": "Fixate bottom of plant", "hidden": true, - "value": 1, + "value": 1.0, "min": 0, "max": 1 }, diff --git a/packages/utils/src/geometry/extrude_path.rs b/packages/utils/src/geometry/extrude_path.rs index c17af53..ee221eb 100644 --- a/packages/utils/src/geometry/extrude_path.rs +++ b/packages/utils/src/geometry/extrude_path.rs @@ -33,7 +33,7 @@ pub fn extrude_path(input_path: PathData, res_x: usize) -> Vec { let position_offset = i * res_x; let pos = Vec3::new(path[i * 4], path[i * 4 + 1], path[i * 4 + 2]); - let thickness = path[i * 4 + 3]; + let thickness = path[i * 4 + 3].max(0.000001); // Get direction of the current segment let segment_dir = (if i == 0 { diff --git a/packages/utils/src/geometry/math.rs b/packages/utils/src/geometry/math.rs new file mode 100644 index 0000000..5a69e9a --- /dev/null +++ b/packages/utils/src/geometry/math.rs @@ -0,0 +1,21 @@ +use glam::{Quat, Vec3}; + +/// Rotates a vector around a given axis by a specified angle. +/// +/// Arguments: +/// * `vector` - The vector to rotate. +/// * `axis` - The axis to rotate around. +/// * `angle_radians` - The angle to rotate by, in radians. +/// +/// Returns: +/// * The rotated vector. +pub fn rotate_vector_by_angle(vector: Vec3, axis: Vec3, angle_radians: f32) -> Vec3 { + // Normalize the axis to ensure it's a unit vector + let normalized_axis = axis.normalize(); + + // Create a quaternion representing the rotation around the axis by the given angle + let rotation_quat = Quat::from_axis_angle(normalized_axis, angle_radians); + + // Rotate the vector using the quaternion + rotation_quat.mul_vec3(vector) +} diff --git a/packages/utils/src/geometry/mod.rs b/packages/utils/src/geometry/mod.rs index 9a8f890..44574af 100644 --- a/packages/utils/src/geometry/mod.rs +++ b/packages/utils/src/geometry/mod.rs @@ -1,11 +1,13 @@ mod calculate_normals; mod extrude_path; mod geometry_data; +mod math; mod path_data; mod transform; pub use calculate_normals::*; pub use extrude_path::*; pub use geometry_data::*; +pub use math::*; pub use path_data::*; pub use transform::*; diff --git a/packages/utils/src/geometry/path_data.rs b/packages/utils/src/geometry/path_data.rs index c13e085..2f1b99f 100644 --- a/packages/utils/src/geometry/path_data.rs +++ b/packages/utils/src/geometry/path_data.rs @@ -1,3 +1,5 @@ +use glam::{vec3, vec4, Vec3, Vec4, Vec4Swizzles}; + // 0: node-type, stem: 0 // 1: depth static PATH_HEADER_SIZE: usize = 2; @@ -207,3 +209,112 @@ pub fn get_direction_at_path(path: &[f32], alpha: f32) -> [f32; 3] { [dx / norm, dy / norm, dz / norm] } + +/// A function that interpolates a position along a path given by `points_data` at a position +/// specified by `alpha` (ranging from 0.0 to 1.0), calculates an orthogonal vector to the path, +/// and returns the direction of the path at that point. +/// +/// Arguments: +/// * `points_data` - A slice of `f32` containing x, y, z coordinates and thickness for each point defining the path. +/// * `alpha` - A float from 0.0 to 1.0 indicating the relative position along the path. +/// +/// Returns: +/// * A tuple containing the interpolated position along the path as Vec4 (including thickness), +/// a vector orthogonal to the path, and the direction of the path at that position. +pub fn interpolate_along_path(points_data: &[f32], _alpha: f32) -> (Vec4, Vec3, Vec3) { + let alpha = _alpha.min(0.999999).max(0.000001); + assert!( + points_data.len() % 4 == 0, + "The points data must be a multiple of 4." + ); + + let num_points = points_data.len() / 4; + assert!( + num_points > 1, + "There must be at least two points to define a path." + ); + + // Calculate the total length of the path and the lengths of each segment. + let mut segment_lengths = Vec::with_capacity(num_points - 1); + let mut total_length = 0.0; + for i in 0..num_points - 1 { + let start_index = i * 4; + let end_index = (i + 1) * 4; + let start_point = vec3( + points_data[start_index], + points_data[start_index + 1], + points_data[start_index + 2], + ); + let end_point = vec3( + points_data[end_index], + points_data[end_index + 1], + points_data[end_index + 2], + ); + let length = (end_point - start_point).length(); + segment_lengths.push(length); + total_length += length; + } + + // Find the target length along the path corresponding to `alpha`. + let target_length = alpha * total_length; + let mut accumulated_length = 0.0; + + // Find the segment that contains the point at `target_length`. + for (i, &length) in segment_lengths.iter().enumerate() { + if accumulated_length + length >= target_length { + // Calculate the position within this segment. + let segment_alpha = (target_length - accumulated_length) / length; + let start_index = i * 4; + let end_index = (i + 1) * 4; + let start_point = vec4( + points_data[start_index], + points_data[start_index + 1], + points_data[start_index + 2], + points_data[start_index + 3], + ); + let end_point = vec4( + points_data[end_index], + points_data[end_index + 1], + points_data[end_index + 2], + points_data[end_index + 3], + ); + let position = start_point + (end_point - start_point) * segment_alpha; + + // Calculate the tangent vector to the path at this segment. + let tangent = (end_point.xyz() - start_point.xyz()).normalize(); + + // Calculate an orthogonal vector. Assume using the global up vector (0, 1, 0) + let global_up = vec3(0.0, 1.0, 0.0); + let orthogonal = tangent.cross(global_up).normalize(); + + // If the orthogonal vector is zero, choose another axis. + let orthogonal = if orthogonal.length_squared() == 0.0 { + tangent.cross(vec3(1.0, 0.0, 0.0)).normalize() + } else { + orthogonal + }; + + return (position, orthogonal, tangent); + } + accumulated_length += length; + } + + // As a fallback for numerical precision issues, use the last point and a default orthogonal vector. + let last_start_index = (num_points - 2) * 4; + let last_end_index = (num_points - 1) * 4; + let last_start_point = vec4( + points_data[last_start_index], + points_data[last_start_index + 1], + points_data[last_start_index + 2], + points_data[last_start_index + 3], + ); + let last_end_point = vec4( + points_data[last_end_index], + points_data[last_end_index + 1], + points_data[last_end_index + 2], + points_data[last_end_index + 3], + ); + let last_tangent = (last_end_point.xyz() - last_start_point.xyz()).normalize(); + let last_orthogonal = last_tangent.cross(vec3(0.0, 1.0, 0.0)).normalize(); + (last_end_point, last_orthogonal, last_tangent) +}