From 10a12ad41cf56c6863a5d53eb52ab1eb8bf5486d Mon Sep 17 00:00:00 2001 From: Max Richter Date: Mon, 6 May 2024 01:10:23 +0200 Subject: [PATCH] feat: instance node --- Cargo.lock | 15 ++ app/src/lib/result-viewer/Scene.svelte | 19 +- app/src/lib/result-viewer/geometryPool.ts | 252 ++++++++++++++++++ app/src/lib/result-viewer/updateGeometries.ts | 139 +--------- docs/PLANTARIUM.md | 25 ++ nodes/max/plantarium/box/src/lib.rs | 6 +- nodes/max/plantarium/instance/.gitignore | 6 + nodes/max/plantarium/instance/Cargo.toml | 29 ++ nodes/max/plantarium/instance/package.json | 6 + nodes/max/plantarium/instance/src/input.json | 32 +++ nodes/max/plantarium/instance/src/lib.rs | 119 +++++++++ nodes/max/plantarium/instance/tests/web.rs | 13 + nodes/max/plantarium/output/src/lib.rs | 13 +- packages/runtime/src/runtime-executor.ts | 2 +- .../utils/src/geometry/calculate_normals.rs | 3 +- packages/utils/src/geometry/geometry_data.rs | 24 ++ packages/utils/src/geometry/instance_data.rs | 118 ++++++++ packages/utils/src/geometry/mod.rs | 2 + packages/utils/src/geometry/path_data.rs | 10 + packages/utils/src/tree.rs | 9 +- 20 files changed, 695 insertions(+), 147 deletions(-) create mode 100644 app/src/lib/result-viewer/geometryPool.ts create mode 100644 docs/PLANTARIUM.md create mode 100644 nodes/max/plantarium/instance/.gitignore create mode 100644 nodes/max/plantarium/instance/Cargo.toml create mode 100644 nodes/max/plantarium/instance/package.json create mode 100644 nodes/max/plantarium/instance/src/input.json create mode 100644 nodes/max/plantarium/instance/src/lib.rs create mode 100644 nodes/max/plantarium/instance/tests/web.rs create mode 100644 packages/utils/src/geometry/instance_data.rs diff --git a/Cargo.lock b/Cargo.lock index 39a7bd5..886d46d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -157,6 +157,21 @@ dependencies = [ "web-sys", ] +[[package]] +name = "nodarium_instance" +version = "0.1.0" +dependencies = [ + "console_error_panic_hook", + "glam", + "nodarium_macros", + "nodarium_utils", + "serde", + "serde-wasm-bindgen", + "wasm-bindgen", + "wasm-bindgen-test", + "web-sys", +] + [[package]] name = "nodarium_macros" version = "0.1.0" diff --git a/app/src/lib/result-viewer/Scene.svelte b/app/src/lib/result-viewer/Scene.svelte index a3e95c2..11e8dbe 100644 --- a/app/src/lib/result-viewer/Scene.svelte +++ b/app/src/lib/result-viewer/Scene.svelte @@ -7,6 +7,9 @@ Vector3, type Vector3Tuple, Box3, + Mesh, + MeshMatcapMaterial, + MeshBasicMaterial, } from "three"; import { AppSettings } from "../settings/app-settings"; import Camera from "./Camera.svelte"; @@ -51,11 +54,21 @@ export let centerCamera: boolean = true; let center = new Vector3(0, 4, 0); + function isMesh(child: Mesh | any): child is Mesh { + return child.isObject3D && "material" in child; + } + + function isMatCapMaterial(material: any): material is MeshBasicMaterial { + return material.isMaterial && "matcap" in material; + } + $: if ($AppSettings && scene) { - scene.children.forEach((child) => { - child.material.wireframe = $AppSettings.wireframe; - threlte.invalidate(); + scene.traverse(function (child) { + if (isMesh(child) && isMatCapMaterial(child.material)) { + child.material.wireframe = $AppSettings.wireframe; + } }); + threlte.invalidate(); } function getPosition(geo: BufferGeometry, i: number) { diff --git a/app/src/lib/result-viewer/geometryPool.ts b/app/src/lib/result-viewer/geometryPool.ts new file mode 100644 index 0000000..273d912 --- /dev/null +++ b/app/src/lib/result-viewer/geometryPool.ts @@ -0,0 +1,252 @@ +import { fastHashArrayBuffer } from "@nodes/utils"; +import { BufferAttribute, BufferGeometry, Float32BufferAttribute, Group, InstancedMesh, Material, Matrix4, Mesh } from "three" + + +function fastArrayHash(arr: ArrayBuffer) { + let ints = new Uint8Array(arr); + + const sampleDistance = Math.max(Math.floor(ints.length / 100), 1); + const sampleCount = Math.floor(ints.length / sampleDistance); + + let hash = new Uint8Array(sampleCount); + + for (let i = 0; i < sampleCount; i++) { + const index = i * sampleDistance; + hash[i] = ints[index]; + } + + return fastHashArrayBuffer(hash.buffer); +} + +export function createGeometryPool(parentScene: Group, material: Material) { + const scene = new Group(); + parentScene.add(scene); + + let meshes: Mesh[] = []; + + function updateSingleGeometry(data: Int32Array, existingMesh: Mesh | null = null) { + + let hash = fastArrayHash(data); + + let geometry = existingMesh ? existingMesh.geometry : new BufferGeometry(); + + // Extract data from the encoded array + // const geometryType = encodedData[index++]; + let index = 1; + const vertexCount = data[index++]; + const faceCount = data[index++]; + + if (geometry.userData?.hash === hash) { + return; + } + + // Indices + const indicesEnd = index + faceCount * 3; + const indices = data.subarray(index, indicesEnd); + index = indicesEnd; + + // Vertices + const vertices = new Float32Array( + data.buffer, + index * 4, + vertexCount * 3, + ); + index = index + vertexCount * 3; + + let posAttribute = geometry.getAttribute( + "position", + ) as BufferAttribute | null; + + if (posAttribute && posAttribute.count === vertexCount) { + posAttribute.set(vertices, 0); + posAttribute.needsUpdate = true; + } else { + geometry.setAttribute( + "position", + new Float32BufferAttribute(vertices, 3), + ); + } + + const normals = new Float32Array( + data.buffer, + index * 4, + vertexCount * 3, + ); + index = index + vertexCount * 3; + + if ( + geometry.userData?.faceCount !== faceCount || + geometry.userData?.vertexCount !== vertexCount + ) { + // Add data to geometry + geometry.setIndex([...indices]); + } + + const normalsAttribute = geometry.getAttribute( + "normal", + ) as BufferAttribute | null; + if (normalsAttribute && normalsAttribute.count === vertexCount) { + normalsAttribute.set(normals, 0); + normalsAttribute.needsUpdate = true; + } else { + geometry.setAttribute("normal", new Float32BufferAttribute(normals, 3)); + } + + geometry.userData = { + vertexCount, + faceCount, + hash, + }; + + if (!existingMesh) { + const mesh = new Mesh(geometry, material); + scene.add(mesh); + meshes.push(mesh); + } + + } + + + return { + update( + newData: Int32Array[], + ) { + for (let i = 0; i < Math.max(newData.length, meshes.length); i++) { + const existingMesh = meshes[i]; + let input = newData[i]; + if (input) { + updateSingleGeometry(input, existingMesh || null); + } else if (existingMesh) { + existingMesh.visible = false; + scene.remove(existingMesh); + } + } + } + } +} + +export function createInstancedGeometryPool(parentScene: Group, material: Material) { + const scene = new Group(); + parentScene.add(scene); + + const instances: InstancedMesh[] = []; + + function updateSingleInstance(data: Int32Array, existingInstance: InstancedMesh | null = null) { + + let hash = fastArrayHash(data); + + let geometry = existingInstance ? existingInstance.geometry : new BufferGeometry(); + + // Extract data from the encoded array + let index = 0; + const geometryType = data[index++]; + const vertexCount = data[index++]; + const faceCount = data[index++]; + const instanceCount = data[index++]; + const stemDepth = data[index++]; + + // Indices + const indicesEnd = index + faceCount * 3; + const indices = data.subarray(index, indicesEnd); + index = indicesEnd; + if ( + geometry.userData?.faceCount !== faceCount || + geometry.userData?.vertexCount !== vertexCount + ) { + // Add data to geometry + geometry.setIndex([...indices]); + } + + // Vertices + const vertices = new Float32Array( + data.buffer, + index * 4, + vertexCount * 3, + ); + index = index + vertexCount * 3; + let posAttribute = geometry.getAttribute( + "position", + ) as BufferAttribute | null; + if (posAttribute && posAttribute.count === vertexCount) { + posAttribute.set(vertices, 0); + posAttribute.needsUpdate = true; + } else { + geometry.setAttribute( + "position", + new Float32BufferAttribute(vertices, 3), + ); + } + + const normals = new Float32Array( + data.buffer, + index * 4, + vertexCount * 3, + ); + index = index + vertexCount * 3; + const normalsAttribute = geometry.getAttribute( + "normal", + ) as BufferAttribute | null; + if (normalsAttribute && normalsAttribute.count === vertexCount) { + normalsAttribute.set(normals, 0); + normalsAttribute.needsUpdate = true; + } else { + geometry.setAttribute("normal", new Float32BufferAttribute(normals, 3)); + } + + if (existingInstance && instanceCount > existingInstance.geometry.userData.count) { + console.log("recreating instance") + scene.remove(existingInstance); + instances.splice(instances.indexOf(existingInstance), 1); + existingInstance = new InstancedMesh(geometry, material, instanceCount); + scene.add(existingInstance) + instances.push(existingInstance) + } else if (!existingInstance) { + console.log("creating instance") + existingInstance = new InstancedMesh(geometry, material, instanceCount); + scene.add(existingInstance) + instances.push(existingInstance) + } else { + console.log("updating instance") + existingInstance.count = instanceCount; + } + + // update the matrices + + const matrices = new Float32Array( + data.buffer, + index * 4, + instanceCount * 16); + + for (let i = 0; i < instanceCount; i++) { + const matrix = new Matrix4().fromArray(matrices.subarray(i * 16, i * 16 + 16)); + existingInstance.setMatrixAt(i, matrix); + } + + geometry.userData = { + vertexCount, + faceCount, + count: Math.max(instanceCount, existingInstance.geometry.userData.count || 0), + hash, + }; + + existingInstance.instanceMatrix.needsUpdate = true; + + } + + return { + update( + newData: Int32Array[], + ) { + for (let i = 0; i < Math.max(newData.length, instances.length); i++) { + const existingMesh = instances[i]; + let input = newData[i]; + if (input) { + updateSingleInstance(input, existingMesh || null); + } else if (existingMesh) { + existingMesh.visible = false; + scene.remove(existingMesh); + } + } + } + } +} diff --git a/app/src/lib/result-viewer/updateGeometries.ts b/app/src/lib/result-viewer/updateGeometries.ts index 76de252..0be14b2 100644 --- a/app/src/lib/result-viewer/updateGeometries.ts +++ b/app/src/lib/result-viewer/updateGeometries.ts @@ -1,21 +1,5 @@ -import { fastHashArrayBuffer } from "@nodes/utils"; -import { BufferAttribute, BufferGeometry, Float32BufferAttribute, Mesh, MeshMatcapMaterial, TextureLoader, type Group } from "three"; - -function fastArrayHash(arr: ArrayBuffer) { - let ints = new Uint8Array(arr); - - const sampleDistance = Math.max(Math.floor(ints.length / 100), 1); - const sampleCount = Math.floor(ints.length / sampleDistance); - - let hash = new Uint8Array(sampleCount); - - for (let i = 0; i < sampleCount; i++) { - const index = i * sampleDistance; - hash[i] = ints[index]; - } - - return fastHashArrayBuffer(hash.buffer); -} +import { MeshMatcapMaterial, TextureLoader, type Group } from "three"; +import { createGeometryPool, createInstancedGeometryPool } from "./geometryPool"; const loader = new TextureLoader(); const matcap = loader.load('/matcap_green.jpg'); @@ -25,122 +9,19 @@ const material = new MeshMatcapMaterial({ matcap }); -function createGeometryFromEncodedData( - encodedData: Int32Array, - geometry = new BufferGeometry(), -): BufferGeometry { - // Extract data from the encoded array - let index = 1; - // const geometryType = encodedData[index++]; - const vertexCount = encodedData[index++]; - const faceCount = encodedData[index++]; - - let hash = fastArrayHash(encodedData); - - if (geometry.userData?.hash === hash) { - return geometry; - } - - // Indices - const indicesEnd = index + faceCount * 3; - const indices = encodedData.subarray(index, indicesEnd); - index = indicesEnd; - - // Vertices - const vertices = new Float32Array( - encodedData.buffer, - index * 4, - vertexCount * 3, - ); - index = index + vertexCount * 3; - - let posAttribute = geometry.getAttribute( - "position", - ) as BufferAttribute | null; - - if (posAttribute && posAttribute.count === vertexCount) { - posAttribute.set(vertices, 0); - posAttribute.needsUpdate = true; - } else { - geometry.setAttribute( - "position", - new Float32BufferAttribute(vertices, 3), - ); - } - - const normals = new Float32Array( - encodedData.buffer, - index * 4, - vertexCount * 3, - ); - index = index + vertexCount * 3; - - if ( - geometry.userData?.faceCount !== faceCount || - geometry.userData?.vertexCount !== vertexCount - ) { - // Add data to geometry - geometry.setIndex([...indices]); - } - - const normalsAttribute = geometry.getAttribute( - "normal", - ) as BufferAttribute | null; - if (normalsAttribute && normalsAttribute.count === vertexCount) { - normalsAttribute.set(normals, 0); - normalsAttribute.needsUpdate = true; - } else { - geometry.setAttribute("normal", new Float32BufferAttribute(normals, 3)); - } - - geometry.userData = { - vertexCount, - faceCount, - hash, - }; - - return geometry; -} - -let meshes: Mesh[] = []; - +let geometryPool: ReturnType; +let instancePool: ReturnType; export function updateGeometries(inputs: Int32Array[], group: Group) { + + geometryPool = geometryPool || createGeometryPool(group, material); + instancePool = instancePool || createInstancedGeometryPool(group, material); + let totalVertices = 0; let totalFaces = 0; - let newGeometries = []; - for (let i = 0; i < Math.max(meshes.length, inputs.length); i++) { - let existingMesh = meshes[i]; - let input = inputs[i]; - - if (input) { - if (input[0] !== 1) { - continue - } - totalVertices += input[1]; - totalFaces += input[2]; - } else { - if (existingMesh) { - existingMesh.visible = false; - } - continue; - } - - if (existingMesh) { - createGeometryFromEncodedData(input, existingMesh.geometry); - } else { - let geo = createGeometryFromEncodedData(input); - const mesh = new Mesh(geo, material); - meshes[i] = mesh; - newGeometries.push(mesh); - } - - } - - for (let i = 0; i < newGeometries.length; i++) { - group.add(newGeometries[i]); - } + geometryPool.update(inputs.filter(i => i[0] === 1)); + instancePool.update(inputs.filter(i => i[0] === 2)); return { totalFaces, totalVertices }; } diff --git a/docs/PLANTARIUM.md b/docs/PLANTARIUM.md new file mode 100644 index 0000000..3b41294 --- /dev/null +++ b/docs/PLANTARIUM.md @@ -0,0 +1,25 @@ +# Plantarium Documentation + +## Encoding Scheme + +Plantarium encodes different types of data differently. Right now we have three types of data: + +### Path +[0, stem_depth, ... x,y,z,thickness, x,y,z,thickness...] + +### Geometry +[1, vertex_amount, face_amount, ... faces in index_a, index_b, index_c ..., ... vertices in x,y,z,x,y,z ..., ... normals in x,y,z,x,y,z ...] + +### Instances +[ + 2, + vertex_amount, + face_amount, + instance_amount, + stem_depth, + ...faces..., + ...vertices..., + ...normals..., + ...4x4 matrices for instances... +] + diff --git a/nodes/max/plantarium/box/src/lib.rs b/nodes/max/plantarium/box/src/lib.rs index 0e0dcd3..e550888 100644 --- a/nodes/max/plantarium/box/src/lib.rs +++ b/nodes/max/plantarium/box/src/lib.rs @@ -1,7 +1,7 @@ use nodarium_macros::include_definition_file; use nodarium_utils::{ - encode_float, evaluate_float, geometry::calculate_normals, log, set_panic_hook, split_args, - wrap_arg, + concat_args, encode_float, evaluate_float, geometry::calculate_normals, log, set_panic_hook, + split_args, wrap_arg, }; use wasm_bindgen::prelude::*; @@ -81,7 +81,7 @@ pub fn execute(input: &[i32]) -> Vec { let res = wrap_arg(&cube_geometry); - log!("WASM(cube): output: {:?}", res); + log!("WASM(box): output: {:?}", res); res diff --git a/nodes/max/plantarium/instance/.gitignore b/nodes/max/plantarium/instance/.gitignore new file mode 100644 index 0000000..4e30131 --- /dev/null +++ b/nodes/max/plantarium/instance/.gitignore @@ -0,0 +1,6 @@ +/target +**/*.rs.bk +Cargo.lock +bin/ +pkg/ +wasm-pack.log diff --git a/nodes/max/plantarium/instance/Cargo.toml b/nodes/max/plantarium/instance/Cargo.toml new file mode 100644 index 0000000..69ea1b8 --- /dev/null +++ b/nodes/max/plantarium/instance/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "nodarium_instance" +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. +nodarium_utils = { version = "0.1.0", path = "../../../../packages/utils" } +nodarium_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"] } +glam = "0.27.0" + +[dev-dependencies] +wasm-bindgen-test = "0.3.34" diff --git a/nodes/max/plantarium/instance/package.json b/nodes/max/plantarium/instance/package.json new file mode 100644 index 0000000..86916c9 --- /dev/null +++ b/nodes/max/plantarium/instance/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/instance/src/input.json b/nodes/max/plantarium/instance/src/input.json new file mode 100644 index 0000000..f82b1a1 --- /dev/null +++ b/nodes/max/plantarium/instance/src/input.json @@ -0,0 +1,32 @@ +{ + "id": "max/plantarium/instance", + "outputs": [ + "path" + ], + "inputs": { + "plant": { + "type": "path" + }, + "geometry": { + "type": "geometry", + "value": [0,0,0] + }, + "amount": { + "type": "integer" + }, + "lowestInstance": { + "type": "float", + "min":0, + "max":1, + "value": 0.5, + "hidden": true + }, + "highestInstance": { + "type": "float", + "min":0, + "max":1, + "value":1, + "hidden": true + } + } +} diff --git a/nodes/max/plantarium/instance/src/lib.rs b/nodes/max/plantarium/instance/src/lib.rs new file mode 100644 index 0000000..d482bee --- /dev/null +++ b/nodes/max/plantarium/instance/src/lib.rs @@ -0,0 +1,119 @@ +use glam::{Mat4, Quat, Vec3}; +use nodarium_macros::include_definition_file; +use nodarium_utils::{ + concat_args, encode_float, evaluate_float, evaluate_int, + geometry::{ + calculate_normals, create_instance_data, wrap_geometry_data, wrap_instance_data, wrap_path, + }, + log, set_panic_hook, split_args, wrap_arg, +}; +use wasm_bindgen::prelude::*; + +include_definition_file!("src/input.json"); + +#[rustfmt::skip] +fn create_geo() -> Vec { + let size = 5.0; + + let p = encode_float(size); + let n = encode_float(-size); + + // [[1,3, x, y, z, x, y,z,x,y,z]]; + wrap_arg(calculate_normals(&mut [ + 1, // 1: geometry + 8, // 8 vertices + 12, // 12 faces + /* + Indeces: + 5----6 + | 4--+-7 + 0-|--1 | + 3----2 + + */ + // this are the indeces for the face + 0, 1, 2, + 0, 2, 3, + 0, 3, 4, + 4, 5, 0, + 6, 1, 0, + 5, 6, 0, + 7, 2, 1, + 6, 7, 1, + 2, 7, 3, + 3, 7, 4, + 7, 6, 4, + 4, 6, 5, // Bottom plate + p, n, n, + p, n, p, + n, n, p, + n, n, n, // Top Plate + n, p, n, + p, p, n, + p, p, p, + n, p, p, // this is the normal for every single vert 1065353216 == 1.0f encoded is i32 + 0, 0, 0, + 0, 0, 0, + 0, 0, 0, + 0, 0, 0, + 0, 0, 0, + 0, 0, 0, + 0, 0, 0, + 0, 0, 0, + ])) + +} + +#[wasm_bindgen] +pub fn execute(input: &[i32]) -> Vec { + set_panic_hook(); + + let args = split_args(input); + let mut inputs = split_args(args[0]); + log!("WASM(instance): inputs: {:?}", inputs); + + let mut geo_data = args[1].to_vec(); + let geo = wrap_geometry_data(&mut geo_data); + + let mut transforms: Vec = Vec::new(); + + for path_data in inputs.iter() { + let amount = evaluate_int(args[2]); + + let lowest_instance = evaluate_float(args[3]); + let highest_instance = evaluate_float(args[4]); + + let path = wrap_path(path_data); + + for i in 0..amount { + let alpha = + lowest_instance + (i as f32 / amount as f32) * (highest_instance - lowest_instance); + + let point = path.get_point_at(alpha); + let direction = path.get_direction_at(alpha); + + let transform = Mat4::from_scale_rotation_translation( + Vec3::new(0.1, 0.1, 0.1), + Quat::from_xyzw(direction[0], direction[1], direction[2], 1.0).normalize(), + Vec3::from_slice(&point), + ); + transforms.push(transform); + } + } + + let mut instance_data = create_instance_data( + geo.positions.len() / 3, + geo.faces.len() / 3, + transforms.len(), + ); + let mut instances = wrap_instance_data(&mut instance_data); + instances.set_geometry(geo); + (0..transforms.len()).for_each(|i| { + instances.set_transformation_matrix(i, &transforms[i].to_cols_array()); + }); + + log!("WASM(instance): geo: {:?}", instance_data); + inputs.push(&instance_data); + + concat_args(inputs) +} diff --git a/nodes/max/plantarium/instance/tests/web.rs b/nodes/max/plantarium/instance/tests/web.rs new file mode 100644 index 0000000..de5c1da --- /dev/null +++ b/nodes/max/plantarium/instance/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/output/src/lib.rs b/nodes/max/plantarium/output/src/lib.rs index 0dd49ef..402195e 100644 --- a/nodes/max/plantarium/output/src/lib.rs +++ b/nodes/max/plantarium/output/src/lib.rs @@ -12,6 +12,8 @@ include_definition_file!("src/inputs.json"); pub fn execute(input: &[i32]) -> Vec { set_panic_hook(); + log!("WASM(output): input: {:?}", input); + let args = split_args(input); log!("WASM(output) args: {:?}", args); @@ -29,16 +31,15 @@ pub fn execute(input: &[i32]) -> Vec { log!("arg_type: {}, \n {:?}", arg_type, arg,); if arg_type == 0 { - // this is path - let vec = arg.to_vec(); - output.push(vec.clone()); + // if this is path we need to extrude it + output.push(arg.to_vec()); let path_data = wrap_path(arg); let geometry = extrude_path(path_data, resolution); output.push(geometry); - } else if arg_type == 1 { - // this is geometry - output.push(arg.to_vec()); + continue; } + + output.push(arg.to_vec()); } concat_args(output.iter().map(|v| v.as_slice()).collect()) diff --git a/packages/runtime/src/runtime-executor.ts b/packages/runtime/src/runtime-executor.ts index 2e790a1..3d35f6e 100644 --- a/packages/runtime/src/runtime-executor.ts +++ b/packages/runtime/src/runtime-executor.ts @@ -184,7 +184,7 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor { const inputNode = node.tmp?.inputNodes?.[key]; if (inputNode) { if (results[inputNode.id] === undefined) { - throw new Error("Input node has no result"); + throw new Error(`Node ${node.type} is missing input from node ${inputNode.type}`); } return results[inputNode.id]; } diff --git a/packages/utils/src/geometry/calculate_normals.rs b/packages/utils/src/geometry/calculate_normals.rs index b38d17f..166b132 100644 --- a/packages/utils/src/geometry/calculate_normals.rs +++ b/packages/utils/src/geometry/calculate_normals.rs @@ -1,7 +1,7 @@ use crate::{decode_float, encode_float}; use glam::Vec3; -pub fn calculate_normals(geometry: &mut [i32]) { +pub fn calculate_normals(geometry: &mut [i32]) -> &mut [i32] { let vertex_count = geometry[1] as usize; let face_count = geometry[2] as usize; let index_start = 3; @@ -47,4 +47,5 @@ pub fn calculate_normals(geometry: &mut [i32]) { geometry[normals_start + idx + 2] = encode_float(normal.z); } } + geometry } diff --git a/packages/utils/src/geometry/geometry_data.rs b/packages/utils/src/geometry/geometry_data.rs index 3e0f4d5..3edc965 100644 --- a/packages/utils/src/geometry/geometry_data.rs +++ b/packages/utils/src/geometry/geometry_data.rs @@ -11,6 +11,30 @@ pub struct GeometryData<'a> { pub faces: &'a mut [i32], // View into `data` } +impl GeometryData<'_> { + pub fn set_position(&mut self, index: usize, x: f32, y: f32, z: f32) { + assert!(index < self.positions.len() / 3, "Index out of bounds."); + let i = index * 3; + self.positions[i] = x; + self.positions[i + 1] = y; + self.positions[i + 2] = z; + } + pub fn set_normal(&mut self, index: usize, x: f32, y: f32, z: f32) { + assert!(index < self.normals.len() / 3, "Index out of bounds."); + let i = index * 3; + self.normals[i] = x; + self.normals[i + 1] = y; + self.normals[i + 2] = z; + } + pub fn set_face(&mut self, index: usize, a: i32, b: i32, c: i32) { + assert!(index < self.faces.len() / 3, "Index out of bounds."); + let i = index * 3; + self.faces[i] = a; + self.faces[i + 1] = b; + self.faces[i + 2] = c; + } +} + pub fn create_geometry_data(vertex_amount: usize, face_amount: usize) -> Vec { let amount = GEOMETRY_HEADER_SIZE // definition (type, vertex_amount, face_amount) + 4 // opening and closing brackets diff --git a/packages/utils/src/geometry/instance_data.rs b/packages/utils/src/geometry/instance_data.rs new file mode 100644 index 0000000..ed92519 --- /dev/null +++ b/packages/utils/src/geometry/instance_data.rs @@ -0,0 +1,118 @@ +use super::GeometryData; + +pub struct InstanceData<'a> { + pub instances: &'a mut [f32], // View into `data` + pub instance_amount: usize, + pub positions: &'a mut [f32], // View into `data` + pub normals: &'a mut [f32], // View into `data` + pub faces: &'a mut [i32], // View into `data` +} + +impl InstanceData<'_> { + pub fn set_geometry(&mut self, geometry: GeometryData) { + assert_eq!(self.positions.len(), geometry.positions.len()); + + for i in 0..self.positions.len() { + self.positions[i] = geometry.positions[i]; + } + for i in 0..self.normals.len() { + self.normals[i] = geometry.normals[i]; + } + for i in 0..self.faces.len() { + self.faces[i] = geometry.faces[i]; + } + } + + pub fn set_transformation_matrix(&mut self, index: usize, matrix: &[f32; 16]) { + assert_eq!(matrix.len(), 16, "Matrix length must be 16."); + assert!( + index * 16 + 16 <= self.instances.len(), + "Matrix does not fit. index: {} len: {}", + index, + self.instances.len() + ); + + (0..16).for_each(|i| { + self.instances[index * 16 + i] = matrix[i]; + }); + } +} + +static INSTANCE_HEADER_SIZE: usize = 5; +// 0: type instance = 2; +// 1: vertex amount +// 2: face amount +// 3: stem_depth +// 4: instance amount + +pub fn create_instance_data( + vertex_amount: usize, + face_amount: usize, + instance_amount: usize, +) -> Vec { + let amount = INSTANCE_HEADER_SIZE // definition (type, vertex_amount, face_amount, instance_amount) + + 4 // opening and closing brackets + + instance_amount * 16 // instances stored as 4x4 matrices + + vertex_amount * 3 // positions + + vertex_amount * 3 // normals + + face_amount * 3; // faces + + let mut geo = vec![0; amount]; + + geo[0] = 0; // opening bracket + geo[1] = amount as i32 - 3; // opening bracket + geo[2] = 2; // type: instance + geo[3] = vertex_amount as i32; + geo[4] = face_amount as i32; + geo[5] = instance_amount as i32; + geo[6] = 1; // stem_depth + + geo[amount - 2] = 1; // closing bracket + geo[amount - 1] = 1; // closing bracket + + geo +} + +pub fn wrap_instance_data(instances: &mut [i32]) -> InstanceData { + assert!( + instances.len() > INSTANCE_HEADER_SIZE, + "Instance vector does not contain enough data for a header." + ); + + let (header, rest) = instances.split_at_mut(2 + INSTANCE_HEADER_SIZE); + + let vertices_amount = header[3] as usize; + let face_amount = header[4] as usize; + let instance_amount = header[5] as usize; + + let (faces, rest) = rest.split_at_mut(face_amount * 3); + let (positions_slice, rest) = rest.split_at_mut(vertices_amount * 3); + let (normals_slice, rest) = rest.split_at_mut(vertices_amount * 3); + let (instances_slice, _) = rest.split_at_mut(instance_amount * 16); + + let positions: &mut [f32] = unsafe { + std::slice::from_raw_parts_mut( + positions_slice.as_mut_ptr() as *mut f32, + positions_slice.len(), + ) + }; + + let normals: &mut [f32] = unsafe { + std::slice::from_raw_parts_mut(normals_slice.as_mut_ptr() as *mut f32, normals_slice.len()) + }; + + let instances: &mut [f32] = unsafe { + std::slice::from_raw_parts_mut( + instances_slice.as_mut_ptr() as *mut f32, + instances_slice.len(), + ) + }; + + InstanceData { + instances, + instance_amount, + positions, + normals, + faces, + } +} diff --git a/packages/utils/src/geometry/mod.rs b/packages/utils/src/geometry/mod.rs index 44574af..e6e5fdb 100644 --- a/packages/utils/src/geometry/mod.rs +++ b/packages/utils/src/geometry/mod.rs @@ -1,6 +1,7 @@ mod calculate_normals; mod extrude_path; mod geometry_data; +mod instance_data; mod math; mod path_data; mod transform; @@ -8,6 +9,7 @@ mod transform; pub use calculate_normals::*; pub use extrude_path::*; pub use geometry_data::*; +pub use instance_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 7050aef..f3de7e9 100644 --- a/packages/utils/src/geometry/path_data.rs +++ b/packages/utils/src/geometry/path_data.rs @@ -31,6 +31,7 @@ impl PathDataMut<'_> { } } +#[derive(Debug)] pub struct PathData<'a> { pub depth: i32, pub length: usize, @@ -55,6 +56,15 @@ impl PathData<'_> { } l } + pub fn get_point_at(&self, alpha: f32) -> [f32; 4] { + get_point_at_path(self.points, alpha) + } + pub fn get_direction_at(&self, alpha: f32) -> [f32; 3] { + get_direction_at_path(self.points, alpha) + } + pub fn interpolate_along(&self, alpha: f32) -> (Vec4, Vec3, Vec3) { + interpolate_along_path(self.points, alpha) + } } pub fn create_multiple_paths(amount: usize, point_amount: usize, depth: i32) -> Vec { diff --git a/packages/utils/src/tree.rs b/packages/utils/src/tree.rs index 6527ddd..67b7674 100644 --- a/packages/utils/src/tree.rs +++ b/packages/utils/src/tree.rs @@ -118,11 +118,12 @@ pub fn concat_args(mut data: Vec<&[i32]>) -> Vec { } pub fn wrap_arg(arg: &[i32]) -> Vec { - let mut out_args = Vec::with_capacity(arg.len() + 8); - out_args.extend_from_slice(&[0, 1, 0, arg.len() as i32 + 1]); + let mut out_args = Vec::with_capacity(arg.len() + 4); + out_args.push(0); + out_args.push(arg.len() as i32 + 1); out_args.extend_from_slice(arg); - out_args.extend_from_slice(&[1, 1]); - out_args.extend_from_slice(&[1, 1]); + out_args.push(1); + out_args.push(1); out_args }