refactor: let the bevy rewrite begin: mostly working model nodes
Signed-off-by: Schmarni <marnistromer@gmail.com>
This commit is contained in:
5132
Cargo.lock
generated
5132
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
49
Cargo.toml
49
Cargo.toml
@@ -21,37 +21,29 @@ name = "stardust-xr-server"
|
|||||||
path = "src/main.rs"
|
path = "src/main.rs"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["wayland"]
|
default = []
|
||||||
wayland = ["dep:smithay", "dep:wayland-scanner", "dep:wayland-backend"]
|
wayland = ["dep:smithay", "dep:wayland-scanner", "dep:wayland-backend"]
|
||||||
profile_tokio = ["dep:console-subscriber", "tokio/tracing"]
|
profile_tokio = ["dep:console-subscriber", "tokio/tracing"]
|
||||||
profile_app = ["dep:tracing-tracy"]
|
profile_app = ["dep:tracing-tracy"]
|
||||||
local_deps = ["stereokit-rust/force-local-deps"]
|
|
||||||
|
|
||||||
[package.metadata.appimage]
|
[package.metadata.appimage]
|
||||||
auto_link = true
|
auto_link = true
|
||||||
auto_link_exclude_list = [
|
auto_link_exclude_list = ["libc*", "libdl*", "libpthread*", "ld-linux*"]
|
||||||
"libc*",
|
|
||||||
"libdl*",
|
|
||||||
"libpthread*",
|
|
||||||
"ld-linux*",
|
|
||||||
"libGL*",
|
|
||||||
"libEGL*",
|
|
||||||
]
|
|
||||||
|
|
||||||
[profile.dev.package."*"]
|
[profile.dev.package."*"]
|
||||||
opt-level = 3
|
opt-level = 3
|
||||||
debug = true
|
|
||||||
strip = false
|
|
||||||
debug-assertions = true
|
|
||||||
overflow-checks = true
|
|
||||||
|
|
||||||
[profile.release]
|
# [profile.release]
|
||||||
opt-level = 3
|
# opt-level = 3
|
||||||
debug = "line-tables-only"
|
# debug = "line-tables-only"
|
||||||
strip = true
|
# strip = true
|
||||||
debug-assertions = true
|
# debug-assertions = true
|
||||||
overflow-checks = false
|
# overflow-checks = false
|
||||||
lto = "thin"
|
# lto = "thin"
|
||||||
|
|
||||||
|
[patch.crates-io]
|
||||||
|
bevy_mod_openxr = { git = "https://github.com/Schmarni-Dev/bevy_openxr", branch = "non_default_wait_frame_system" }
|
||||||
|
bevy_mod_xr = { git = "https://github.com/Schmarni-Dev/bevy_openxr", branch = "non_default_wait_frame_system" }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
# small utility thingys
|
# small utility thingys
|
||||||
@@ -63,6 +55,7 @@ send_wrapper = "0.6.0"
|
|||||||
slotmap = "1.0.7"
|
slotmap = "1.0.7"
|
||||||
global_counter = "=0.2.2"
|
global_counter = "=0.2.2"
|
||||||
parking_lot = "0.12.3"
|
parking_lot = "0.12.3"
|
||||||
|
dashmap = "6.1.0"
|
||||||
|
|
||||||
# rust errors/logging
|
# rust errors/logging
|
||||||
color-eyre = { version = "0.6.3", default-features = false }
|
color-eyre = { version = "0.6.3", default-features = false }
|
||||||
@@ -83,6 +76,17 @@ glam = { version = "0.29.0", features = ["mint", "serde"] }
|
|||||||
mint = "0.5.9"
|
mint = "0.5.9"
|
||||||
tokio = { version = "1.39.2", features = ["rt-multi-thread", "signal", "time"] }
|
tokio = { version = "1.39.2", features = ["rt-multi-thread", "signal", "time"] }
|
||||||
|
|
||||||
|
# bevy
|
||||||
|
bevy = { version = "0.16", features = ["wayland", "bevy_remote"] }
|
||||||
|
bevy_mod_xr = "0.3"
|
||||||
|
bevy_mod_openxr = "0.3"
|
||||||
|
bevy_mod_meshtext.git = "https://github.com/Schmarni-Dev/bevy_mod_meshtext"
|
||||||
|
# bevy_sk.git = "https://github.com/MalekiRe/bevy_sk"
|
||||||
|
# bevy_sk = { git = "https://github.com/Schmarni-Dev/bevy_sk", branch = "fix_mat_stuff" }
|
||||||
|
bevy_sk.path = "../bevy_sk"
|
||||||
|
|
||||||
|
openxr = "0.19"
|
||||||
|
|
||||||
# linux stuffs
|
# linux stuffs
|
||||||
input-event-codes = "6.2.0"
|
input-event-codes = "6.2.0"
|
||||||
zbus = { version = "5.0.0", default-features = false, features = ["tokio"] }
|
zbus = { version = "5.0.0", default-features = false, features = ["tokio"] }
|
||||||
@@ -92,7 +96,7 @@ xkbcommon-rs = "0.1.0"
|
|||||||
# wayland
|
# wayland
|
||||||
wayland-backend = { version = "0.3.7", optional = true, default-features = false }
|
wayland-backend = { version = "0.3.7", optional = true, default-features = false }
|
||||||
wayland-scanner = { version = "0.31.4", optional = true }
|
wayland-scanner = { version = "0.31.4", optional = true }
|
||||||
dashmap = "6.1.0"
|
|
||||||
|
|
||||||
[dependencies.smithay]
|
[dependencies.smithay]
|
||||||
git = "https://github.com/smithay/smithay.git"
|
git = "https://github.com/smithay/smithay.git"
|
||||||
@@ -100,7 +104,6 @@ default-features = false
|
|||||||
features = ["desktop", "backend_drm", "renderer_gl", "wayland_frontend"]
|
features = ["desktop", "backend_drm", "renderer_gl", "wayland_frontend"]
|
||||||
optional = true
|
optional = true
|
||||||
|
|
||||||
|
|
||||||
[dependencies.stereokit-rust]
|
[dependencies.stereokit-rust]
|
||||||
git = "https://github.com/mvvvv/StereoKit-rust.git"
|
git = "https://github.com/mvvvv/StereoKit-rust.git"
|
||||||
rev = "73ffaae6f42aa369e599a6ea0391f77840d682d8"
|
rev = "73ffaae6f42aa369e599a6ea0391f77840d682d8"
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ use std::{
|
|||||||
time::Instant,
|
time::Instant,
|
||||||
};
|
};
|
||||||
use tokio::{net::UnixStream, sync::watch, task::JoinHandle};
|
use tokio::{net::UnixStream, sync::watch, task::JoinHandle};
|
||||||
use tracing::info;
|
use tracing::{info, warn};
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
pub static ref CLIENTS: OwnedRegistry<Client> = OwnedRegistry::new();
|
pub static ref CLIENTS: OwnedRegistry<Client> = OwnedRegistry::new();
|
||||||
@@ -201,7 +201,9 @@ impl Client {
|
|||||||
std::fs::read_link(cwd_proc_path).ok()
|
std::fs::read_link(cwd_proc_path).ok()
|
||||||
}
|
}
|
||||||
pub async fn save_state(&self) -> Option<ClientStateParsed> {
|
pub async fn save_state(&self) -> Option<ClientStateParsed> {
|
||||||
|
println!("start save state");
|
||||||
let internal = self.root.get()?.save_state().await.ok()?;
|
let internal = self.root.get()?.save_state().await.ok()?;
|
||||||
|
println!("finished save state");
|
||||||
Some(ClientStateParsed::from_deserialized(self, internal))
|
Some(ClientStateParsed::from_deserialized(self, internal))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
155
src/main.rs
155
src/main.rs
@@ -10,11 +10,39 @@ use crate::core::destroy_queue;
|
|||||||
use crate::nodes::items::camera;
|
use crate::nodes::items::camera;
|
||||||
use crate::nodes::{audio, drawable, input};
|
use crate::nodes::{audio, drawable, input};
|
||||||
|
|
||||||
|
use bevy::MinimalPlugins;
|
||||||
|
use bevy::a11y::AccessibilityPlugin;
|
||||||
|
use bevy::app::{App, ScheduleRunnerPlugin, TerminalCtrlCHandlerPlugin};
|
||||||
|
use bevy::asset::{AssetMetaCheck, UnapprovedPathMode};
|
||||||
|
use bevy::audio::AudioPlugin;
|
||||||
|
use bevy::core_pipeline::CorePipelinePlugin;
|
||||||
|
use bevy::gizmos::GizmoPlugin;
|
||||||
|
use bevy::gltf::GltfPlugin;
|
||||||
|
use bevy::input::{InputPlugin, InputSystem};
|
||||||
|
use bevy::pbr::PbrPlugin;
|
||||||
|
use bevy::remote::RemotePlugin;
|
||||||
|
use bevy::remote::http::RemoteHttpPlugin;
|
||||||
|
use bevy::render::{RenderDebugFlags, RenderPlugin};
|
||||||
|
use bevy::scene::ScenePlugin;
|
||||||
|
use bevy::text::FontLoader;
|
||||||
|
use bevy::winit::{WakeUp, WinitPlugin};
|
||||||
|
use bevy_mod_meshtext::MeshTextPlugin;
|
||||||
|
use bevy_mod_openxr::add_xr_plugins;
|
||||||
|
use bevy_mod_openxr::exts::OxrExtensions;
|
||||||
|
use bevy_mod_openxr::features::overlay::OxrOverlaySettings;
|
||||||
|
use bevy_mod_openxr::init::OxrInitPlugin;
|
||||||
|
use bevy_mod_openxr::reference_space::OxrReferenceSpacePlugin;
|
||||||
|
use bevy_mod_openxr::resources::OxrSessionConfig;
|
||||||
|
use bevy_mod_openxr::types::AppInfo;
|
||||||
|
use bevy_mod_xr::hand_debug_gizmos::HandGizmosPlugin;
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use core::client::{Client, tick_internal_client};
|
use core::client::{Client, tick_internal_client};
|
||||||
use core::task;
|
use core::task;
|
||||||
use directories::ProjectDirs;
|
use directories::ProjectDirs;
|
||||||
|
use nodes::drawable::model::ModelNodePlugin;
|
||||||
|
use nodes::spatial::SpatialNodePlugin;
|
||||||
use objects::ServerObjects;
|
use objects::ServerObjects;
|
||||||
|
use openxr::{EnvironmentBlendMode, ReferenceSpaceType};
|
||||||
use session::{launch_start, save_session};
|
use session::{launch_start, save_session};
|
||||||
use stardust_xr::schemas::dbus::object_registry::ObjectRegistry;
|
use stardust_xr::schemas::dbus::object_registry::ObjectRegistry;
|
||||||
use stardust_xr::server;
|
use stardust_xr::server;
|
||||||
@@ -38,6 +66,8 @@ use tracing_subscriber::{EnvFilter, fmt, prelude::*};
|
|||||||
use zbus::Connection;
|
use zbus::Connection;
|
||||||
use zbus::fdo::ObjectManager;
|
use zbus::fdo::ObjectManager;
|
||||||
|
|
||||||
|
use bevy::prelude::*;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Parser)]
|
#[derive(Debug, Clone, Parser)]
|
||||||
#[clap(author, version, about, long_about = None)]
|
#[clap(author, version, about, long_about = None)]
|
||||||
struct CliArgs {
|
struct CliArgs {
|
||||||
@@ -176,7 +206,7 @@ async fn main() {
|
|||||||
let cli_args = cli_args.clone();
|
let cli_args = cli_args.clone();
|
||||||
let dbus_connection = dbus_connection.clone();
|
let dbus_connection = dbus_connection.clone();
|
||||||
move || {
|
move || {
|
||||||
stereokit_loop(
|
bevy_loop(
|
||||||
sk_ready_notifier,
|
sk_ready_notifier,
|
||||||
project_dirs,
|
project_dirs,
|
||||||
cli_args,
|
cli_args,
|
||||||
@@ -209,6 +239,129 @@ async fn main() {
|
|||||||
static DEFAULT_SKYTEX: OnceLock<Tex> = OnceLock::new();
|
static DEFAULT_SKYTEX: OnceLock<Tex> = OnceLock::new();
|
||||||
static DEFAULT_SKYLIGHT: OnceLock<SphericalHarmonics> = OnceLock::new();
|
static DEFAULT_SKYLIGHT: OnceLock<SphericalHarmonics> = OnceLock::new();
|
||||||
|
|
||||||
|
fn bevy_loop(
|
||||||
|
ready_notifier: Arc<Notify>,
|
||||||
|
project_dirs: Option<ProjectDirs>,
|
||||||
|
args: CliArgs,
|
||||||
|
dbus_connection: Connection,
|
||||||
|
object_registry: ObjectRegistry,
|
||||||
|
) {
|
||||||
|
let mut app = App::new();
|
||||||
|
app.add_plugins(AssetPlugin {
|
||||||
|
meta_check: AssetMetaCheck::Never,
|
||||||
|
unapproved_path_mode: UnapprovedPathMode::Allow,
|
||||||
|
..default()
|
||||||
|
});
|
||||||
|
let mut plugins = MinimalPlugins
|
||||||
|
.build()
|
||||||
|
.disable::<ScheduleRunnerPlugin>()
|
||||||
|
.add(TransformPlugin)
|
||||||
|
.add(InputPlugin)
|
||||||
|
/* .add(AccessibilityPlugin) */;
|
||||||
|
// TODO: figure out headless
|
||||||
|
{
|
||||||
|
plugins = plugins.add(WindowPlugin::default()).add({
|
||||||
|
let mut winit = WinitPlugin::<WakeUp>::default();
|
||||||
|
winit.run_on_any_thread = true;
|
||||||
|
winit
|
||||||
|
});
|
||||||
|
}
|
||||||
|
plugins = plugins
|
||||||
|
.add(TerminalCtrlCHandlerPlugin)
|
||||||
|
// bevy_mod_openxr will replace this, TODO: figure out how to mix this with
|
||||||
|
// bevy-dmabuf
|
||||||
|
.add(RenderPlugin::default())
|
||||||
|
.add(ImagePlugin::default())
|
||||||
|
.add(CorePipelinePlugin)
|
||||||
|
// theoretically we shouldn't need this because of bevy_sk, but everything is tangled in
|
||||||
|
// there and idk what we actually need to run
|
||||||
|
.add(PbrPlugin {
|
||||||
|
// this seems to only apply to StandardMaterial, we don't use that
|
||||||
|
prepass_enabled: true,
|
||||||
|
add_default_deferred_lighting_plugin: false,
|
||||||
|
use_gpu_instance_buffer_builder: true,
|
||||||
|
debug_flags: RenderDebugFlags::default(),
|
||||||
|
})
|
||||||
|
// required for gltf
|
||||||
|
.add(ScenePlugin)
|
||||||
|
.add(GltfPlugin::default())
|
||||||
|
.add(AudioPlugin::default())
|
||||||
|
.add(GizmoPlugin)
|
||||||
|
.add(AccessibilityPlugin);
|
||||||
|
app.add_plugins(
|
||||||
|
add_xr_plugins(plugins)
|
||||||
|
.set(OxrInitPlugin {
|
||||||
|
app_info: AppInfo {
|
||||||
|
name: "Stardust XR".into(),
|
||||||
|
version: bevy_mod_openxr::types::Version(0, 44, 1),
|
||||||
|
},
|
||||||
|
exts: {
|
||||||
|
// all OpenXR extensions can be requested here
|
||||||
|
let mut exts = OxrExtensions::default();
|
||||||
|
exts.enable_hand_tracking();
|
||||||
|
if args.overlay_priority.is_some() {
|
||||||
|
exts.enable_extx_overlay();
|
||||||
|
}
|
||||||
|
exts
|
||||||
|
},
|
||||||
|
..default()
|
||||||
|
})
|
||||||
|
.set(OxrReferenceSpacePlugin {
|
||||||
|
default_primary_ref_space: ReferenceSpaceType::LOCAL,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
app.init_asset::<Font>().init_asset_loader::<FontLoader>();
|
||||||
|
if let Some(priority) = args.overlay_priority {
|
||||||
|
app.insert_resource(OxrOverlaySettings {
|
||||||
|
session_layer_placement: priority,
|
||||||
|
..default()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
app.add_plugins((
|
||||||
|
bevy_sk::hand::HandPlugin,
|
||||||
|
bevy_sk::vr_materials::SkMaterialPlugin {
|
||||||
|
replace_standard_material: true,
|
||||||
|
},
|
||||||
|
bevy_sk::skytext::SphericalHarmonicsPlugin,
|
||||||
|
));
|
||||||
|
app.add_plugins(HandGizmosPlugin);
|
||||||
|
app.add_plugins(MeshTextPlugin);
|
||||||
|
app.insert_resource(OxrSessionConfig {
|
||||||
|
blend_modes: Some(vec![
|
||||||
|
EnvironmentBlendMode::ALPHA_BLEND,
|
||||||
|
EnvironmentBlendMode::ADDITIVE,
|
||||||
|
EnvironmentBlendMode::OPAQUE,
|
||||||
|
]),
|
||||||
|
..default()
|
||||||
|
});
|
||||||
|
app.insert_resource(ClearColor(Color::BLACK.with_alpha(0.0)));
|
||||||
|
app.add_plugins((RemotePlugin::default(), RemoteHttpPlugin::default()));
|
||||||
|
app.add_plugins((SpatialNodePlugin, ModelNodePlugin));
|
||||||
|
ready_notifier.notify_waiters();
|
||||||
|
app.add_systems(PreUpdate, main_loop_system.after(InputSystem));
|
||||||
|
app.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main_loop_system(world: &mut World) {
|
||||||
|
// camera::update(token);
|
||||||
|
#[cfg(feature = "wayland")]
|
||||||
|
wayland.frame_event();
|
||||||
|
destroy_queue::clear();
|
||||||
|
|
||||||
|
// objects.update(&sk, token, &dbus_connection, &object_registry);
|
||||||
|
// input::process_input();
|
||||||
|
let time = world.resource::<bevy::prelude::Time>().delta_secs_f64();
|
||||||
|
nodes::root::Root::send_frame_events(time);
|
||||||
|
|
||||||
|
// Wait
|
||||||
|
|
||||||
|
tick_internal_client();
|
||||||
|
#[cfg(feature = "wayland")]
|
||||||
|
wayland.update();
|
||||||
|
// drawable::draw(token);
|
||||||
|
// audio::update();
|
||||||
|
}
|
||||||
|
|
||||||
fn stereokit_loop(
|
fn stereokit_loop(
|
||||||
sk_ready_notifier: Arc<Notify>,
|
sk_ready_notifier: Arc<Notify>,
|
||||||
project_dirs: Option<ProjectDirs>,
|
project_dirs: Option<ProjectDirs>,
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ use stereokit_rust::{sk::MainThreadToken, system::Renderer, tex::SHCubemap};
|
|||||||
// #[instrument(level = "debug", skip(sk))]
|
// #[instrument(level = "debug", skip(sk))]
|
||||||
pub fn draw(token: &MainThreadToken) {
|
pub fn draw(token: &MainThreadToken) {
|
||||||
lines::draw_all(token);
|
lines::draw_all(token);
|
||||||
model::draw_all(token);
|
|
||||||
text::draw_all(token);
|
text::draw_all(token);
|
||||||
match QUEUED_SKYTEX.lock().take() {
|
match QUEUED_SKYTEX.lock().take() {
|
||||||
Some(Some(skytex)) => {
|
Some(Some(skytex)) => {
|
||||||
|
|||||||
@@ -6,128 +6,350 @@ use crate::core::registry::Registry;
|
|||||||
use crate::core::resource::get_resource_file;
|
use crate::core::resource::get_resource_file;
|
||||||
use crate::nodes::Node;
|
use crate::nodes::Node;
|
||||||
use crate::nodes::alias::{Alias, AliasList};
|
use crate::nodes::alias::{Alias, AliasList};
|
||||||
use crate::nodes::spatial::Spatial;
|
use crate::nodes::spatial::{Spatial, SpatialNode};
|
||||||
|
use bevy::prelude::*;
|
||||||
|
use bevy_sk::vr_materials::PbrMaterial;
|
||||||
use color_eyre::eyre::eyre;
|
use color_eyre::eyre::eyre;
|
||||||
use glam::{Mat4, Vec2, Vec3};
|
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use rustc_hash::FxHashMap;
|
use rustc_hash::{FxHashMap, FxHasher};
|
||||||
use stardust_xr::values::ResourceID;
|
use stardust_xr::values::ResourceID;
|
||||||
use std::ffi::OsStr;
|
use std::ffi::OsStr;
|
||||||
use std::hash::{Hash, Hasher};
|
use std::hash::{Hash, Hasher};
|
||||||
use std::sync::{Arc, LazyLock, OnceLock, Weak};
|
use std::path::PathBuf;
|
||||||
use stereokit_rust::material::Transparency;
|
use std::sync::{Arc, OnceLock, Weak};
|
||||||
use stereokit_rust::maths::Bounds;
|
use stereokit_rust::maths::Bounds;
|
||||||
use stereokit_rust::sk::MainThreadToken;
|
use tokio::sync::mpsc;
|
||||||
use stereokit_rust::{material::Material, model::Model as SKModel, tex::Tex, util::Color128};
|
|
||||||
|
|
||||||
pub struct MaterialWrapper(pub Material);
|
static LOAD_MODEL: OnceLock<mpsc::UnboundedSender<(Arc<Model>, PathBuf)>> = OnceLock::new();
|
||||||
impl Drop for MaterialWrapper {
|
|
||||||
fn drop(&mut self) {
|
pub struct ModelNodePlugin;
|
||||||
MATERIAL_REGISTRY.remove(self);
|
impl Plugin for ModelNodePlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
let (tx, rx) = mpsc::unbounded_channel();
|
||||||
|
LOAD_MODEL.set(tx).unwrap();
|
||||||
|
app.insert_resource(MpscReceiver(rx));
|
||||||
|
app.add_systems(Update, load_models);
|
||||||
|
app.add_systems(PostUpdate, (gen_model_parts, apply_materials).chain());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Hash for MaterialWrapper {
|
#[derive(Resource)]
|
||||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
struct MpscReceiver<T>(mpsc::UnboundedReceiver<T>);
|
||||||
self.0.get_shader().0.as_ptr().hash(state);
|
#[derive(Component)]
|
||||||
for param in self.0.get_all_param_info() {
|
struct ModelNode(Weak<Model>);
|
||||||
param.name.hash(state);
|
|
||||||
param.to_string().hash(state);
|
|
||||||
}
|
|
||||||
self.0.get_chain().map(MaterialWrapper).hash(state)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl PartialEq for MaterialWrapper {
|
|
||||||
fn eq(&self, other: &Self) -> bool {
|
|
||||||
if self.0.get_shader().0.as_ptr() != other.0.get_shader().0.as_ptr() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if self.0.get_all_param_info().count() != other.0.get_all_param_info().count() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
for self_param in self.0.get_all_param_info() {
|
|
||||||
let Some(other_param) = other
|
|
||||||
.0
|
|
||||||
.get_all_param_info()
|
|
||||||
.get_data(self_param.get_name(), self_param.get_type())
|
|
||||||
else {
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
if self_param.to_string() != other_param.to_string() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.0.get_chain().map(MaterialWrapper) == other.0.get_chain().map(MaterialWrapper)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl Eq for MaterialWrapper {}
|
|
||||||
unsafe impl Send for MaterialWrapper {}
|
|
||||||
unsafe impl Sync for MaterialWrapper {}
|
|
||||||
|
|
||||||
#[derive(Default)]
|
fn load_models(
|
||||||
struct MaterialRegistry(Mutex<FxHashMap<u64, Weak<MaterialWrapper>>>);
|
asset_server: Res<AssetServer>,
|
||||||
impl MaterialRegistry {
|
mut cmds: Commands,
|
||||||
fn add_or_get(&self, material: Arc<MaterialWrapper>) -> Arc<MaterialWrapper> {
|
mut mpsc_receiver: ResMut<MpscReceiver<(Arc<Model>, PathBuf)>>,
|
||||||
let hash = {
|
) {
|
||||||
use std::hash::{Hash, Hasher};
|
while let Ok((model, path)) = mpsc_receiver.0.try_recv() {
|
||||||
let mut hasher = std::collections::hash_map::DefaultHasher::new();
|
// idk of the asset label is the correct approach here
|
||||||
material.hash(&mut hasher);
|
let handle = asset_server.load(GltfAssetLabel::Scene(0).from_asset(path));
|
||||||
hasher.finish()
|
let entity = cmds
|
||||||
|
.spawn((
|
||||||
|
SceneRoot(handle),
|
||||||
|
ModelNode(Arc::downgrade(&model)),
|
||||||
|
SpatialNode(Arc::downgrade(&model.space)),
|
||||||
|
))
|
||||||
|
.id();
|
||||||
|
model.bevy_scene_entity.set(entity).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_materials(
|
||||||
|
mut query: Query<&mut MeshMaterial3d<PbrMaterial>>,
|
||||||
|
mut material_registry: Local<MaterialRegistry>,
|
||||||
|
asset_server: Res<AssetServer>,
|
||||||
|
mut materials: ResMut<Assets<PbrMaterial>>,
|
||||||
|
) -> bevy::prelude::Result {
|
||||||
|
for model_part in MODEL_REGISTRY
|
||||||
|
.get_valid_contents()
|
||||||
|
.iter()
|
||||||
|
.filter_map(|p| p.parts.get())
|
||||||
|
.flatten()
|
||||||
|
{
|
||||||
|
let mut mesh_mat = query.get_mut(*model_part.mesh_entity.get().unwrap())?;
|
||||||
|
if let Some(material) = model_part.pending_material_replacement.lock().take() {
|
||||||
|
let pbr_mat = material.to_pbr_mat(&asset_server);
|
||||||
|
let handle = material_registry.get_handle(pbr_mat, &mut materials);
|
||||||
|
mesh_mat.0 = handle;
|
||||||
|
}
|
||||||
|
for (param_name, param) in model_part.pending_material_parameters.lock().drain() {
|
||||||
|
let mut new_mat = materials.get(&mesh_mat.0).unwrap().clone();
|
||||||
|
param.apply_to_material(
|
||||||
|
&model_part.space.node().unwrap().get_client().unwrap(),
|
||||||
|
&mut new_mat,
|
||||||
|
¶m_name,
|
||||||
|
&asset_server,
|
||||||
|
);
|
||||||
|
let handle = material_registry.get_handle(new_mat, &mut materials);
|
||||||
|
mesh_mat.0 = handle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn gen_model_parts(
|
||||||
|
scenes: Res<Assets<Scene>>,
|
||||||
|
query: Query<(Entity, &SceneRoot, &ModelNode, &Children)>,
|
||||||
|
children_query: Query<&Children>,
|
||||||
|
part_query: Query<(&Name, Option<&Children>, &Transform), Without<Mesh3d>>,
|
||||||
|
has_mesh: Query<Has<Mesh3d>>,
|
||||||
|
mut cmds: Commands,
|
||||||
|
) {
|
||||||
|
for (entity, scene_root, model_node, model_children) in query.iter() {
|
||||||
|
let Some(model) = model_node.0.upgrade() else {
|
||||||
|
cmds.entity(entity).despawn();
|
||||||
|
return;
|
||||||
};
|
};
|
||||||
|
if model.parts.get().is_some() {
|
||||||
let mut lock = self.0.lock();
|
continue;
|
||||||
if let Some(mat) = lock.get(&hash) {
|
|
||||||
if let Some(mat) = mat.upgrade() {
|
|
||||||
return mat;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
if scenes.get(scene_root.0.id()).is_none() {
|
||||||
lock.insert(hash, Arc::downgrade(&material));
|
continue;
|
||||||
material
|
}
|
||||||
}
|
let mut parts = Vec::new();
|
||||||
fn remove(&self, material: &MaterialWrapper) {
|
for entity in model_children
|
||||||
let hash = {
|
.iter()
|
||||||
use std::hash::{Hash, Hasher};
|
.filter_map(|e| children_query.get(e).ok())
|
||||||
let mut hasher = std::collections::hash_map::DefaultHasher::new();
|
.flat_map(|c| c.iter())
|
||||||
material.hash(&mut hasher);
|
{
|
||||||
hasher.finish()
|
gen_path(
|
||||||
};
|
entity,
|
||||||
let mut lock = self.0.lock();
|
&part_query,
|
||||||
lock.remove(&hash);
|
None,
|
||||||
|
&mut |entity, name, transform, parent| {
|
||||||
|
let path = parent
|
||||||
|
.as_ref()
|
||||||
|
.map(|p| format!("{}/{}", &p.path, name.as_str()))
|
||||||
|
.unwrap_or_else(|| name.to_string());
|
||||||
|
let parent_spatial = parent
|
||||||
|
.as_ref()
|
||||||
|
.map(|p| p.space.clone())
|
||||||
|
.unwrap_or_else(|| model.space.clone());
|
||||||
|
let client = model.space.node()?.get_client()?;
|
||||||
|
let (spatial, model_part) =
|
||||||
|
match model.pre_bound_parts.lock().iter().find(|v| v.path == path) {
|
||||||
|
None => {
|
||||||
|
let node =
|
||||||
|
client.scenegraph.add_node(Node::generate(&client, false));
|
||||||
|
let spatial = Spatial::add_to(
|
||||||
|
&node,
|
||||||
|
Some(parent_spatial),
|
||||||
|
transform.compute_matrix(),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
let model_part = node.add_aspect(ModelPart {
|
||||||
|
entity: OnceLock::new(),
|
||||||
|
mesh_entity: OnceLock::new(),
|
||||||
|
path,
|
||||||
|
space: spatial.clone(),
|
||||||
|
model: Arc::downgrade(&model),
|
||||||
|
pending_material_parameters: Mutex::default(),
|
||||||
|
pending_material_replacement: Mutex::default(),
|
||||||
|
aliases: AliasList::default(),
|
||||||
|
});
|
||||||
|
(spatial, model_part)
|
||||||
|
}
|
||||||
|
Some(part) => {
|
||||||
|
part.space
|
||||||
|
.set_spatial_parent(Some(&parent_spatial))
|
||||||
|
.unwrap();
|
||||||
|
(part.space.clone(), part.clone())
|
||||||
|
}
|
||||||
|
};
|
||||||
|
_ = spatial.bounding_box_calc.set(|_| {
|
||||||
|
// TODO: actually impl aabb
|
||||||
|
Bounds::default()
|
||||||
|
});
|
||||||
|
cmds.entity(entity)
|
||||||
|
.insert(SpatialNode(Arc::downgrade(&spatial)));
|
||||||
|
let mesh_entity = children_query
|
||||||
|
.get(entity)
|
||||||
|
.iter()
|
||||||
|
.flat_map(|v| v.iter())
|
||||||
|
.find(|e| has_mesh.get(*e).unwrap_or(false))?;
|
||||||
|
_ = model_part.entity.set(entity);
|
||||||
|
_ = model_part.mesh_entity.set(mesh_entity);
|
||||||
|
parts.push(model_part.clone());
|
||||||
|
Some(model_part)
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
_ = model.parts.set(parts);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static MATERIAL_REGISTRY: LazyLock<MaterialRegistry> = LazyLock::new(MaterialRegistry::default);
|
fn gen_path(
|
||||||
|
current_entity: Entity,
|
||||||
|
part_query: &Query<(&Name, Option<&Children>, &Transform), Without<Mesh3d>>,
|
||||||
|
parent: Option<Arc<ModelPart>>,
|
||||||
|
func: &mut dyn FnMut(
|
||||||
|
Entity,
|
||||||
|
&Name,
|
||||||
|
&Transform,
|
||||||
|
Option<Arc<ModelPart>>,
|
||||||
|
) -> Option<Arc<ModelPart>>,
|
||||||
|
) {
|
||||||
|
let Ok((name, children, transform)) = part_query.get(current_entity) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(parent) = func(current_entity, name, transform, parent) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
for e in children.iter().flat_map(|c| c.iter()) {
|
||||||
|
gen_path(e, part_query, Some(parent.clone()), func);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Deref, DerefMut, Clone, Copy, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
struct HashedPbrMaterial(u64);
|
||||||
|
impl HashedPbrMaterial {
|
||||||
|
fn new(material: &PbrMaterial) -> Self {
|
||||||
|
let mut hasher = FxHasher::default();
|
||||||
|
Self::hash_pbr_mat(material, &mut hasher);
|
||||||
|
Self(hasher.finish())
|
||||||
|
}
|
||||||
|
fn hash_pbr_mat<H: Hasher>(mat: &PbrMaterial, state: &mut H) {
|
||||||
|
hash_color(mat.color, state);
|
||||||
|
hash_color(mat.emission_factor, state);
|
||||||
|
state.write_u32(mat.metallic.to_bits());
|
||||||
|
state.write_u32(mat.roughness.to_bits());
|
||||||
|
mat.use_stereokit_uvs.hash(state);
|
||||||
|
match mat.alpha_mode {
|
||||||
|
AlphaMode::Opaque => state.write_u8(0),
|
||||||
|
AlphaMode::Mask(v) => {
|
||||||
|
state.write_u8(1);
|
||||||
|
state.write_u32(v.to_bits());
|
||||||
|
}
|
||||||
|
AlphaMode::Blend => state.write_u8(2),
|
||||||
|
AlphaMode::Premultiplied => state.write_u8(3),
|
||||||
|
AlphaMode::AlphaToCoverage => state.write_u8(4),
|
||||||
|
AlphaMode::Add => state.write_u8(5),
|
||||||
|
AlphaMode::Multiply => state.write_u8(6),
|
||||||
|
}
|
||||||
|
state.write_u8(mat.double_sided as u8);
|
||||||
|
mat.diffuse_texture.hash(state);
|
||||||
|
mat.emission_texture.hash(state);
|
||||||
|
mat.metal_texture.hash(state);
|
||||||
|
mat.occlusion_texture.hash(state);
|
||||||
|
// should always be the same, TODO: make the spherical harmonics buffer a per mesh instance thing
|
||||||
|
mat.spherical_harmonics.hash(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn hash_color<H: Hasher>(color: Color, state: &mut H) {
|
||||||
|
match color {
|
||||||
|
Color::Srgba(srgba) => {
|
||||||
|
state.write_u8(0);
|
||||||
|
state.write(&srgba.to_u8_array());
|
||||||
|
}
|
||||||
|
Color::LinearRgba(linear_rgba) => {
|
||||||
|
state.write_u8(1);
|
||||||
|
state.write(&linear_rgba.to_u8_array());
|
||||||
|
}
|
||||||
|
Color::Hsla(hsla) => {
|
||||||
|
state.write_u8(2);
|
||||||
|
hsla.to_f32_array()
|
||||||
|
.iter()
|
||||||
|
.for_each(|v| state.write_u32(v.to_bits()));
|
||||||
|
}
|
||||||
|
Color::Hsva(hsva) => {
|
||||||
|
state.write_u8(3);
|
||||||
|
hsva.to_f32_array()
|
||||||
|
.iter()
|
||||||
|
.for_each(|v| state.write_u32(v.to_bits()));
|
||||||
|
}
|
||||||
|
Color::Hwba(hwba) => {
|
||||||
|
state.write_u8(4);
|
||||||
|
hwba.to_f32_array()
|
||||||
|
.iter()
|
||||||
|
.for_each(|v| state.write_u32(v.to_bits()));
|
||||||
|
}
|
||||||
|
Color::Laba(laba) => {
|
||||||
|
state.write_u8(5);
|
||||||
|
laba.to_f32_array()
|
||||||
|
.iter()
|
||||||
|
.for_each(|v| state.write_u32(v.to_bits()));
|
||||||
|
}
|
||||||
|
Color::Lcha(lcha) => {
|
||||||
|
state.write_u8(6);
|
||||||
|
lcha.to_f32_array()
|
||||||
|
.iter()
|
||||||
|
.for_each(|v| state.write_u32(v.to_bits()));
|
||||||
|
}
|
||||||
|
Color::Oklaba(oklaba) => {
|
||||||
|
state.write_u8(7);
|
||||||
|
oklaba
|
||||||
|
.to_f32_array()
|
||||||
|
.iter()
|
||||||
|
.for_each(|v| state.write_u32(v.to_bits()));
|
||||||
|
}
|
||||||
|
Color::Oklcha(oklcha) => {
|
||||||
|
state.write_u8(8);
|
||||||
|
oklcha
|
||||||
|
.to_f32_array()
|
||||||
|
.iter()
|
||||||
|
.for_each(|v| state.write_u32(v.to_bits()));
|
||||||
|
}
|
||||||
|
Color::Xyza(xyza) => {
|
||||||
|
state.write_u8(9);
|
||||||
|
xyza.to_f32_array()
|
||||||
|
.iter()
|
||||||
|
.for_each(|v| state.write_u32(v.to_bits()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
static MODEL_REGISTRY: Registry<Model> = Registry::new();
|
static MODEL_REGISTRY: Registry<Model> = Registry::new();
|
||||||
static HOLDOUT_MATERIAL: OnceLock<Arc<MaterialWrapper>> = OnceLock::new();
|
|
||||||
|
|
||||||
impl MaterialParameter {
|
impl MaterialParameter {
|
||||||
fn apply_to_material(&self, client: &Client, material: &Material, parameter_name: &str) {
|
fn apply_to_material(
|
||||||
let mut params = material.get_all_param_info();
|
&self,
|
||||||
|
client: &Client,
|
||||||
|
mat: &mut PbrMaterial,
|
||||||
|
parameter_name: &str,
|
||||||
|
asset_server: &AssetServer,
|
||||||
|
) {
|
||||||
match self {
|
match self {
|
||||||
MaterialParameter::Bool(val) => {
|
MaterialParameter::Bool(val) => match parameter_name {
|
||||||
params.set_bool(parameter_name, *val);
|
"double_sided" => mat.double_sided = *val,
|
||||||
|
v => {
|
||||||
|
error!("unknown param_name ({v}) for color")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
MaterialParameter::Int(_val) => {
|
||||||
|
// nothing uses an int
|
||||||
}
|
}
|
||||||
MaterialParameter::Int(val) => {
|
MaterialParameter::UInt(_val) => {
|
||||||
params.set_int(parameter_name, &[*val]);
|
// nothing uses an uint
|
||||||
}
|
|
||||||
MaterialParameter::UInt(val) => {
|
|
||||||
params.set_uint(parameter_name, &[*val]);
|
|
||||||
}
|
}
|
||||||
MaterialParameter::Float(val) => {
|
MaterialParameter::Float(val) => {
|
||||||
params.set_float(parameter_name, *val);
|
match parameter_name {
|
||||||
|
"metallic" => mat.metallic = *val,
|
||||||
|
"roughness" => mat.roughness = *val,
|
||||||
|
// we probably don't want to expose tex_scale
|
||||||
|
// "tex_scale" => mat.tex_scale = *val,
|
||||||
|
v => {
|
||||||
|
error!("unknown param_name ({v}) for float")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
MaterialParameter::Vec2(val) => {
|
MaterialParameter::Vec2(_val) => {
|
||||||
params.set_vec2(parameter_name, Vec2::from(*val));
|
// nothing uses a Vec2
|
||||||
}
|
}
|
||||||
MaterialParameter::Vec3(val) => {
|
MaterialParameter::Vec3(_val) => {
|
||||||
params.set_vec3(parameter_name, Vec3::from(*val));
|
// nothing uses a Vec3
|
||||||
}
|
}
|
||||||
MaterialParameter::Color(val) => {
|
MaterialParameter::Color(val) => {
|
||||||
params.set_color(
|
let rgba = Srgba::new(val.c.r, val.c.g, val.c.b, val.a);
|
||||||
parameter_name,
|
match parameter_name {
|
||||||
Color128::new(val.c.r, val.c.g, val.c.b, val.a),
|
"color" => mat.color = rgba.into(),
|
||||||
);
|
"emission_factor" => mat.emission_factor = rgba.into(),
|
||||||
|
v => {
|
||||||
|
error!("unknown param_name ({v}) for color")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
MaterialParameter::Texture(resource) => {
|
MaterialParameter::Texture(resource) => {
|
||||||
let Some(texture_path) =
|
let Some(texture_path) =
|
||||||
@@ -135,180 +357,103 @@ impl MaterialParameter {
|
|||||||
else {
|
else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
if let Ok(tex) = Tex::from_file(texture_path, true, None) {
|
info!(texture_param = parameter_name, path = ?texture_path);
|
||||||
params.set_texture(parameter_name, &tex);
|
let handle = asset_server.load(texture_path);
|
||||||
|
match parameter_name {
|
||||||
|
"diffuse" => mat.diffuse_texture = Some(handle),
|
||||||
|
"emission" => mat.emission_texture = Some(handle),
|
||||||
|
"metal" => mat.metal_texture = Some(handle),
|
||||||
|
"occlusion" => mat.occlusion_texture = Some(handle),
|
||||||
|
v => {
|
||||||
|
error!("unknown param_name ({v}) for texture");
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
mat.alpha_mode = AlphaMode::AlphaToCoverage;
|
||||||
|
mat.use_stereokit_uvs = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct Material {
|
||||||
|
pub color: Color,
|
||||||
|
pub emission_factor: Color,
|
||||||
|
pub metallic: f32,
|
||||||
|
pub roughness: f32,
|
||||||
|
pub alpha_mode: AlphaMode,
|
||||||
|
pub double_sided: bool,
|
||||||
|
|
||||||
|
pub diffuse_texture: Option<PathBuf>,
|
||||||
|
pub emission_texture: Option<PathBuf>,
|
||||||
|
pub metal_texture: Option<PathBuf>,
|
||||||
|
pub occlusion_texture: Option<PathBuf>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Material {
|
||||||
|
fn to_pbr_mat(&self, asset_server: &AssetServer) -> PbrMaterial {
|
||||||
|
PbrMaterial {
|
||||||
|
color: self.color,
|
||||||
|
emission_factor: self.emission_factor,
|
||||||
|
metallic: self.metallic,
|
||||||
|
roughness: self.roughness,
|
||||||
|
alpha_mode: self.alpha_mode,
|
||||||
|
double_sided: self.double_sided,
|
||||||
|
diffuse_texture: self
|
||||||
|
.diffuse_texture
|
||||||
|
.as_ref()
|
||||||
|
.map(|p| asset_server.load(p.as_path())),
|
||||||
|
emission_texture: self
|
||||||
|
.emission_texture
|
||||||
|
.as_ref()
|
||||||
|
.map(|p| asset_server.load(p.as_path())),
|
||||||
|
metal_texture: self
|
||||||
|
.metal_texture
|
||||||
|
.as_ref()
|
||||||
|
.map(|p| asset_server.load(p.as_path())),
|
||||||
|
occlusion_texture: self
|
||||||
|
.occlusion_texture
|
||||||
|
.as_ref()
|
||||||
|
.map(|p| asset_server.load(p.as_path())),
|
||||||
|
spherical_harmonics: bevy_sk::skytext::SPHERICAL_HARMONICS_HANDLE,
|
||||||
|
use_stereokit_uvs: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct ModelPart {
|
pub struct ModelPart {
|
||||||
id: i32,
|
entity: OnceLock<Entity>,
|
||||||
|
mesh_entity: OnceLock<Entity>,
|
||||||
path: String,
|
path: String,
|
||||||
space: Arc<Spatial>,
|
space: Arc<Spatial>,
|
||||||
model: Weak<Model>,
|
model: Weak<Model>,
|
||||||
material: Mutex<Option<Arc<MaterialWrapper>>>,
|
|
||||||
pending_material_parameters: Mutex<FxHashMap<String, MaterialParameter>>,
|
pending_material_parameters: Mutex<FxHashMap<String, MaterialParameter>>,
|
||||||
pending_material_replacement: Mutex<Option<Arc<MaterialWrapper>>>,
|
pending_material_replacement: Mutex<Option<Material>>,
|
||||||
aliases: AliasList,
|
aliases: AliasList,
|
||||||
}
|
}
|
||||||
impl ModelPart {
|
impl ModelPart {
|
||||||
fn create_for_model(model: &Arc<Model>, sk_model: &SKModel) {
|
pub fn replace_material(&self, replacement: Material) {
|
||||||
HOLDOUT_MATERIAL.get_or_init(|| {
|
|
||||||
let mut mat = Material::copy(&Material::unlit());
|
|
||||||
mat.transparency(Transparency::None);
|
|
||||||
mat.color_tint(Color128::BLACK_TRANSPARENT);
|
|
||||||
Arc::new(MaterialWrapper(mat))
|
|
||||||
});
|
|
||||||
|
|
||||||
let nodes = sk_model.get_nodes();
|
|
||||||
for part in nodes.all() {
|
|
||||||
ModelPart::create(model, &part);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn create(model: &Arc<Model>, part: &stereokit_rust::model::ModelNode) -> Option<Arc<Self>> {
|
|
||||||
let mut parts = model.parts.lock();
|
|
||||||
let parent_part = part
|
|
||||||
.get_parent()
|
|
||||||
.and_then(|part| parts.iter().find(|p| p.id == *part.get_id()));
|
|
||||||
|
|
||||||
let stardust_model_part = model.space.node()?;
|
|
||||||
let client = stardust_model_part.get_client()?;
|
|
||||||
let mut part_path = parent_part
|
|
||||||
.map(|n| n.path.clone() + "/")
|
|
||||||
.unwrap_or_default();
|
|
||||||
part_path += part.get_name().unwrap();
|
|
||||||
|
|
||||||
let node = client.scenegraph.add_node(Node::generate(&client, false));
|
|
||||||
let spatial_parent = parent_part
|
|
||||||
.map(|n| n.space.clone())
|
|
||||||
.unwrap_or_else(|| model.space.clone());
|
|
||||||
|
|
||||||
let local_transform = unsafe { part.get_local_transform().m };
|
|
||||||
let space = Spatial::add_to(
|
|
||||||
&node,
|
|
||||||
Some(spatial_parent),
|
|
||||||
Mat4::from_cols_array(&local_transform),
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
|
|
||||||
let _ = space.bounding_box_calc.set(|node| {
|
|
||||||
let Ok(model_part) = node.get_aspect::<ModelPart>() else {
|
|
||||||
return Bounds::default();
|
|
||||||
};
|
|
||||||
let Some(model) = model_part.model.upgrade() else {
|
|
||||||
return Bounds::default();
|
|
||||||
};
|
|
||||||
let Some(sk_model) = model.sk_model.get() else {
|
|
||||||
return Bounds::default();
|
|
||||||
};
|
|
||||||
let model_nodes = sk_model.get_nodes();
|
|
||||||
let Some(model_node) = model_nodes.get_index(model_part.id) else {
|
|
||||||
return Bounds::default();
|
|
||||||
};
|
|
||||||
let Some(sk_mesh) = model_node.get_mesh() else {
|
|
||||||
return Bounds::default();
|
|
||||||
};
|
|
||||||
sk_mesh.get_bounds()
|
|
||||||
});
|
|
||||||
|
|
||||||
let model_part = Arc::new(ModelPart {
|
|
||||||
id: *part.get_id(),
|
|
||||||
path: part_path,
|
|
||||||
space,
|
|
||||||
model: Arc::downgrade(model),
|
|
||||||
pending_material_parameters: Mutex::new(FxHashMap::default()),
|
|
||||||
pending_material_replacement: Mutex::new(None),
|
|
||||||
aliases: AliasList::default(),
|
|
||||||
material: Mutex::new(part.get_material().map(MaterialWrapper).map(Arc::new)),
|
|
||||||
});
|
|
||||||
node.add_aspect_raw(model_part.clone());
|
|
||||||
parts.push(model_part.clone());
|
|
||||||
Some(model_part)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn replace_material(&self, replacement: Arc<MaterialWrapper>) {
|
|
||||||
let shared_material = MATERIAL_REGISTRY.add_or_get(replacement);
|
|
||||||
self.pending_material_replacement
|
self.pending_material_replacement
|
||||||
.lock()
|
.lock()
|
||||||
.replace(shared_material);
|
.replace(replacement);
|
||||||
}
|
|
||||||
/// only to be run on the main thread
|
|
||||||
pub fn replace_material_now(&self, replacement: &Material) {
|
|
||||||
let Some(model) = self.model.upgrade() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let Some(sk_model) = model.sk_model.get() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let nodes = sk_model.get_nodes();
|
|
||||||
let Some(mut part) = nodes.get_index(self.id) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let shared_material =
|
|
||||||
MATERIAL_REGISTRY.add_or_get(Arc::new(MaterialWrapper(replacement.copy())));
|
|
||||||
|
|
||||||
let mut lock = self.material.lock();
|
|
||||||
part.material(&shared_material.0);
|
|
||||||
lock.replace(shared_material);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update(&self) {
|
|
||||||
let Some(model) = self.model.upgrade() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let Some(sk_model) = model.sk_model.get() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let Some(node) = model.space.node() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let nodes = sk_model.get_nodes();
|
|
||||||
let Some(mut part) = nodes.get_index(self.id) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
part.model_transform(Spatial::space_to_space_matrix(
|
|
||||||
Some(&self.space),
|
|
||||||
Some(&model.space),
|
|
||||||
));
|
|
||||||
|
|
||||||
let Some(client) = node.get_client() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(material_replacement) = self.pending_material_replacement.lock().take() {
|
|
||||||
let mut lock = self.material.lock();
|
|
||||||
part.material(&material_replacement.0);
|
|
||||||
lock.replace(material_replacement);
|
|
||||||
}
|
|
||||||
|
|
||||||
'mat_params: {
|
|
||||||
let mut material_parameters = self.pending_material_parameters.lock();
|
|
||||||
if !material_parameters.is_empty() {
|
|
||||||
let Some(material) = part.get_material() else {
|
|
||||||
break 'mat_params;
|
|
||||||
};
|
|
||||||
let new_material = material.copy();
|
|
||||||
for (parameter_name, parameter_value) in material_parameters.drain() {
|
|
||||||
parameter_value.apply_to_material(&client, &new_material, ¶meter_name);
|
|
||||||
}
|
|
||||||
|
|
||||||
let shared_material =
|
|
||||||
MATERIAL_REGISTRY.add_or_get(Arc::new(MaterialWrapper(new_material)));
|
|
||||||
let mut lock = self.material.lock();
|
|
||||||
part.material(&shared_material.0);
|
|
||||||
lock.replace(shared_material);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl ModelPartAspect for ModelPart {
|
impl ModelPartAspect for ModelPart {
|
||||||
#[doc = "Set this model part's material to one that cuts a hole in the world. Often used for overlays/passthrough where you want to show the background through an object."]
|
#[doc = "Set this model part's material to one that cuts a hole in the world. Often used for overlays/passthrough where you want to show the background through an object."]
|
||||||
fn apply_holdout_material(node: Arc<Node>, _calling_client: Arc<Client>) -> Result<()> {
|
fn apply_holdout_material(node: Arc<Node>, _calling_client: Arc<Client>) -> Result<()> {
|
||||||
let model_part = node.get_aspect::<ModelPart>()?;
|
let model_part = node.get_aspect::<ModelPart>()?;
|
||||||
model_part.replace_material(HOLDOUT_MATERIAL.get().unwrap().clone());
|
model_part.replace_material(Material {
|
||||||
|
color: Color::BLACK.with_alpha(0.0),
|
||||||
|
emission_factor: Color::BLACK,
|
||||||
|
metallic: 0.0,
|
||||||
|
roughness: 1.0,
|
||||||
|
alpha_mode: AlphaMode::Opaque,
|
||||||
|
double_sided: false,
|
||||||
|
diffuse_texture: None,
|
||||||
|
emission_texture: None,
|
||||||
|
metal_texture: None,
|
||||||
|
occlusion_texture: None,
|
||||||
|
});
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -328,12 +473,37 @@ impl ModelPartAspect for ModelPart {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#[derive(Default)]
|
||||||
|
struct MaterialRegistry(FxHashMap<HashedPbrMaterial, Handle<PbrMaterial>>);
|
||||||
|
impl MaterialRegistry {
|
||||||
|
/// returns strong handle for PbrMaterial elminitating duplications
|
||||||
|
fn get_handle(
|
||||||
|
&mut self,
|
||||||
|
material: PbrMaterial,
|
||||||
|
materials: &mut ResMut<Assets<PbrMaterial>>,
|
||||||
|
) -> Handle<PbrMaterial> {
|
||||||
|
let hash = HashedPbrMaterial::new(&material);
|
||||||
|
match self
|
||||||
|
.0
|
||||||
|
.get(&hash)
|
||||||
|
.and_then(|v| materials.get_strong_handle(v.id()))
|
||||||
|
{
|
||||||
|
Some(v) => v,
|
||||||
|
None => {
|
||||||
|
let handle = materials.add(material);
|
||||||
|
self.0.insert(hash, handle.clone_weak());
|
||||||
|
handle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct Model {
|
pub struct Model {
|
||||||
space: Arc<Spatial>,
|
space: Arc<Spatial>,
|
||||||
_resource_id: ResourceID,
|
_resource_id: ResourceID,
|
||||||
sk_model: OnceLock<SKModel>,
|
bevy_scene_entity: OnceLock<Entity>,
|
||||||
parts: Mutex<Vec<Arc<ModelPart>>>,
|
parts: OnceLock<Vec<Arc<ModelPart>>>,
|
||||||
|
pre_bound_parts: Mutex<Vec<Arc<ModelPart>>>,
|
||||||
}
|
}
|
||||||
impl Model {
|
impl Model {
|
||||||
pub fn add_to(node: &Arc<Node>, resource_id: ResourceID) -> Result<Arc<Model>> {
|
pub fn add_to(node: &Arc<Node>, resource_id: ResourceID) -> Result<Arc<Model>> {
|
||||||
@@ -347,42 +517,21 @@ impl Model {
|
|||||||
let model = Arc::new(Model {
|
let model = Arc::new(Model {
|
||||||
space: node.get_aspect::<Spatial>().unwrap().clone(),
|
space: node.get_aspect::<Spatial>().unwrap().clone(),
|
||||||
_resource_id: resource_id,
|
_resource_id: resource_id,
|
||||||
sk_model: OnceLock::new(),
|
bevy_scene_entity: OnceLock::new(),
|
||||||
parts: Mutex::new(Vec::default()),
|
pre_bound_parts: Mutex::default(),
|
||||||
|
parts: OnceLock::new(),
|
||||||
});
|
});
|
||||||
|
LOAD_MODEL
|
||||||
|
.get()
|
||||||
|
.unwrap()
|
||||||
|
.send((model.clone(), pending_model_path))
|
||||||
|
.unwrap();
|
||||||
MODEL_REGISTRY.add_raw(&model);
|
MODEL_REGISTRY.add_raw(&model);
|
||||||
|
|
||||||
// technically doing this in anything but the main thread isn't a good idea but dangit we need those model nodes ASAP
|
|
||||||
let sk_model = SKModel::copy(SKModel::from_file(
|
|
||||||
pending_model_path.to_str().unwrap(),
|
|
||||||
None,
|
|
||||||
)?);
|
|
||||||
ModelPart::create_for_model(&model, &sk_model);
|
|
||||||
let _ = model.sk_model.set(sk_model);
|
|
||||||
node.add_aspect_raw(model.clone());
|
node.add_aspect_raw(model.clone());
|
||||||
Ok(model)
|
Ok(model)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw(&self, token: &MainThreadToken) {
|
|
||||||
let Some(sk_model) = self.sk_model.get() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let parts = self.parts.lock();
|
|
||||||
for model_node in &*parts {
|
|
||||||
model_node.update();
|
|
||||||
}
|
|
||||||
drop(parts);
|
|
||||||
|
|
||||||
if let Some(node) = self.space.node() {
|
|
||||||
if node.enabled() {
|
|
||||||
sk_model.draw(token, self.space.global_transform(), None, None);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// TODO: proper hread safety in stereokit_rust (probably just bind stereokit directly)
|
|
||||||
unsafe impl Send for Model {}
|
|
||||||
unsafe impl Sync for Model {}
|
|
||||||
impl ModelAspect for Model {
|
impl ModelAspect for Model {
|
||||||
#[doc = "Bind a model part to the node with the ID input."]
|
#[doc = "Bind a model part to the node with the ID input."]
|
||||||
fn bind_model_part(
|
fn bind_model_part(
|
||||||
@@ -392,10 +541,44 @@ impl ModelAspect for Model {
|
|||||||
part_path: String,
|
part_path: String,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let model = node.get_aspect::<Model>()?;
|
let model = node.get_aspect::<Model>()?;
|
||||||
let parts = model.parts.lock();
|
let part = match model
|
||||||
let Some(part) = parts.iter().find(|p| p.path == part_path) else {
|
.parts
|
||||||
let paths = parts.iter().map(|p| &p.path).collect::<Vec<_>>();
|
.get()
|
||||||
bail!("Couldn't find model part at path {part_path}, all available paths: {paths:?}",);
|
.map(|v| v.iter().find(|p| p.path == part_path))
|
||||||
|
{
|
||||||
|
Some(Some(part)) => part.clone(),
|
||||||
|
Some(None) => {
|
||||||
|
let paths = model
|
||||||
|
.parts
|
||||||
|
.get()
|
||||||
|
.unwrap()
|
||||||
|
.iter()
|
||||||
|
.map(|p| &p.path)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
bail!(
|
||||||
|
"Couldn't find model part at path {part_path}, all available paths: {paths:?}",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
let part_node = calling_client
|
||||||
|
.scenegraph
|
||||||
|
.add_node(Node::generate(&calling_client, false));
|
||||||
|
let model = node.get_aspect::<Model>()?;
|
||||||
|
let spatial =
|
||||||
|
Spatial::add_to(&part_node, Some(model.space.clone()), Mat4::IDENTITY, false);
|
||||||
|
let part = part_node.add_aspect(ModelPart {
|
||||||
|
entity: OnceLock::new(),
|
||||||
|
mesh_entity: OnceLock::new(),
|
||||||
|
path: part_path,
|
||||||
|
space: spatial,
|
||||||
|
model: Arc::downgrade(&model),
|
||||||
|
pending_material_parameters: Mutex::default(),
|
||||||
|
pending_material_replacement: Mutex::default(),
|
||||||
|
aliases: AliasList::default(),
|
||||||
|
});
|
||||||
|
model.pre_bound_parts.lock().push(part.clone());
|
||||||
|
part
|
||||||
|
}
|
||||||
};
|
};
|
||||||
Alias::create_with_id(
|
Alias::create_with_id(
|
||||||
&part.space.node().unwrap(),
|
&part.space.node().unwrap(),
|
||||||
@@ -412,9 +595,3 @@ impl Drop for Model {
|
|||||||
MODEL_REGISTRY.remove(self);
|
MODEL_REGISTRY.remove(self);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn draw_all(token: &MainThreadToken) {
|
|
||||||
for model in MODEL_REGISTRY.get_valid_contents() {
|
|
||||||
model.draw(token);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -9,10 +9,7 @@ use crate::{
|
|||||||
core::{client::Client, registry::Registry, scenegraph::MethodResponseSender},
|
core::{client::Client, registry::Registry, scenegraph::MethodResponseSender},
|
||||||
nodes::{
|
nodes::{
|
||||||
Message, Node,
|
Message, Node,
|
||||||
drawable::{
|
drawable::{model::ModelPart, shaders::UNLIT_SHADER_BYTES},
|
||||||
model::{MaterialWrapper, ModelPart},
|
|
||||||
shaders::UNLIT_SHADER_BYTES,
|
|
||||||
},
|
|
||||||
items::TypeInfo,
|
items::TypeInfo,
|
||||||
spatial::{Spatial, Transform},
|
spatial::{Spatial, Transform},
|
||||||
},
|
},
|
||||||
@@ -68,7 +65,6 @@ pub struct CameraItem {
|
|||||||
space: Arc<Spatial>,
|
space: Arc<Spatial>,
|
||||||
frame_info: Mutex<FrameInfo>,
|
frame_info: Mutex<FrameInfo>,
|
||||||
sk_tex: OnceLock<TexWrapper>,
|
sk_tex: OnceLock<TexWrapper>,
|
||||||
sk_mat: OnceLock<Arc<MaterialWrapper>>,
|
|
||||||
applied_to: Registry<ModelPart>,
|
applied_to: Registry<ModelPart>,
|
||||||
apply_to: Registry<ModelPart>,
|
apply_to: Registry<ModelPart>,
|
||||||
}
|
}
|
||||||
@@ -82,7 +78,6 @@ impl CameraItem {
|
|||||||
px_size,
|
px_size,
|
||||||
}),
|
}),
|
||||||
sk_tex: OnceLock::new(),
|
sk_tex: OnceLock::new(),
|
||||||
sk_mat: OnceLock::new(),
|
|
||||||
applied_to: Registry::new(),
|
applied_to: Registry::new(),
|
||||||
apply_to: Registry::new(),
|
apply_to: Registry::new(),
|
||||||
});
|
});
|
||||||
@@ -139,15 +134,15 @@ impl CameraItem {
|
|||||||
TexFormat::RGBA32Linear,
|
TexFormat::RGBA32Linear,
|
||||||
))
|
))
|
||||||
});
|
});
|
||||||
let sk_mat = self.sk_mat.get_or_init(|| {
|
// let sk_mat = self.sk_mat.get_or_init(|| {
|
||||||
let shader = Shader::from_memory(UNLIT_SHADER_BYTES).unwrap();
|
// let shader = Shader::from_memory(UNLIT_SHADER_BYTES).unwrap();
|
||||||
let mut mat = Material::new(&shader, None);
|
// let mut mat = Material::new(&shader, None);
|
||||||
mat.get_all_param_info().set_texture("diffuse", &sk_tex.0);
|
// mat.get_all_param_info().set_texture("diffuse", &sk_tex.0);
|
||||||
mat.transparency(Transparency::Blend);
|
// mat.transparency(Transparency::Blend);
|
||||||
Arc::new(MaterialWrapper(mat))
|
// Arc::new(MaterialWrapper(mat))
|
||||||
});
|
// });
|
||||||
for model_part in self.apply_to.take_valid_contents() {
|
for model_part in self.apply_to.take_valid_contents() {
|
||||||
model_part.replace_material(sk_mat.clone())
|
// model_part.replace_material(sk_mat.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
if !self.applied_to.is_empty() {
|
if !self.applied_to.is_empty() {
|
||||||
|
|||||||
@@ -9,16 +9,85 @@ use crate::core::client::Client;
|
|||||||
use crate::core::error::Result;
|
use crate::core::error::Result;
|
||||||
use crate::core::registry::Registry;
|
use crate::core::registry::Registry;
|
||||||
use crate::nodes::{Node, OWNED_ASPECT_ALIAS_INFO};
|
use crate::nodes::{Node, OWNED_ASPECT_ALIAS_INFO};
|
||||||
|
use bevy::ecs::entity_disabling::Disabled;
|
||||||
|
use bevy::prelude::Transform as BevyTransform;
|
||||||
|
use bevy::prelude::*;
|
||||||
use color_eyre::eyre::OptionExt;
|
use color_eyre::eyre::OptionExt;
|
||||||
use glam::{Mat4, Quat, Vec3, vec3a};
|
use glam::{Mat4, Quat, Vec3, vec3a};
|
||||||
use mint::Vector3;
|
use mint::Vector3;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use rustc_hash::FxHashMap;
|
use rustc_hash::FxHashMap;
|
||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
|
use std::sync::atomic::Ordering;
|
||||||
use std::sync::{Arc, OnceLock, Weak};
|
use std::sync::{Arc, OnceLock, Weak};
|
||||||
use std::{f32, ptr};
|
use std::{f32, ptr};
|
||||||
use stereokit_rust::maths::Bounds;
|
use stereokit_rust::maths::Bounds;
|
||||||
|
|
||||||
|
pub struct SpatialNodePlugin;
|
||||||
|
impl Plugin for SpatialNodePlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
app.add_systems(
|
||||||
|
PostUpdate,
|
||||||
|
(replace_childof, update_spatial_nodes).before(TransformSystem::TransformPropagate),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_spatial_nodes(
|
||||||
|
mut query: Query<(Entity, &mut BevyTransform, &SpatialNode, Has<Disabled>)>,
|
||||||
|
cmds: ParallelCommands,
|
||||||
|
) {
|
||||||
|
query
|
||||||
|
.par_iter_mut()
|
||||||
|
.for_each(|(entity, mut transform, spatial_node, disabled)| {
|
||||||
|
let Some(spatial) = spatial_node.0.upgrade() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if !spatial
|
||||||
|
.node()
|
||||||
|
.map(|n| n.enabled.load(Ordering::Relaxed))
|
||||||
|
.unwrap_or(true)
|
||||||
|
{
|
||||||
|
// cmds.command_scope(|mut cmds| {
|
||||||
|
// cmds.entity(entity).insert(Visibility::Hidden);
|
||||||
|
// });
|
||||||
|
// return;
|
||||||
|
}
|
||||||
|
let mat4 = spatial.global_transform();
|
||||||
|
|
||||||
|
let scale_zero = mat4.to_scale_rotation_translation().0.length_squared() > 0.0;
|
||||||
|
if scale_zero {
|
||||||
|
// cmds.command_scope(|mut cmds| {
|
||||||
|
// cmds.entity(entity).insert(Visibility::Hidden);
|
||||||
|
// });
|
||||||
|
} else if disabled {
|
||||||
|
// cmds.command_scope(|mut cmds| {
|
||||||
|
// cmds.entity(entity).insert(Visibility::Visible);
|
||||||
|
// });
|
||||||
|
}
|
||||||
|
*transform = BevyTransform::from_matrix(mat4);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn replace_childof(query: Query<(Entity, &ChildOf), With<SpatialNode>>, mut cmds: Commands) {
|
||||||
|
for (entity, parent) in &query {
|
||||||
|
cmds.entity(entity)
|
||||||
|
.insert(NonSpatialChildOf(parent.0))
|
||||||
|
.remove::<ChildOf>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Component, Default, Debug, PartialEq, Eq)]
|
||||||
|
#[relationship_target(relationship = NonSpatialChildOf, linked_spawn)]
|
||||||
|
pub struct NonSpatialChildren(Vec<Entity>);
|
||||||
|
#[derive(Component, Debug, PartialEq, Eq)]
|
||||||
|
#[relationship(relationship_target = NonSpatialChildren)]
|
||||||
|
pub struct NonSpatialChildOf(Entity);
|
||||||
|
|
||||||
|
#[derive(Clone, Component, Debug)]
|
||||||
|
#[require(BevyTransform)]
|
||||||
|
pub struct SpatialNode(pub Weak<Spatial>);
|
||||||
|
|
||||||
stardust_xr_server_codegen::codegen_spatial_protocol!();
|
stardust_xr_server_codegen::codegen_spatial_protocol!();
|
||||||
impl Transform {
|
impl Transform {
|
||||||
pub fn to_mat4(&self, position: bool, rotation: bool, scale: bool) -> Mat4 {
|
pub fn to_mat4(&self, position: bool, rotation: bool, scale: bool) -> Mat4 {
|
||||||
|
|||||||
Reference in New Issue
Block a user