Compare commits

...

7 Commits

Author SHA1 Message Date
Jan Hohenheim
5e73d68718
Merge 4e7f1b3c81 into d08c235122 2024-08-02 23:47:26 +00:00
Jan Hohenheim
4e7f1b3c81
Fix path 2024-08-03 01:47:16 +02:00
Jan Hohenheim
3948289c36
Write about Avian 2024-08-03 01:46:25 +02:00
kaosat.dev
d08c235122 feat(examples:Save_load): created much simpler example for save load
* removed old assets & co
 * fresh blend with Blenvy logic
 * generated assets (wip)
 * started updating code (wip)
2024-08-03 01:29:34 +02:00
kaosat.dev
171ec7490a feat(Blenvy:Bevy): slowly adding back save/load (wip)
* moved out old code
 * added very basics of saving (HEAVY WIP)
2024-08-03 01:28:46 +02:00
kaosat.dev
ae9f07f549 feat(Blenvy:Blender): started upgrading code relevant to save/load, dynamic entities etc
* using some of the newer components api to determine if an object/its collection is dynamic or not
2024-08-03 01:27:44 +02:00
Jan Hohenheim
2c7d991b3c
React to feedback 2024-08-02 19:57:57 +02:00
60 changed files with 15247 additions and 1274 deletions

View File

@ -27,7 +27,7 @@ Want to jump right in? See the [quickstart guide](./docs/quickstart/readme.md) f
* automatically load all assets for each blueprint (gltf files, manually added assets), with no setup required
* hot reload of your levels & blueprints
* minimal setup & code, you can have something basic running fast
* minimal dependencies: Bevy, Serde & Ron only !
* minimal dependencies: Bevy, Serde & RON only!
* opensource
> If you were previously using the individual bevy_gltf_xxx crates & Blender add-ons please see the [migration guide](./Migration_guide.md)
@ -82,6 +82,12 @@ The workflow goes as follows (once you got your Bevy code setup)
* then add your components to objects in Blender **with a nice UI** see [here](./README-workflow-ui.md) for more details
See the [quickstart](./docs/quickstart/readme.md) for a full step-by-step guide.
## Third Party Integration
Read about the [Avian Physics Integration](docs/avian/readme) to learn how to setup colliders in Blender that will be used by the Avian physics engine in Bevy.
## Limitations / issues
* Some of `avian` or `bevy_rapier` /physics code / ways to define colliders could perhaps be done better/visually within Blender

View File

