diff --git a/.gitignore b/.gitignore index 6db330e..082663a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ /target Cargo.lock .idea +.vscode build backup otls/backup @@ -9,6 +10,8 @@ bindgen/hapi-bindgen/bindings.rs env.ps1 **/*target _build +_tmp +cpp *.hip* .cargo -_* \ No newline at end of file +_* diff --git a/.vscode/settings.json b/.vscode/settings.json index 9fbe60f..b9286af 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,9 +2,10 @@ "cSpell.enabled": false, "python.languageServer": "Pylance", "python.analysis.extraPaths": [ - "C:/Houdini/19.0.455/houdini/python3.7libs" + "C:/Houdini/20.5.445/houdini/python3.11libs" ], "rust-analyzer.linkedProjects": [ ".\\apps\\viewport\\Cargo.toml" ], + "rust-analyzer.showUnlinkedFileNotification": false, } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 0727404..5092829 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,13 +1,30 @@ # hapi-rs changelog +## [0.11.0] + +- Update to Houdini 20.5.445 +- (new in 20.5) Add APIs for setting/getting of unique attribute values +- (new in 20.5) Add initial support for async attribute access (new in 20.5). WIP and not working properly yet. +- (new in 20.5) Add new shared-memory HARS server type +- (new in 20.5) Add HAPI_GetMessageNodeIds and HAPI_GetNodeCookResult APIs +- (new in 20.5) Add performance monitor APIs +- Fixed some issue with PDG blocking cooking +- `quick_session` now uses shared-memory server type instead of named-pipe. +- An experimental Bevy example app. +- Bunch os other small improvements and cleanups +- Mark `Attribute: Send` (by @BerKai97) + +**Some public API have been changed (both on the SideFX and this library side)** + ## [0.10.0] + - **Minimal** Houdini version bumped to 20.0.625. - Support new attribute APIs and add some previously missing APIs. - **Serveral (minimal) public APIs changed**. - Other fixes and cleanup - ## [0.9.3] + - Bump Houdini version to `19.5.716`. - Add API for working with parameter tags. - String parameters of type Node can take `NodeHandle` values. @@ -15,14 +32,18 @@ - Expose cache APIs. ## [0.9.2] + ### New + - Add `NumericAttribute::read_into()` method for reusing a buffer when reading attribute data. - Reintroduced an internal reentrant mutex to make sure 2 or more API calls are atomic. - Add a demo of OpenGL viewer - Update dependencies ## [0.9.1] + ### Changed: + - Remove internal Mutex from `Session`. - Slightly improved `Parameter` APIs. - Use `StringHandle` instead of `i32`. @@ -30,7 +51,9 @@ - Minor cleanups and improvements across the crate. ## [0.9.0] + ### New + - New builder pattern for creating nodes. - New `start_houdini_server` function will launch Houdini application with engine server running in it. See _live_session.rs_ example. @@ -43,8 +66,8 @@ - New `Parameter::save_parm_file` to save the file from the parameter to disk. - Can now delete geometry attributes. - ### Changed + - Simplified `Session::create_node` only take node name now. Use builder pattern for more options. - `connect_to_pipe` now take an optional timeout parameter, and will try to connect multiple times @@ -55,10 +78,10 @@ - Add support for creating nodes for more asset types with less boilerplate code. - Fixed setting of array geometry attributes. - - ## [0.8.0] + ### Changed + - AssetLibrary::try_create_first() can now crate nodes other than of Object type. - Functions taking optional parent (`Option`) are now generic and can take `HoudiniNode` too. - Improve the error type handling and printing. @@ -66,24 +89,30 @@ - Add lots of `debug_assert!` for input validation. ### New + - `ManagerType` enum represents a network root node. - Add several missing geometry APIs. ## [0.7.0] + ## Changed + - Reworked parameter APIs - - Separate `get/set` and `get_array/set_array` methods - - `get/set` now take an index of a parameter tuple. - - Eliminate extra String clone for `set_*` string parameters. + - Separate `get/set` and `get_array/set_array` methods + - `get/set` now take an index of a parameter tuple. + - Eliminate extra String clone for `set_*` string parameters. ## [0.6.0] + ### New + - Switch to HAPI-5.0 (Houdini 19.5) API. - Implement most common PDG APIs for event-based (async) cooking. - Builder pattern for `SessionOption` - Example of event-based cooking of PDG network. ### Changed + - Make `DataArray` types public. - Session creation APIs now require `SessionOptions` argument. - Add new metadata to `Session` handle with extra information about connection. diff --git a/Cargo.toml b/Cargo.toml index dbef431..8839db2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,28 +6,26 @@ repository = "https://github.com/alexxbb/hapi-rs/" keywords = ["vfx", "graphics", "gamedev", "houdini"] categories = ["graphics", "game-development"] readme = "README.md" -version = "0.10.0" +version = "0.11.0" authors = ["Aleksei Rusev "] edition = "2021" license = "MIT" -exclude = [ - "otls/*", -] +include = ["/src", "build.rs", "LICENSE"] [dependencies] -log = "0.4.14" -paste = "1.0.6" -parking_lot = "0.12.1" -duplicate = { version = "1.0.0", features = [], default-features = false } +log = "0.4.22" +paste = "1.0.15" +parking_lot = "0.12.3" +duplicate = { version = "2.0.0", features = [], default-features = false } debug-ignore = "1.0.5" -tempfile = "3.3.0" +tempfile = "3.13.0" [dev-dependencies] -once_cell = "1.5.2" -env_logger = "0.10.0" +once_cell = "1.20.2" +env_logger = "0.11.5" prettytable-rs = "0.10.0" -fastrand = "1.6.0" -anyhow = "1.0.66" -argh = "0.1.9" -ctrlc = "3.2.5" +fastrand = "2.1.1" +anyhow = "1.0.91" +argh = "0.1.12" +ctrlc = "3.4.5" tinyjson = "2.5.1" diff --git a/LICENSE b/LICENSE index 923ec44..abb3eba 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) [2022] [Aleksei Rusev] +Copyright (c) [2024] [Aleksei Rusev] Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/apps/bevy/Cargo.toml b/apps/bevy/Cargo.toml new file mode 100644 index 0000000..729f5c5 --- /dev/null +++ b/apps/bevy/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "hapi-bevy" +version = "0.1.0" +edition = "2021" + +[dependencies] +hapi-rs = { path = "../..", version = "*" } +bevy_panorbit_camera = "0.21.2" + + +[features] +dev = ["bevy/dynamic_linking"] + + +[dependencies.bevy] +version = "0.15.0" +default-features = false +features = [ + "bevy_window", + "bevy_winit", + "bevy_pbr", + "bevy_ui", + "default_font", + "tonemapping_luts", + "ktx2", + "png", + "bevy_state", + "bevy_dev_tools", + "multi_threaded" +] + +[profile.dev.package."*"] +opt-level = 3 \ No newline at end of file diff --git a/apps/bevy/README.md b/apps/bevy/README.md new file mode 100644 index 0000000..7d4dd47 --- /dev/null +++ b/apps/bevy/README.md @@ -0,0 +1,3 @@ +# A simple experimental Bevy app which can load HDA geometry and render it. + +`cargo run --release` \ No newline at end of file diff --git a/apps/bevy/assets/environment_maps/pisa_diffuse_rgb9e5_zstd.ktx2 b/apps/bevy/assets/environment_maps/pisa_diffuse_rgb9e5_zstd.ktx2 new file mode 100644 index 0000000..e260df2 Binary files /dev/null and b/apps/bevy/assets/environment_maps/pisa_diffuse_rgb9e5_zstd.ktx2 differ diff --git a/apps/bevy/assets/environment_maps/pisa_specular_rgb9e5_zstd.ktx2 b/apps/bevy/assets/environment_maps/pisa_specular_rgb9e5_zstd.ktx2 new file mode 100644 index 0000000..2018838 Binary files /dev/null and b/apps/bevy/assets/environment_maps/pisa_specular_rgb9e5_zstd.ktx2 differ diff --git a/apps/bevy/assets/hda/geo.hda b/apps/bevy/assets/hda/geo.hda new file mode 100644 index 0000000..d97ab95 Binary files /dev/null and b/apps/bevy/assets/hda/geo.hda differ diff --git a/apps/bevy/src/houdini_mesh.rs b/apps/bevy/src/houdini_mesh.rs new file mode 100644 index 0000000..03eaca8 --- /dev/null +++ b/apps/bevy/src/houdini_mesh.rs @@ -0,0 +1,388 @@ +use bevy::asset::RenderAssetUsages; +use bevy::prelude::Mesh; +use bevy::render::mesh::PrimitiveTopology; +use hapi_rs::attribute::NumericAttr; +use hapi_rs::geometry::{extra::GeometryExtension, AttributeName, AttributeOwner, Geometry}; +use hapi_rs::Result; + +pub enum NormalAttribute { + Point(Vec), + Vertex(Vec), +} + +pub enum ColorAttribute { + Point(Vec), + Vertex(Vec), +} + +pub struct HoudiniMeshData { + pub vertex_list: Vec, + pub face_counts: Vec, + pub positions: Option>, + pub normals: Option, + pub colors: Option, + pub uvs: Option>, +} + +fn get_houdini_geometry_color(geometry: &Geometry) -> Result> { + let part = geometry.part_info(0)?.expect("part 0"); + let mut is_point_attr = false; + let mut cd_attr = geometry.get_color_attribute(&part, AttributeOwner::Vertex)?; + if cd_attr.is_none() { + is_point_attr = true; + cd_attr = geometry.get_color_attribute(&part, AttributeOwner::Point)?; + } + match cd_attr { + Some(cd_attr) => { + let values = cd_attr.get(0)?; + Ok(Some(match is_point_attr { + true => ColorAttribute::Point(values), + false => ColorAttribute::Vertex(values), + })) + } + None => Ok(None), + } +} + +fn get_houdini_geometry_normals(geometry: &Geometry) -> Result> { + let part = geometry.part_info(0)?.expect("part 0"); + let mut is_point_attr = false; + let mut n_attr = geometry.get_normal_attribute(&part, AttributeOwner::Vertex)?; + if n_attr.is_none() { + is_point_attr = true; + n_attr = geometry.get_normal_attribute(&part, AttributeOwner::Point)?; + } + match n_attr { + Some(n_attr) => { + let values = n_attr.get(0)?; + Ok(Some(match is_point_attr { + true => NormalAttribute::Point(values), + false => NormalAttribute::Vertex(values), + })) + } + None => Ok(None), + } +} + +#[derive(Debug, Default)] +pub struct HoudiniGeometryDataBuilder { + positions: bool, + normals: bool, + colors: bool, + uv: bool, +} + +impl HoudiniGeometryDataBuilder { + pub fn new() -> Self { + Self::default() + } + pub fn with_positions(mut self) -> HoudiniGeometryDataBuilder { + self.positions = true; + self + } + + pub fn with_normals(mut self) -> HoudiniGeometryDataBuilder { + self.normals = true; + self + } + + pub fn with_uv(mut self) -> HoudiniGeometryDataBuilder { + self.uv = true; + self + } + + pub fn with_color(mut self) -> HoudiniGeometryDataBuilder { + self.colors = true; + self + } + pub fn build(self, geometry: &Geometry) -> Result { + get_houdini_geometry_data_arrays( + geometry, + self.positions, + self.normals, + self.colors, + self.uv, + ) + } +} + +fn get_houdini_geometry_data_arrays( + geometry: &Geometry, + build_positions: bool, + build_normals: bool, + build_colors: bool, + build_uv: bool, +) -> Result { + let part = geometry.part_info(0)?.unwrap(); + let vertex_list = geometry.vertex_list(&part)?; + let face_counts = geometry.get_face_counts(&part)?; + let mut positions = None; + let mut normals = None; + let mut colors = None; + let mut uvs = None; + if build_positions { + positions = Some( + geometry + .get_position_attribute(part.part_id())? + .get(part.part_id())?, + ) + } + if build_normals { + normals = get_houdini_geometry_normals(geometry)?; + } + + if build_colors { + colors = get_houdini_geometry_color(geometry)?; + } + + if build_uv { + uvs = match geometry.get_attribute( + part.part_id(), + AttributeOwner::Vertex, + AttributeName::Uv, + )? { + None => None, + Some(attr) => Some( + attr.downcast::>() + .expect("UV is NumericAttribute") + .get(part.part_id())?, + ), + }; + } + + Ok(HoudiniMeshData { + vertex_list, + face_counts, + positions, + normals, + colors, + uvs, + }) +} + +pub fn vertex_deform(geometry: &Geometry) -> Result { + let mesh_data = HoudiniGeometryDataBuilder::new() + .with_positions() + .with_normals() + .build(&geometry)?; + + Ok(convert_vertex_data(mesh_data)) +} + +struct Offset { + global_vertex_offset: (usize, usize, usize), + triangle_vertex_offset: (usize, usize, usize), +} + +fn iterate_buffer_offsets(vertex_list: &[i32], face_counts: &[i32], mut func: impl FnMut(Offset)) { + let mut offset = 0usize; + + for vertex_per_face in face_counts { + let vertex_per_face = *vertex_per_face as usize; + let num_tris = vertex_per_face - 2; + + for tr in 0..num_tris { + let off0 = offset + tr + 2; + let off1 = offset + tr + 1; + let off2 = offset + 0; + + let point_0_index = vertex_list[off0] as usize; + let point_1_index = vertex_list[off1] as usize; + let point_2_index = vertex_list[off2] as usize; + let offset = Offset { + global_vertex_offset: (off0, off1, off2), + triangle_vertex_offset: (point_0_index, point_1_index, point_2_index), + }; + func(offset); + } + offset += vertex_per_face; + } +} + +pub struct BevyMeshData { + pub vertices: Option>, + pub normals: Option>, + pub colors: Option>, + pub uvs: Option>, +} + +pub fn convert_vertex_data(data: HoudiniMeshData) -> BevyMeshData { + let HoudiniMeshData { + vertex_list, + face_counts, + positions, + normals, + colors, + uvs, + } = data; + + let num_vertices = face_counts.iter().sum::() as usize; + let mut out_vertices = positions + .is_some() + .then(|| Vec::with_capacity(num_vertices)); + let mut out_normals = normals.is_some().then(|| Vec::with_capacity(num_vertices)); + let mut out_colors = colors.is_some().then(|| Vec::with_capacity(num_vertices)); + let mut out_uvs = uvs.is_some().then(|| Vec::with_capacity(num_vertices)); + + iterate_buffer_offsets(&vertex_list, &face_counts, |offset| { + let Offset { + global_vertex_offset, + triangle_vertex_offset, + .. + } = offset; + + if let (Some(positions), Some(out_vertices)) = (&positions, &mut out_vertices) { + out_vertices.push([ + positions[triangle_vertex_offset.0 * 3 + 0], + positions[triangle_vertex_offset.0 * 3 + 1], + positions[triangle_vertex_offset.0 * 3 + 2], + ]); + out_vertices.push([ + positions[triangle_vertex_offset.1 * 3 + 0], + positions[triangle_vertex_offset.1 * 3 + 1], + positions[triangle_vertex_offset.1 * 3 + 2], + ]); + out_vertices.push([ + positions[triangle_vertex_offset.2 * 3 + 0], + positions[triangle_vertex_offset.2 * 3 + 1], + positions[triangle_vertex_offset.2 * 3 + 2], + ]); + } + + if let (Some(normals_attr), Some(out_normals)) = (&normals, &mut out_normals) { + let (data, idx_0, idx_1, idx_2) = match normals_attr { + NormalAttribute::Point(data) => ( + data, + triangle_vertex_offset.0, + triangle_vertex_offset.1, + triangle_vertex_offset.2, + ), + NormalAttribute::Vertex(data) => ( + data, + global_vertex_offset.0, + global_vertex_offset.1, + global_vertex_offset.2, + ), + }; + out_normals.push([ + data[idx_0 * 3 + 0], + data[idx_0 * 3 + 1], + data[idx_0 * 3 + 2], + ]); + + out_normals.push([ + data[idx_1 * 3 + 0], + data[idx_1 * 3 + 1], + data[idx_1 * 3 + 2], + ]); + + out_normals.push([ + data[idx_2 * 3 + 0], + data[idx_2 * 3 + 1], + data[idx_2 * 3 + 2], + ]); + } + + if let (Some(colors_attr), Some(out_colors)) = (&colors, &mut out_colors) { + let (data, idx_0, idx_1, idx_2) = match colors_attr { + ColorAttribute::Point(data) => ( + data, + triangle_vertex_offset.0, + triangle_vertex_offset.1, + triangle_vertex_offset.2, + ), + ColorAttribute::Vertex(data) => ( + data, + global_vertex_offset.0, + global_vertex_offset.1, + global_vertex_offset.2, + ), + }; + + out_colors.push([ + data[idx_0 * 3 + 0], + data[idx_0 * 3 + 1], + data[idx_0 * 3 + 2], + 0., + ]); + + out_colors.push([ + data[idx_1 * 3 + 0], + data[idx_1 * 3 + 1], + data[idx_1 * 3 + 2], + 0., + ]); + + out_colors.push([ + data[idx_2 * 3 + 0], + data[idx_2 * 3 + 1], + data[idx_2 * 3 + 2], + 0., + ]); + } + + if let (Some(uvs), Some(out_uvs)) = (&uvs, &mut out_uvs) { + out_uvs.extend([ + [ + uvs[global_vertex_offset.0 * 3 + 0], + 1.0 - uvs[global_vertex_offset.0 * 3 + 1], + ], + [ + uvs[global_vertex_offset.1 * 3 + 0], + 1.0 - uvs[global_vertex_offset.1 * 3 + 1], + ], + [ + uvs[global_vertex_offset.2 * 3 + 0], + 1.0 - uvs[global_vertex_offset.2 * 3 + 1], + ], + ]); + } + }); + + BevyMeshData { + vertices: out_vertices, + normals: out_normals, + colors: out_colors, + uvs: out_uvs, + } +} + +pub fn create_bevy_mesh_from_houdini(geometry: &Geometry) -> Result { + let mesh_data = HoudiniGeometryDataBuilder::new() + .with_positions() + .with_normals() + .with_color() + .with_uv() + .build(&geometry)?; + + let bevy_mesh = convert_vertex_data(mesh_data); + + let mut mesh = Mesh::new( + PrimitiveTopology::TriangleList, + RenderAssetUsages::default(), + ); + + if let Some(position) = bevy_mesh.vertices { + mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, position); + } + if let Some(colors) = bevy_mesh.colors { + mesh.insert_attribute(Mesh::ATTRIBUTE_COLOR, colors); + } + + if let Some(uv) = bevy_mesh.uvs { + mesh.insert_attribute(Mesh::ATTRIBUTE_UV_0, uv); + } + + if let Some(normals) = bevy_mesh.normals { + mesh.insert_attribute(Mesh::ATTRIBUTE_NORMAL, normals); + if mesh.generate_tangents().is_err() { + eprintln!("Could not compute mesh tangents"); + } + } else { + // smooth normals can only be computed for an indexed mesh + println!("Computing flat normals"); + mesh.compute_flat_normals(); + } + + Ok(mesh) +} diff --git a/apps/bevy/src/main.rs b/apps/bevy/src/main.rs new file mode 100644 index 0000000..4aa69c2 --- /dev/null +++ b/apps/bevy/src/main.rs @@ -0,0 +1,254 @@ +mod houdini_mesh; +mod material; + +use bevy::dev_tools::fps_overlay::{FpsOverlayConfig, FpsOverlayPlugin}; +use bevy::prelude::*; +use bevy::render::mesh::VertexAttributeValues; +use bevy::tasks::{block_on, futures_lite::future}; +use bevy::tasks::{AsyncComputeTaskPool, Task}; +use bevy::text::FontSmoothing; +use bevy_panorbit_camera::{PanOrbitCamera, PanOrbitCameraPlugin}; +use hapi_rs::geometry::Geometry; +use hapi_rs::node::HoudiniNode; +use hapi_rs::parameter::Parameter; +#[allow(unused_imports)] +use hapi_rs::session::connect_to_memory_server; +use hapi_rs::session::{new_in_process, SessionOptions}; +use hapi_rs::Result as HapiResult; + +#[derive(Resource)] +struct HoudiniResource { + asset: HoudiniNode, + geometry: Geometry, +} + +#[derive(Component)] +struct HoudiniMesh { + animated: bool, +} + +#[derive(Default, Debug, Hash, Eq, PartialEq, Clone, States)] +enum HoudiniSetupState { + #[default] + Loading, + Ready, +} + +fn main() { + App::new() + .add_plugins(( + DefaultPlugins.set(WindowPlugin { + primary_window: Some(Window { + title: "HAPI Demo".to_string(), + ..default() + }), + ..default() + }), + PanOrbitCameraPlugin, + FpsOverlayPlugin { + config: FpsOverlayConfig { + text_config: TextFont { + // Here we define size of our overlay + font_size: 16.0, + // If we want, we can use a custom font + font: default(), + // We could also disable font smoothing, + font_smoothing: FontSmoothing::default(), + }, + enabled: true, + ..default() + }, + }, + )) + .init_state::() + .add_systems(Startup, setup_scene) + .add_systems(Startup, setup_houdini) + .add_systems( + Update, + get_loading_state.run_if(in_state(HoudiniSetupState::Loading)), + ) + .add_systems(OnEnter(HoudiniSetupState::Ready), houdini_ready) + .add_systems( + Update, + (input_handler, animate).run_if(in_state(HoudiniSetupState::Ready)), + ) + .run(); +} + +fn houdini_ready( + mut commands: Commands, + text: Single>, + mut meshes: ResMut>, + mut textures: ResMut>, + mut materials: ResMut>, + res: Res, +) { + commands.entity(text.into_inner()).despawn_recursive(); + + let HoudiniResource { asset, geometry } = res.into_inner(); + + let tex_maps = material::extract_texture_maps(asset, false).expect("texture maps"); + let mesh = houdini_mesh::create_bevy_mesh_from_houdini(geometry).expect("Bevy mesh"); + let mesh_handle = meshes.add(mesh); + + commands.spawn(( + HoudiniMesh { animated: true }, + Mesh3d(mesh_handle), + MeshMaterial3d(materials.add(StandardMaterial { + base_color_texture: tex_maps.color.map(|image| textures.add(image)), + metallic_roughness_texture: tex_maps.specular.map(|image| textures.add(image)), + normal_map_texture: tex_maps.normal.map(|image| textures.add(image)), + reflectance: 0.5, + perceptual_roughness: 1.0, + ..default() + })), + )); +} + +#[derive(Debug, Component)] +pub struct LoadingText; + +fn setup_scene(mut commands: Commands, asset_server: Res) { + commands.spawn(( + LoadingText, + Text::new("Initializing Houdini...".to_string()), + TextFont { + font_size: 50.0, + ..Default::default() + }, + Node { + align_content: AlignContent::Center, + align_self: AlignSelf::Center, + ..default() + }, + )); + commands.spawn(( + PanOrbitCamera { + focus: Vec3::new(0.0, 1.0, 0.0), + orbit_smoothness: 0.6, + orbit_sensitivity: 1.8, + button_pan: MouseButton::Middle, + ..default() + }, + Transform::from_xyz(3.0, 1.0, 0.0).looking_at(Vec3::ZERO, Vec3::Y), + EnvironmentMapLight { + diffuse_map: asset_server.load("environment_maps/pisa_diffuse_rgb9e5_zstd.ktx2"), + specular_map: asset_server.load("environment_maps/pisa_specular_rgb9e5_zstd.ktx2"), + intensity: 1800.0, + ..default() + }, + )); + + commands.spawn(( + DirectionalLight { + illuminance: 2_500., + ..default() + }, + Transform::from_xyz(50.0, 50.0, 50.0), + )); +} + +fn animate( + query: Query<(&Mesh3d, &HoudiniMesh)>, + session: Res, + time: Res