@ -10,6 +10,9 @@ pub use registry::*;
pub mod blueprints;
pub use blueprints::*;
pub mod save_load;
pub use save_load::*;
#[derive(Clone, Resource)]
pub struct BlenvyConfig {
// registry
@ -26,6 +29,8 @@ pub struct BlenvyConfig {
// save & load
pub(crate) save_component_filter: SceneFilter,
pub(crate) save_resource_filter: SceneFilter,
//pub(crate) save_path: PathBuf,
// save_path: PathBuf::from("saves"),
}
#[derive(Debug, Clone)]
@ -63,6 +68,7 @@ impl Plugin for BlenvyPlugin {
#[cfg(not(target_arch = "wasm32"))]
ExportRegistryPlugin::default(),
BlueprintsPlugin::default(),
SaveLoadPlugin::default()
))
.insert_resource(BlenvyConfig {
export_registry: self.export_registry,

View File

@ -1,108 +1,59 @@
pub mod saveable;
use std::path::PathBuf;
pub use saveable::*;
pub mod saving;
pub use saving::*;
pub mod loading;
pub use loading::*;
use bevy::core_pipeline::core_3d::{Camera3dDepthTextureUsage, ScreenSpaceTransmissionQuality};
use bevy::prelude::*;
use bevy::prelude::{App, IntoSystemConfigs, Plugin};
use blenvy::GltfBlueprintsSet;
#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)]
pub enum SavingSet {
Save,
}
#[derive(Component, Reflect, Debug, Default)]
#[reflect(Component)]
/// component used to mark any entity as Dynamic: aka add this to make sure your entity is going to be saved
pub struct Dynamic;
#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)]
pub enum LoadingSet {
Load,
}
#[derive(Component, Reflect, Debug, Default)]
#[reflect(Component)]
/// marker component for entities that do not have parents, or whose parents should be ignored when serializing
pub(crate) struct RootEntity;
// Plugin configuration
#[derive(Component, Debug)]
/// internal helper component to store parents before resetting them
pub(crate) struct OriginalParent(pub(crate) Entity);
#[derive(Clone, Resource)]
pub struct SaveLoadConfig {
pub(crate) save_path: PathBuf,
pub(crate) component_filter: SceneFilter,
pub(crate) resource_filter: SceneFilter,
}
// define the plugin
pub struct SaveLoadPlugin {
pub component_filter: SceneFilter,
pub resource_filter: SceneFilter,
pub save_path: PathBuf,
}
impl Default for SaveLoadPlugin {
fn default() -> Self {
Self {
component_filter: SceneFilter::default(),
resource_filter: SceneFilter::default(),
save_path: PathBuf::from("scenes"),
}
}
}
/// Marker component to Flag the root entity of all static entities (immutables)
#[derive(Component, Reflect, Debug, Default)]
#[reflect(Component)]
pub struct StaticEntitiesRoot;
/// Marker component to Flag the root entity of all dynamic entities (mutables)
#[derive(Component, Reflect, Debug, Default)]
#[reflect(Component)]
pub struct DynamicEntitiesRoot;
#[derive(Resource, Clone, Debug, Default, Reflect)]
#[reflect(Resource)]
pub struct StaticEntitiesBlueprintInfo {
//pub blueprint_info: BlueprintInfo,
pub path: String,
}
pub mod saving;
pub use saving::*;
#[derive(Debug, Clone, Default)]
/// Plugin for saving & loading
pub struct SaveLoadPlugin {}
impl Plugin for SaveLoadPlugin {
fn build(&self, app: &mut App) {
app.register_type::<Dynamic>()
.register_type::<StaticEntitiesRoot>()
// TODO: remove these in bevy 0.13, as these are now registered by default
.register_type::<Camera3dDepthTextureUsage>()
.register_type::<ScreenSpaceTransmissionQuality>()
.register_type::<StaticEntitiesStorage>()
.add_event::<SaveRequest>()
.add_event::<LoadRequest>()
.add_event::<LoadingFinished>()
.add_event::<SavingFinished>()
.insert_resource(SaveLoadConfig {
save_path: self.save_path.clone(),
component_filter: self.component_filter.clone(),
resource_filter: self.resource_filter.clone(),
})
.configure_sets(
Update,
(LoadingSet::Load).chain().before(GltfBlueprintsSet::Spawn), //.before(GltfComponentsSet::Injection)
)
.add_event::<SaveRequest>()
.add_event::<SaveFinished>()
.add_systems(
PreUpdate,
Update,
(prepare_save_game, apply_deferred, save_game, cleanup_save)
.chain()
.run_if(should_save),
)
.add_systems(Update, mark_load_requested)
.add_systems(
Update,
(unload_world, apply_deferred, load_game)
.chain()
.run_if(resource_exists::<LoadRequested>)
.run_if(not(resource_exists::<LoadFirstStageDone>))
.in_set(LoadingSet::Load),
)
.add_systems(
Update,
(load_static, apply_deferred, cleanup_loaded_scene)
.chain()
.run_if(resource_exists::<LoadFirstStageDone>)
// .run_if(in_state(AppState::LoadingGame))
.in_set(LoadingSet::Load),
);
;
}
}

View File

@ -0,0 +1,76 @@
pub mod saveable;
use std::path::PathBuf;
pub use saveable::*;
pub mod saving;
pub use saving::*;
pub mod loading;
pub use loading::*;
use bevy::core_pipeline::core_3d::{Camera3dDepthTextureUsage, ScreenSpaceTransmissionQuality};
use bevy::prelude::*;
use bevy::prelude::{App, IntoSystemConfigs, Plugin};
use blenvy::GltfBlueprintsSet;
#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)]
pub enum SavingSet {
Save,
}
#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)]
pub enum LoadingSet {
Load,
}
#[derive(Component, Reflect, Debug, Default)]
#[reflect(Component)]
pub struct StaticEntitiesRoot;
#[derive(Component, Reflect, Debug, Default)]
#[reflect(Component)]
pub struct DynamicEntitiesRoot;
impl Plugin for SaveLoadPlugin {
fn build(&self, app: &mut App) {
app.register_type::<Dynamic>()
.register_type::<StaticEntitiesRoot>()
// TODO: remove these in bevy 0.13, as these are now registered by default
.register_type::<Camera3dDepthTextureUsage>()
.register_type::<ScreenSpaceTransmissionQuality>()
.register_type::<StaticEntitiesStorage>()
.add_event::<SaveRequest>()
.add_event::<LoadRequest>()
.add_event::<LoadingFinished>()
.add_event::<SavingFinished>()
.configure_sets(
Update,
(LoadingSet::Load).chain().before(GltfBlueprintsSet::Spawn), //.before(GltfComponentsSet::Injection)
)
.add_systems(
PreUpdate,
(prepare_save_game, apply_deferred, save_game, cleanup_save)
.chain()
.run_if(should_save),
)
.add_systems(Update, mark_load_requested)
.add_systems(
Update,
(unload_world, apply_deferred, load_game)
.chain()
.run_if(resource_exists::<LoadRequested>)
.run_if(not(resource_exists::<LoadFirstStageDone>))
.in_set(LoadingSet::Load),
)
.add_systems(
Update,
(load_static, apply_deferred, cleanup_loaded_scene)
.chain()
.run_if(resource_exists::<LoadFirstStageDone>)
// .run_if(in_state(AppState::LoadingGame))
.in_set(LoadingSet::Load),
);
}
}

View File

@ -0,0 +1,196 @@
use bevy::prelude::*;
use bevy::tasks::IoTaskPool;
use blenvy::{BlueprintName, InBlueprint, Library, SpawnHere};
use std::fs::File;
use std::io::Write;
use std::path::Path;
use crate::{DynamicEntitiesRoot, SaveLoadConfig, StaticEntitiesRoot};
#[derive(Event, Debug)]
pub struct SaveRequest {
pub path: String,
}
#[derive(Event)]
pub struct SavingFinished;
pub fn should_save(save_requests: EventReader<SaveRequest>) -> bool {
!save_requests.is_empty()
}
#[derive(Resource, Clone, Debug, Default, Reflect)]
#[reflect(Resource)]
pub struct StaticEntitiesStorage {
pub name: String,
pub library_path: String,
}
#[derive(Component, Reflect, Debug, Default)]
#[reflect(Component)]
/// marker component for entities that do not have parents, or whose parents should be ignored when serializing
pub(crate) struct RootEntity;
#[derive(Component, Debug)]
/// internal helper component to store parents before resetting them
pub(crate) struct OriginalParent(pub(crate) Entity);
// any child of dynamic/ saveable entities that is not saveable itself should be removed from the list of children
pub(crate) fn prepare_save_game(
saveables: Query<Entity, (With<Dynamic>, With<BlueprintName>)>,
root_entities: Query<Entity, Or<(With<DynamicEntitiesRoot>, Without<Parent>)>>, // With<DynamicEntitiesRoot>
dynamic_entities: Query<(Entity, &Parent, Option<&Children>), With<Dynamic>>,
static_entities: Query<(Entity, &BlueprintName, Option<&Library>), With<StaticEntitiesRoot>>,
mut commands: Commands,
) {
for entity in saveables.iter() {
commands.entity(entity).insert(SpawnHere);
}
for (entity, parent, children) in dynamic_entities.iter() {
let parent = parent.get();
if root_entities.contains(parent) {
commands.entity(entity).insert(RootEntity);
}
if let Some(children) = children {
for sub_child in children.iter() {
if !dynamic_entities.contains(*sub_child) {
commands.entity(*sub_child).insert(OriginalParent(entity));
commands.entity(entity).remove_children(&[*sub_child]);
}
}
}
}
for (_, blueprint_name, library) in static_entities.iter() {
let library_path: String = library.map_or_else(|| "", |l| l.0.to_str().unwrap()).into();
commands.insert_resource(StaticEntitiesStorage {
name: blueprint_name.0.clone(),
library_path,
});
}
}
pub(crate) fn save_game(world: &mut World) {
info!("saving");
let mut save_path: String = "".into();
let mut events = world.resource_mut::<Events<SaveRequest>>();
for event in events.get_reader().read(&events) {
info!("SAVE EVENT !! {:?}", event);
save_path.clone_from(&event.path);
}
events.clear();
let saveable_entities: Vec<Entity> = world
.query_filtered::<Entity, (With<Dynamic>, Without<InBlueprint>, Without<RootEntity>)>()
.iter(world)
.collect();
let saveable_root_entities: Vec<Entity> = world
.query_filtered::<Entity, (With<Dynamic>, Without<InBlueprint>, With<RootEntity>)>()
.iter(world)
.collect();
info!("saveable entities {}", saveable_entities.len());
info!("saveable root entities {}", saveable_root_entities.len());
let save_load_config = world
.get_resource::<SaveLoadConfig>()
.expect("SaveLoadConfig should exist at this stage");
// we hardcode some of the always allowed types
let filter = save_load_config
.component_filter
.clone()
.allow::<Parent>()
.allow::<Children>()
.allow::<BlueprintName>()
.allow::<SpawnHere>()
.allow::<Dynamic>()
;
// for root entities, it is the same EXCEPT we make sure parents are not included
let filter_root = filter.clone().deny::<Parent>();
let filter_resources = save_load_config
.resource_filter
.clone()
.allow::<StaticEntitiesStorage>()
;
// for default stuff
let scene_builder = DynamicSceneBuilder::from_world(world)
.with_filter(filter.clone())
.with_resource_filter(filter_resources.clone());
let mut dyn_scene = scene_builder
.extract_resources()
.extract_entities(saveable_entities.clone().into_iter())
.remove_empty_entities()
.build();
// for root entities
let scene_builder_root = DynamicSceneBuilder::from_world(world)
.with_filter(filter_root.clone())
.with_resource_filter(filter_resources.clone());
// FIXME : add back
let mut dyn_scene_root = scene_builder_root
.extract_resources()
.extract_entities(
saveable_root_entities.clone().into_iter(), // .chain(static_world_markers.into_iter()),
)
.remove_empty_entities()
.build();
dyn_scene.entities.append(&mut dyn_scene_root.entities);
// dyn_scene.resources.append(&mut dyn_scene_root.resources);
let serialized_scene = dyn_scene
.serialize(&world.resource::<AppTypeRegistry>().read())
.unwrap();
let save_path = Path::new("assets")
.join(&save_load_config.save_path)
.join(Path::new(save_path.as_str())); // Path::new(&save_load_config.save_path).join(Path::new(save_path.as_str()));
info!("saving game to {:?}", save_path);
// world.send_event(SavingFinished);
#[cfg(not(target_arch = "wasm32"))]
IoTaskPool::get()
.spawn(async move {
// Write the scene RON data to file
File::create(save_path)
.and_then(|mut file| file.write(serialized_scene.as_bytes()))
.expect("Error while writing save to file");
})
.detach();
}
pub(crate) fn cleanup_save(
needs_parent_reset: Query<(Entity, &OriginalParent)>,
mut saving_finished: EventWriter<SavingFinished>,
mut commands: Commands,
) {
for (entity, original_parent) in needs_parent_reset.iter() {
commands.entity(original_parent.0).add_child(entity);
}
commands.remove_resource::<StaticEntitiesStorage>();
saving_finished.send(SavingFinished);
}
/*
pub(crate) fn cleanup_save(mut world: &mut World) {
let mut query = world.query::<(Entity, &OriginalParent)>();
for (mut entity, original_parent) in query.iter_mut(&mut world) {
let e = world.entity_mut(original_parent.0);
// .add_child(entity);
}
}*/

View File

@ -1,7 +0,0 @@
use bevy::prelude::*;
#[derive(Component, Reflect, Debug, Default)]
#[reflect(Component)]
/// component used to mark any entity as Dynamic: aka add this to make sure your entity is going to be saved
pub struct Dynamic(pub bool);

View File

@ -1,55 +1,42 @@
use bevy::prelude::*;
use bevy::tasks::IoTaskPool;
use blenvy::{BlueprintName, InBlueprint, Library, SpawnHere};
use std::fs::File;
use std::io::Write;
use std::path::Path;
use crate::{Dynamic, DynamicEntitiesRoot, SaveLoadConfig, StaticEntitiesRoot};
use bevy::render::camera::{CameraMainTextureUsages, CameraRenderGraph};
use bevy::{prelude::*, tasks::IoTaskPool};
use bevy::prelude::World;
use crate::{BlenvyConfig, BlueprintInfo, Dynamic, FromBlueprint, RootEntity, SpawnBlueprint};
use super::{DynamicEntitiesRoot, OriginalParent, StaticEntitiesRoot};
#[derive(Event, Debug)]
pub struct SaveRequest {
pub path: String,
}
#[derive(Event)]
pub struct SavingFinished;
pub struct SaveFinished; // TODO: merge the the events above
pub fn should_save(save_requests: EventReader<SaveRequest>) -> bool {
!save_requests.is_empty()
}
#[derive(Resource, Clone, Debug, Default, Reflect)]
#[reflect(Resource)]
pub struct StaticEntitiesStorage {
pub name: String,
pub library_path: String,
}
#[derive(Component, Reflect, Debug, Default)]
#[reflect(Component)]
/// marker component for entities that do not have parents, or whose parents should be ignored when serializing
pub(crate) struct RootEntity;
#[derive(Component, Debug)]
/// internal helper component to store parents before resetting them
pub(crate) struct OriginalParent(pub(crate) Entity);
// any child of dynamic/ saveable entities that is not saveable itself should be removed from the list of children
pub(crate) fn prepare_save_game(
saveables: Query<Entity, (With<Dynamic>, With<BlueprintName>)>,
saveables: Query<Entity, (With<Dynamic>, With<BlueprintInfo>)>,
root_entities: Query<Entity, Or<(With<DynamicEntitiesRoot>, Without<Parent>)>>, // With<DynamicEntitiesRoot>
dynamic_entities: Query<(Entity, &Parent, Option<&Children>), With<Dynamic>>,
static_entities: Query<(Entity, &BlueprintName, Option<&Library>), With<StaticEntitiesRoot>>,
static_entities: Query<(Entity, &BlueprintInfo), With<StaticEntitiesRoot>>,
mut commands: Commands,
) {
for entity in saveables.iter() {
commands.entity(entity).insert(SpawnHere);
commands.entity(entity).insert(SpawnBlueprint);
}
for (entity, parent, children) in dynamic_entities.iter() {
println!("prepare save game");
let parent = parent.get();
if root_entities.contains(parent) {
commands.entity(entity).insert(RootEntity);
@ -64,14 +51,17 @@ pub(crate) fn prepare_save_game(
}
}
}
for (_, blueprint_name, library) in static_entities.iter() {
/*for (_, blueprint_name) in static_entities.iter() {
let library_path: String = library.map_or_else(|| "", |l| l.0.to_str().unwrap()).into();
commands.insert_resource(StaticEntitiesStorage {
name: blueprint_name.0.clone(),
library_path,
});
}
}*/
}
pub(crate) fn save_game(world: &mut World) {
info!("saving");
@ -85,39 +75,49 @@ pub(crate) fn save_game(world: &mut World) {
events.clear();
let saveable_entities: Vec<Entity> = world
.query_filtered::<Entity, (With<Dynamic>, Without<InBlueprint>, Without<RootEntity>)>()
// .query_filtered::<Entity, (With<Dynamic>, Without<FromBlueprint>, Without<RootEntity>)>()
.query_filtered::<Entity, (With<Dynamic>, Without<RootEntity>)>()
.iter(world)
.collect();
let saveable_root_entities: Vec<Entity> = world
.query_filtered::<Entity, (With<Dynamic>, Without<InBlueprint>, With<RootEntity>)>()
.query_filtered::<Entity, (With<Dynamic>, With<RootEntity>)>()
//.query_filtered::<Entity, (With<Dynamic>, Without<FromBlueprint>, With<RootEntity>)>()
.iter(world)
.collect();
info!("saveable entities {}", saveable_entities.len());
info!("saveable root entities {}", saveable_root_entities.len());
let save_load_config = world
.get_resource::<SaveLoadConfig>()
.expect("SaveLoadConfig should exist at this stage");
let config = world
.get_resource::<BlenvyConfig>()
.expect("Blenvy configuration should exist at this stage");
// we hardcode some of the always allowed types
let filter = save_load_config
.component_filter
let filter = config
.save_component_filter
.clone()
.allow::<Parent>()
.allow::<Children>()
.allow::<BlueprintName>()
.allow::<SpawnHere>()
.allow::<Dynamic>();
.allow::<BlueprintInfo>()
.allow::<SpawnBlueprint>()
.allow::<Dynamic>()
/*.deny::<CameraRenderGraph>()
.deny::<CameraMainTextureUsages>()
.deny::<Handle<Mesh>>()
.deny::<Handle<StandardMaterial>>() */
;
// for root entities, it is the same EXCEPT we make sure parents are not included
let filter_root = filter.clone().deny::<Parent>();
let filter_resources = save_load_config
.resource_filter
.clone()
.allow::<StaticEntitiesStorage>();
let filter_resources = config.clone()
.save_resource_filter
.deny::<Time<Real>>()
.clone();
//.allow::<StaticEntitiesStorage>();
// for default stuff
let scene_builder = DynamicSceneBuilder::from_world(world)
@ -143,15 +143,15 @@ pub(crate) fn save_game(world: &mut World) {
.remove_empty_entities()
.build();
dyn_scene.entities.append(&mut dyn_scene_root.entities);
// dyn_scene.entities.append(&mut dyn_scene_root.entities);
// dyn_scene.resources.append(&mut dyn_scene_root.resources);
let serialized_scene = dyn_scene
.serialize(&world.resource::<AppTypeRegistry>().read())
.unwrap();
.expect("filtered scene should serialize correctly");
let save_path = Path::new("assets")
.join(&save_load_config.save_path)
//.join(&config.save_path)
.join(Path::new(save_path.as_str())); // Path::new(&save_load_config.save_path).join(Path::new(save_path.as_str()));
info!("saving game to {:?}", save_path);
@ -170,21 +170,12 @@ pub(crate) fn save_game(world: &mut World) {
pub(crate) fn cleanup_save(
needs_parent_reset: Query<(Entity, &OriginalParent)>,
mut saving_finished: EventWriter<SavingFinished>,
mut saving_finished: EventWriter<SaveFinished>,
mut commands: Commands,
) {
for (entity, original_parent) in needs_parent_reset.iter() {
commands.entity(original_parent.0).add_child(entity);
}
commands.remove_resource::<StaticEntitiesStorage>();
saving_finished.send(SavingFinished);
// commands.remove_resource::<StaticEntitiesStorage>();
saving_finished.send(SaveFinished);
}
/*
pub(crate) fn cleanup_save(mut world: &mut World) {
let mut query = world.query::<(Entity, &OriginalParent)>();
for (mut entity, original_parent) in query.iter_mut(&mut world) {
let e = world.entity_mut(original_parent.0);
// .add_child(entity);
}
}*/

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 842 KiB

BIN
docs/avian/img/cylinder.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 459 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 362 KiB

BIN
docs/avian/img/data.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 284 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 KiB

BIN
docs/avian/img/dynamic.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1004 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 404 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 796 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

BIN
docs/avian/img/falling.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1020 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

BIN
docs/avian/img/hiding.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

BIN
docs/avian/img/torus.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 806 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 413 KiB

408
docs/avian/readme.md Normal file
View File

@ -0,0 +1,408 @@
# Avian Physics Integration
This guide assumes that you have a basic Blenvy setup ready to tinker in.
If you don't have that yet, please refer to the [quickstart](../quickstart/readme.md) guide.
## Add Avian to Bevy
No big surprises here. Simply add `avian3d` as a dependency by running the following from your project root:
```sh
cargo add avian3d
```
Then, where you add plugins to your Bevy app, add the `PhysicsPlugins::default()`.
The most basic `main.rs` that contains a full setup looks like this:
```rust
use avian3d::prelude::*;
use bevy::prelude::*;
use blenvy::*;
fn main() -> AppExit {
App::new()
.add_plugins((
DefaultPlugins,
BlenvyPlugin::default(),
PhysicsPlugins::default(),
))
.add_systems(Startup, setup)
.run()
}
fn setup(mut commands: Commands) {
commands.spawn((
BlueprintInfo::from_path("levels/World.glb"),
SpawnBlueprint,
HideUntilReady,
GameWorldTag,
));
}
```
Run this once with `cargo.run` to generate a `registry.json` that contains the Avian components.
## Prepare the Blueprints
Set up your `World` and `Library` scenes in Blender. Switch to the `Library` scene.
If you're coming from the [quickstart](../quickstart/readme.md) guide, you may now delete the `Player` collection by
right-clicking it in the outliner and selecting `Delete Hierarchy` as we don't need it in this guide.
Remember, you can find the outliner all the way to the right.
We will be showing different ways to add colliders, so we need to add a blueprint for each approach.
Create three new collections in the outliner by doing `rightclick` -> `New Collection` and name them as follows:
- Cube
- Board
- Cylinder
Your outliner should now look like this:
<details>
<summary>Our empty collections</summary>
<img src="img/empty_collections.png" width = 50%/>
</details>
If you accidentally created a collection as a child of another, simply drag-and-drop them around to reorder them until they look like the image above.
### Cube
Click on the `Cube` collection we just created to select it. Then, go to `Add` -> `Mesh` -> `Cube` in the upper left corner to add a cube to the collection. Leave it at the default transform.
### Board
Click on the `Board` collection. Again, go to `Add` -> `Mesh` -> `Cube`. This time, scale it until it looks like a flat board:
<details>
<summary>The cylinder in Blender</summary>
<img src="img/cylinder.png" width = 50%/>
</details>
> [!TIP]
> The above screenshot was made after disabling the visibility of the `Cube` collection by clicking the eye icon in the outliner.
>
> <details>
> <summary>Hiding objects</summary>
> <img src="img/hiding.png" width = 50%/>
> </details>
>
> Hiding other collections becomes quickly essential when working with blueprints.
The scaling we used was the following:
- X: `2.5`
- Y: `0.5`
- Z: `1.5`
### Cylinder
Finally, click on the `Cylinder` collection. Go to `Add` -> `Mesh` -> `Cylinder`. Leave it at the default transform.
You should now have three collections with different shapes in them:
<details>
<summary>Collections with objects in the outliner</summary>
<img src="img/three_object_collection.png" width = 50%/>
</details>
## Add RigidBody Components
Avian makes a distinction between a *rigid body* and its associated *colliders*.
In general, the best practice is to have a parent object be a rigid body and then have at least one descendant object be a collider.
Adding the `RigidBody` is the same for all approaches:
- select the object in the viewport
- go to the Blenvy menu's component manager. Remember, if are missing the side menu, you can open it with `N`.
- type `rigidbody` in the search bar
- select `avian3d::dynamics::rigid_body::RigidBody`
- add it
> [!TIP]
> If you do not see `avian3d::dynamics::rigid_body::RigidBody` in the list of components, make sure you have run a `cargo run` after the `PhysicsPlugins::default()` was added to your Bevy app as described above.
> If you still do not see the component, manually refresh the registry as described in the [quickstart section "Create a blueprint"](../quickstart/readme.md#create-a-blueprint).
The result should look like this:
<details>
<summary>A rigid body on the cube</summary>
<img src="img/dynamic.png" width = 50%/>
</details>
The default value for `RigidBody` is `Dynamic`, which is what we want for all three objects.
It means that they will be affected by gravity and other forces. Repeat this step for the `Board` and `Cylinder` objects.
## Add Primitive Colliders
Colliders come in two flavors: primitive and dynamic. Primitives are made up of simple shapes like cubes, spheres, and cylinders. Dynamic colliders are created at runtime from the mesh of the object they are attached to. In general, it is *way* more efficient to use primitives and placing them manually. You may think that this is a lot of work, but usually you can get away with a very rough more or less boxy shape. We will show you how this approach first.
There are three different ways to add primitive colliders to the objects, in order of increasing complexity.
### Quick and Dirty
Select the cube and search in the components for `colliderconstructor`. Select `avian3d::collision::collider::constructor::ColliderConstructor` and add it.
By default, the collider will be of the variant `Sphere`. Change it to `Cuboid`.
Since the standard cube in Blender is of size 2 m, set the `x_length`, `y_length`, and `z_length` all to `2.0`:
<details>
<summary>A collider on the cube</summary>
<img src="img/cube_primitive.png" width = 50%/>
</details>
That's already it.
> [!CAUTION]
> This method brings a major footgun: Blender uses Z-up coordinates, while Bevy uses Y-up coordinates.
> The information you enter into the `ColliderConstructor` is in Bevy's coordinate system, so don't mix them up!
### Using Empties
You'll notice that the last variant does not actually show you a preview of the collider. Let's fix that.
Click on the `Board` and then select `Add` -> `Empty` -> `Cube`.
To make its properties a bit nice to work with, go to the `Data` tab of the `Properties` window in the lower right:
<details>
<summary>Where to find the data tab</summary>
<img src="img/data.png" width = 50%/>
</details>
You'll notice that it says "Size: 1m". This is a little bit misleading, as we've seen before, since the default cube is actually 2x2x2. The "Size" actually refers to the half-extent of the cube. Set it to `0.5` to make the cube a nice 1x1x1 cube.
<details>
<summary>Where to find the data tab</summary>
<img src="img/data.png" width = 50%/>
</details>
Add a collider to this empty like you did in the ["Quick and Dirty" section](#quick-and-dirty).
Set its lengths to `1` this time.
If you have only the `Empty` set to visible and selected it, your viewport should now look as follows:
<details>
<summary>The empty with the right size and collider</summary>
<img src="img/empty_selected.png" width = 50%/>
</details>
The important bit here is that the empty's outlines perferctly match the attached collider's size.
Now, drag and drop the empty into the `Board` collection. With the empty selected, hold `CTRL` and select the `Board` object.
> [!IMPORTANT]
> It is essential that you *first* select the `Empty` and *then* select the `Board`. The order is key!
With both objects selected, press `CTRL P` to bring up the parenting menu:
<details>
<summary>The screen after creating a new empty</summary>
<img src="img/parenting.png" width = 50%/>
</details>
> [!NOTE]
> Note how the color-coding in the screenshot above shows how `Board` has been selected last.
> Make sure this looks the same on your screen.
In the popup, select the first option, namely `Object`. If everything went right, you should be able to "fold open" the `Board` to find your `Empty` as a child in there:
<details>
<summary>The board is the parent of the empty</summary>
<img src="img/empty_child.png" width = 50%/>
</details>
This hierarchy will exported to Bevy as well!
After this setup, we now have visible collider outlines that we can freely transform. Simply select the empty and transform it however you want. Whatever you do with this empty, the collider generated by Avian will look exactly like the outlines visible in Blender.
While you could (and sometimes should) scale this manually, there is a nice way of finding the right scale. Click on the `Board` object. Then, in the side menu, head to the `Item` tab. Check out the `Dimensions` reported there:
<details>
<summary>The dimensionality of the board</summary>
<img src="img/dimensions.png" width = 50%/>
</details>
> [!TIP]
> If you are not seeing this screen, you have probably clicked on the `Board` collection, and not on the item within it.
As you can see, its dimensions are:
- X: `5`
- Y: `1`
- Z: `3`
You can just use these values as the scale for the `Empty`. After everything is done, your final object should look like this in the viewport, when only the `Board` and its children are visible:
<details>
<summary>Finished board</summary>
<img src="img/empty_scaled.png" width = 100%/>
</details>
Note that the orange collider outlines should align nicely with the board's mesh.
### Using Wireframes
The last variant is a bit of a workaround for the fact that empties in Blender cannot have an arbitrary shape.
For example, a cylinder is not supported. So, we are going to create a new cylinder preview by hand.
Click on `Add` -> `Mesh` -> `Cylinder`. Don't click away yet!
Right after you create an object in Blender, you can modify how it should be generated. In the lower left, you should see the following popup:
<details>
<summary>Post-creation popup</summary>
<img src="img/create_cylinder.png" width = 50%/>
</details>
> [!NOTE]
> If you cannot see this popup, you cae changed Blender's focus after creating the object.
> You have to remove the cylinder and recreate it again.
Open up the popup to reveal a menu with some options for how to create a cylinder.
To again have a collider that nicely fits into a 1x1x1 space, set the `Radius` to `0.5` and the `Depth` to `1`.
To improve performance in Blender, you can also reduce the vertices, but this is not really important until you have hundreds of these colliders.
<details>
<summary>Settings for the cylinder</summary>
<img src="img/create_cylinder_options.png" width = 50%/>
</details>
Hide everything except the newly created cylinder. Press `Tab` to enter the edit mode. Press `A` to select all vertices.
Press `X` to open the deletion menu. Select `Only Faces`. Press `Tab` again to go back into object mode.
You should now have the wireframe of a cylinder.
<details>
<summary>Wireframe</summary>
<img src="img/wireframe.png" width = 50%/>
</details>
Now add a `ColliderConstructor` to it. This time, use the `Cylinder` variant. Set its `height` to `1` and `radius` to `0.5`, just as you did in the menu before.
<details>
<summary>Cylinder collider</summary>
<img src="img/cylinder_collider.png" width = 50%/>
</details>
The rest of the steps are identical to the empty: Drag-and-drop the cylinder collider into the `Cylinder` collection, make it a child of your `Cylinder` object and scale it accordingly. The result should look like this:
<details>
<summary>Cylinder collider on mesh</summary>
<img src="img/cylinder_collider_on_mesh.png" width = 50%/>
</details>
> [!TIP]
> Blender does not support creating all shapes that a collider would want.
> A notable omission is a capsule.
> You can use the builtin [Add Mesh Extra Objects](https://docs.blender.org/manual/en/latest/addons/add_mesh/mesh_extra_objects.html)
> extension to fill this gap.
## Populate the world
Go into your `World` scene. If you are coming from the [quickstart guide](../quickstart/readme.md), you can remove the `Player` empty that is left over.
If you have created this scene yourself in advance, make sure that it contains a camera, a light, and some kind of ground.
For reference, this is how our world setup looks:
<details>
<summary>The world setup before adding any physics</summary>
<img src="img/empty_world.png" width = 100%/>
</details>
Before we add any objects, we'll make the ground a rigid body as well. Add a `RigidBody` component as described before to it, but this time set it to `Static`. Add a collider to it in any of the ways described above. We used the `Quick and Dirty` method for this:
<details>
<summary>Ground collider</summary>
<img src="img/ground collider.png" width = 100%/>
</details>
> [!CAUTION]
> As mentioned before, when using this method you should be aware that the component
> is in Bevy's coordinate system, so set the `y_length` to the height of the ground.
Now add instances of the `Cube`, `Board`, and `Cylinder` to the world by selecting `Add` -> `Collection Instance`.
Since the objects are quite big, you may need to move the camera a bit further away to see them all.
We set its Y location to `-15` and the X rotation to `90` for this reason.
Pressing `0` on your numpad will show you a preview of what the camera sees.
Save the scene to let Blenvy export everything. Run your game with `cargo run` and you should see some objects falling onto the ground!
<details>
<summary>Objects falling onto the ground</summary>
<img src="img/falling.gif" width = 100%/>
</details>
> [!TIP]
> If your scene is doing something weird, try adding Avian's
> [`PhysicsDebugPlugin`](https://docs.rs/avian3d/latest/avian3d/debug_render/struct.PhysicsDebugPlugin.html)
> to your Bevy app to see the colliders at runtime.
## Add Dynamic Colliders
Now let's go for some more complex shapes.
Remember, most of the time you'll want to approximate the shape with a primitive collider, but sometimes you need the exact shape
or just quickly want to test something. For this, we are going to use dynamic colliders.
### Convex
Go back to the `Library` scene, add a new collection, and name it `Torus`. Select `Add` -> `Mesh` -> `Torus`. Leave it at the default transform. Add a `RigidBody` to it. Your scene should now look like this:
<details>
<summary>A simple torus</summary>
<img src="img/torus.png" width = 100%/>
</details>
We will now dynamically generate a convex hull around this torus.
You can imagine the result like how it would look like if you tightly wrapped the torus up as a christmas present.
This means that the hole in the middle will be treated as solid, which is okay for our case.
When using dynamic colliders, try to prefer convex shapes, as they are much faster to calculate than concave shapes.
To use a dynamic collider, we must proceed a bit differently from before.
Instead of adding the component to the torus *object*, we add it to the *mesh*.
You can access it by expanding your object in the outliner. Its icon is a green triangle:
<details>
<summary>The selected mesh</summary>
<img src="img/selected_mesh.png" width = 50%/>
</details>
With the *mesh* selected, add a `ColliderConstructor` to it. Set the variant to `ConvexHullFromMesh`.
If you did everything correctly, the component manager should say "Components for Torus (MESH)" at the top:
<details>
<summary>The component manager for the torus mesh</summary>
<img src="img/torus_component.png" width = 50%/>
</details>
That's all for now
### Concave
Add a new collection and name it `Monkey`. Select `Add` -> `Mesh` -> `Monkey`.
Yes, Blender has a builtin method for creating Suzanne, its monkey mascot. Isn't it great?
Anyways, add a rigid body to it. Afterwards, just as before, select the *mesh* of the monkey.
Add a `ColliderConstructor` to it. This time, set the variant to `TrimeshFromMesh`.
> [!CAUTION]
> While `TrimeshFromMesh` can deal with any kind of mesh, it is also the slowest collider to run.
> Additionally, the generated collider will always be treated as if it was hollow.
> That means that any objects that are completely inside the mesh will not collide with it.
> Only use a concave collider if you *really* need it.
## Add the Dynamic Colliders to the World
Save the scene to let Blenvy export everything.
Go back to the `World` scene. Add instances of the `Torus` and `Monkey` collections to the world and run the game with `cargo run`.
They should now fall onto the ground and interact with the other objects:
<details>
<summary>The primitive and dynamic colliders falling down</summary>
<img src="img/falling_dyn.gif" width = 100%/>
</details>
> [!TIP]
> Is your game crashing with `Tried to add a collider to entity Torus via <ConvexHullFromMesh or TrimeshFromMesh> that requires a mesh, but no mesh handle was found`?
> That means you added your `ColliderConstructor` to the object instead of the mesh.
> Go back to the screenshots above and make sure you have the mesh selected when adding the component.
## Other useful components
The object holding the `ColliderConstructor` can hold some additional components that are useful for tweaking the physics behavior.
- `ColliderDensity` will set the density of the collider and indirectly change the rigid body's mass.
- `Sensor` allow other objects to pass through the collider. It will still report the collision to the physics system so you can react to it.
- `CollisionLayers` controls which other colliders this collider will interact with. Note that since this is a bitflag, manipulating it in Blender is a bit cumbersome. You probably want to set up some kind of `enum` that can be used in Blender and then add the proper `CollisionLayers` in Bevy.
This is just a small selection. Refer to the [Avian documentation](https://docs.rs/avian3d/latest/avian3d/) for more information.

View File

@ -1,10 +1,20 @@
# Quickstart
This guide assumes you use Blender 4.2 or newer.
This guide assumes you use Blender 4.2 or newer and have set it to English.
> [!NOTE]
> This is not a Blender tutorial. No worries, we will make sure you find all necessary buttons to click, but we will not explain in detail what Blender concepts like "collections" or "scenes" are. If you are not familiar with Blender, you might want to look up some basic tutorials first.
## Table of Contents
- [Install the Blender Addon](#install-the-blender-addon)
- [Setup the Bevy side](#setup-the-bevy-side)
- [Setup the Blender addon for your project](#setup-the-blender-addon-for-your-project)
- [Create a blueprint](#create-a-blueprint)
- [Compose the world](#compose-the-world)
- [Run your game](#run-your-game)
- [Next Steps](#next-steps)
## Install the Blender Addon
- Download `blenvy.zip` from the [release page](https://github.com/kaosat-dev/Blenvy/releases/tag/blenvy_v0.1.0_pre_alpha)
@ -32,6 +42,8 @@ cargo add bevy
cargo add blenvy --git https://github.com/kaosat-dev/Blenvy/ --branch blenvy
```
This guide will tell you to `cargo run` at multiple points. We expect that you are still in the `my_game` directory when you do so.
Now, replace the contents of `src/main.rs` with the following:
<details>
@ -89,7 +101,7 @@ Create a directory under `my_game` called `art`. Hop back into Blender and save
</details>
Now, clear the default scene of any objects or collections.
The fastest way to do this is to look for the collection named simply `Collection` all the way on the right of Blender. Right-click on it and select `Delete Hierachy`.
The fastest way to do this is to look for the collection named simply `Collection` all the way on the right of Blender. Right-click on it and select `Delete Hierachy`. For future reference, the place where you just did this is called the *outliner*.
<details>
<summary>The default collection to delete</summary>
@ -154,9 +166,13 @@ Run, run your game with `cargo run`. It may crash because the scene is empty, bu
<img src="img/registry.png" width = 50%/>
</details>
This concludes the setup portion of the guide.
## Create a blueprint
Now, we will create an object for the `Player` component we defined earlier. While still in the `Library` scene, right-click on the `Scene Collection` all the way on the right. Select `New Collection`. Double-click on the new collection that appeared and rename it to `Player`. Click on it to have it selected.
Alright, let's jump into the actual workflow you'll be using to create your game.
We will first create an object for the `Player` component we defined earlier.
While still in the `Library` scene, right-click on the `Scene Collection` in the outliner to the right. Select `New Collection`. Double-click on the new collection that appeared and rename it to `Player`. Click on it to have it selected.
<details>
<summary>The player collection</summary>
@ -201,7 +217,7 @@ Now click `Add`. You can now set all the player's fields in a neat Blender UI. N
Congratulations, you have just created your first blueprint.
## Composing the world
## Compose the world
Let's populate our world now. Switch back to the `World` scene in Blender as described before.
@ -212,7 +228,10 @@ We will add the following kinds of objects to our world:
- A light
- The player character, which will be a blueprint instance
First, let's add a camera through `Add` -> `Camera`. Blender will make the camera face whatever direction the viewport is aimed at, which will probably be completely arbitrary. Move the camera a bit so that it faces the world origin and is rotated the right way. You can just copy our transform if you like:
First, let's add a camera through `Add` -> `Camera`. Blender will make the camera face whatever direction the viewport is aimed at, which will probably be completely arbitrary.
Move the camera a bit so that it faces the world origin and is rotated the right way.
Pressing `0` on your numpad will show you a preview of what the camera sees.
You can just copy our transform if you like:
- Location:
- X: `0`
@ -283,3 +302,9 @@ Congrats, you're done! Just run your game with `cargo run` and you should see yo
</details>
Okay, maybe not that much glory. But the important part is that the player is visible in the scene and has a `Player` component on it. You can now add more components to the player, create more blueprints, and populate your world with them. They can have animations, materials, etc. Have fun!
## Next Steps
- Read the [Blevy for Bevy](../../crates/blenvy/README.md) documentation for more features on the Bevy side.
- Read the [Blevy for Blender](../../tools/blenvy/README.md) documentation for more features on the Blender side.
- Read about the [Avian Physics Integration](../avian/readme.md) to learn how to setup colliders in Blender that will be used by the Avian physics engine in Bevy.

Binary file not shown.

View File

@ -1 +0,0 @@
({})

View File

@ -1,8 +0,0 @@
({
"world":File (path: "models/World.glb"),
"world_dynamic":File (path: "models/World_dynamic.glb"),
"models": Folder (
path: "models/library",
),
})

Binary file not shown.

View File

@ -0,0 +1,5 @@
(
assets:
[
]
)

Binary file not shown.

View File

@ -0,0 +1,5 @@
(
assets:
[
]
)

Binary file not shown.

View File

@ -0,0 +1,9 @@
(
assets:
[
("Pillar", File ( path: "blueprints/Pillar.glb" )),
("Stone", File ( path: "materials/Stone.glb" )),
("Mover", File ( path: "blueprints/Mover.glb" )),
("Material.001", File ( path: "materials/Material.001.glb" )),
]
)

Binary file not shown.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,14 +1,11 @@
use std::any::TypeId;
use bevy::{prelude::*, utils::hashbrown::HashSet};
use blenvy::{AddToGameWorld, BlenvyPlugin, BluePrintBundle, BlueprintInfo, DynamicBlueprintInstance, GameWorldTag, HideUntilReady, SpawnBlueprint};
use blenvy::{AddToGameWorld, BlenvyPlugin, BluePrintBundle, BlueprintInfo, Dynamic, DynamicBlueprintInstance, GameWorldTag, HideUntilReady, SaveRequest, SpawnBlueprint};
use rand::Rng;
mod core;
use crate::core::*;
mod game;
use game::*;
// mod game;
// use game::*;
mod component_examples;
use component_examples::*;
@ -37,13 +34,12 @@ fn main() {
..Default::default()
},
// our custom plugins
CorePlugin, // reusable plugins
GamePlugin, // specific to our game
// GamePlugin, // specific to our game
ComponentsExamplesPlugin, // Showcases different type of components /structs
))
.add_systems(Startup, setup_game)
.add_systems(Update, (spawn_blueprint_instance, save_game, load_game))
.add_systems(Update, (spawn_blueprint_instance, move_movers, save_game, load_game))
.run();
}
@ -60,6 +56,13 @@ fn setup_game(
HideUntilReady, // only reveal the level once it is ready
GameWorldTag,
));
// here we spawn our game world/level, which is also a blueprint !
commands.spawn((
BlueprintInfo::from_path("levels/World_dynamic.glb"), // all we need is a Blueprint info...
SpawnBlueprint, // and spawnblueprint to tell blenvy to spawn the blueprint now
HideUntilReady, // only reveal the level once it is ready
));
}
// you can also spawn blueprint instances at runtime
@ -90,15 +93,47 @@ fn spawn_blueprint_instance(
}
}
fn save_game(
keycode: Res<ButtonInput<KeyCode>>,
fn move_movers(
mut movers: Query<(&mut Transform), With<Dynamic>>
) {
if keycode.just_pressed(KeyCode::KeyS) {
for mut transform in movers.iter_mut(){
println!("moving dynamic entity");
transform.translation.x += 0.01;
}
}
fn save_game(
keycode: Res<ButtonInput<KeyCode>>,
mut save_requests: EventWriter<SaveRequest>,
) {
if keycode.just_pressed(KeyCode::KeyS) {
save_requests.send(SaveRequest {
path: "scenes/save.scn.ron".into(),
});
}
}
/*
pub fn request_save(
mut save_requests: EventWriter<SaveRequest>,
keycode: Res<ButtonInput<KeyCode>>,
current_state: Res<State<GameState>>,
mut next_game_state: ResMut<NextState<GameState>>,
) {
if keycode.just_pressed(KeyCode::KeyS)
&& (current_state.get() != &GameState::InLoading)
&& (current_state.get() != &GameState::InSaving)
{
next_game_state.set(GameState::InSaving);
save_requests.send(SaveRequest {
path: "save.scn.ron".into(),
});
}
}*/
fn load_game(
keycode: Res<ButtonInput<KeyCode>>,
) {

View File

@ -1,12 +1,14 @@
import bpy
from ...bevy_components.components.metadata import get_bevy_component_value_by_long_name
# checks if an object is dynamic
# TODO: for efficiency, it might make sense to write this flag semi automatically at the root level of the object so we can skip the inner loop
# TODO: we need to recompute these on blueprint changes too
# even better, keep a list of dynamic objects per scene , updated only when needed ?
def is_object_dynamic(object):
is_dynamic = object['Dynamic'] if 'Dynamic' in object else False
is_dynamic = get_bevy_component_value_by_long_name(object, 'blenvy::save_load::Dynamic') is not None
#is_dynamic = object['Dynamic'] if 'Dynamic' in object else False
# only look for data in the original collection if it is not alread marked as dynamic at instance level
if not is_dynamic and object.type == 'EMPTY' and hasattr(object, 'instance_collection') and object.instance_collection is not None :
#print("collection", object.instance_collection, "object", object.name)
@ -14,15 +16,18 @@ def is_object_dynamic(object):
collection_name = object.instance_collection.name
original_collection = bpy.data.collections[collection_name]
is_dynamic = get_bevy_component_value_by_long_name(original_collection, 'blenvy::save_load::Dynamic') is not None
# scan original collection, look for a 'Dynamic' flag
for object in original_collection.objects:
"""for object in original_collection.objects:
#print(" inner", object)
if object.type == 'EMPTY' and object.name.endswith("components"):
if object.type == 'EMPTY': #and object.name.endswith("components"):
for component_name in object.keys():
#print(" compo", component_name)
if component_name == 'Dynamic':
is_dynamic = True
break
break"""
print("IS OBJECT DYNAMIC", object, is_dynamic)
return is_dynamic
def is_object_static(object):

View File

@ -1,10 +1,8 @@
import os
import bpy
from pathlib import Path
from blenvy.core.helpers_collections import (traverse_tree)
from blenvy.core.object_makers import make_cube
from blenvy.materials.materials_helpers import add_material_info_to_objects, get_all_materials
from ..common.generate_temporary_scene_and_export import generate_temporary_scene_and_export
from ..common.export_gltf import (generate_gltf_export_settings)