feat(nested_blueprints): added support for nested blueprints both in bevy_gltf_blueprints & in gltf_auto_export (#65)

* feat(bevy_gltf_blueprints): 
  * added support for nested blueprints
  * added example/test for nested blueprints with multiple levels of nesting
* feat(tools/gltf_auto_export): 
  * implemented functionality to export nested blueprints/collection instances
  * added add prefs to control nested blueprints export
  * experiments with nested blueprints merge modes
  * when exporting of nested blueprints is disabled, will not scan for nested blueprints
and thus no export them
* docs(): updated docs & added images for nested blueprints support
This commit is contained in:
Mark Moissette 2023-12-22 00:37:52 +01:00 committed by GitHub
parent 2c4720b951
commit 6047959b3f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
63 changed files with 6719 additions and 42 deletions

14
Cargo.lock generated
View File

@ -779,7 +779,7 @@ dependencies = [
[[package]]
name = "bevy_gltf_blueprints"
version = "0.4.0"
version = "0.5.0"
dependencies = [
"bevy",
"bevy_gltf_components 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
@ -857,6 +857,18 @@ dependencies = [
"rand",
]
[[package]]
name = "bevy_gltf_blueprints_nested_blueprints_example"
version = "0.3.0"
dependencies = [
"bevy",
"bevy_asset_loader",
"bevy_editor_pls",
"bevy_gltf_blueprints",
"bevy_rapier3d",
"rand",
]
[[package]]
name = "bevy_gltf_components"
version = "0.2.0"

View File

@ -6,6 +6,7 @@ members = [
"examples/bevy_gltf_blueprints/basic/",
"examples/bevy_gltf_blueprints/basic_scene_components/",
"examples/bevy_gltf_blueprints/basic_xpbd_physics/",
"examples/bevy_gltf_blueprints/nested_blueprints/",
"examples/bevy_gltf_blueprints/animation/",
"examples/bevy_gltf_blueprints/multiple_levels/",
"examples/bevy_gltf_blueprints/materials/"

View File

@ -1,6 +1,6 @@
[package]
name = "bevy_gltf_blueprints"
version = "0.4.0"
version = "0.5.0"
authors = ["Mark 'kaosat-dev' Moissette"]
description = "Adds the ability to define Blueprints/Prefabs for [Bevy](https://bevyengine.org/) inside gltf files and spawn them in Bevy."
homepage = "https://github.com/kaosat-dev/Blender_bevy_components_workflow"

View File

@ -26,7 +26,7 @@ Here's a minimal usage example:
# Cargo.toml
[dependencies]
bevy="0.12"
bevy_gltf_blueprints = { version = "0.4"}
bevy_gltf_blueprints = { version = "0.5"}
```
@ -64,7 +64,7 @@ fn spawn_blueprint(
Add the following to your `[dependencies]` section in `Cargo.toml`:
```toml
bevy_gltf_blueprints = "0.4"
bevy_gltf_blueprints = "0.5"
```
Or use `cargo add`:
@ -281,6 +281,8 @@ https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/example
https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/examples/materials
https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/examples/nested_blueprints
## Compatible Bevy versions
@ -289,7 +291,7 @@ The main branch is compatible with the latest Bevy release, while the branch `be
Compatibility of `bevy_gltf_blueprints` versions:
| `bevy_gltf_blueprints` | `bevy` |
| :-- | :-- |
| `0.3 - 0.4` | `0.12` |
| `0.3 - 0.5` | `0.12` |
| `0.1 - 0.2` | `0.11` |
| branch `main` | `0.12` |
| branch `bevy_main` | `main` |

View File

@ -34,7 +34,7 @@ pub struct SpawnedRoot;
/// * also takes into account the already exisiting "override" components, ie "override components" > components from blueprint
pub(crate) fn spawn_from_blueprints(
spawn_placeholders: Query<
(Entity, &Name, &BlueprintName, &Transform),
(Entity, &Name, &BlueprintName, &Transform, Option<&Parent>),
(
Added<BlueprintName>,
Added<SpawnHere>,
@ -49,9 +49,12 @@ pub(crate) fn spawn_from_blueprints(
assets_gltf: Res<Assets<Gltf>>,
asset_server: Res<AssetServer>,
blueprints_config: Res<BluePrintsConfig>,
) {
for (entity, name, blupeprint_name, transform) in spawn_placeholders.iter() {
debug!("need to spawn {:?}", blupeprint_name.0);
for (entity, name, blupeprint_name, transform, original_parent) in spawn_placeholders.iter() {
debug!("need to spawn {:?}, id: {:?}", blupeprint_name.0, entity);
let what = &blupeprint_name.0;
let model_file_name = format!("{}.{}", &what, &blueprints_config.format);
let model_path =
@ -60,8 +63,7 @@ pub(crate) fn spawn_from_blueprints(
debug!("attempting to spawn {:?}", model_path);
let model_handle: Handle<Gltf> = asset_server.load(model_path);
let world = game_world.single_mut();
let world = world.1[0]; // FIXME: dangerous hack because our gltf data have a single child like this, but might not always be the case
let gltf = assets_gltf
.get(&model_handle)
@ -92,6 +94,15 @@ pub(crate) fn spawn_from_blueprints(
},
))
.id();
commands.entity(world).add_child(child_scene);
let world = game_world.single_mut();
let mut parent = world.1[0]; // FIXME: dangerous hack because our gltf data have a single child like this, but might not always be the case
// ideally, insert the newly created entity as a child of the original parent, if any, the world otherwise
if let Some(original_parent) = original_parent {
parent = original_parent.get();
}
commands.entity(parent).add_child(child_scene);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,13 @@
[package]
name = "bevy_gltf_blueprints_nested_blueprints_example"
version = "0.3.0"
edition = "2021"
license = "MIT OR Apache-2.0"
[dependencies]
bevy="0.12"
bevy_gltf_blueprints = { path = "../../../crates/bevy_gltf_blueprints" }
bevy_rapier3d = { version = "0.23.0", features = [ "serde-serialize", "debug-render-3d", "enhanced-determinism"] }
bevy_asset_loader = { version = "0.18", features = ["standard_dynamic_assets" ]}
bevy_editor_pls = { version = "0.6" }
rand = "0.8.5"

View File

@ -0,0 +1,14 @@
# Nested blueprints example/demo
Example of blueprints (and thus gltf) nested & reuse to avoid redundant data in blueprints gltfs that lead to asset & memory bloat
- ideally, to be used together with ```gltf_auto_export``` version >0.8 with the "export nested blueprints" option for exports, as that will generate whole
gltf blueprints hierarchies, and minimise their size for you
- It shows you how ou can configure```Bevy_gltf_blueprints``` to spawn nested blueprints
## Running this example
```
cargo run --features bevy/dynamic_linking
```

View File

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

View File

@ -0,0 +1,5 @@
use bevy::prelude::*;
use bevy_asset_loader::prelude::*;
#[derive(AssetCollection, Resource)]
pub struct CoreAssets {}

View File

@ -0,0 +1,13 @@
use bevy::gltf::Gltf;
use bevy::prelude::*;
use bevy::utils::HashMap;
use bevy_asset_loader::prelude::*;
#[derive(AssetCollection, Resource)]
pub struct GameAssets {
#[asset(key = "world")]
pub world: Handle<Gltf>,
#[asset(key = "models", collection(typed, mapped))]
pub models: HashMap<String, Handle<Gltf>>,
}

View File

@ -0,0 +1,35 @@
pub mod assets_core;
pub use assets_core::*;
pub mod assets_game;
pub use assets_game::*;
use bevy::prelude::*;
use bevy_asset_loader::prelude::*;
use crate::state::AppState;
pub struct AssetsPlugin;
impl Plugin for AssetsPlugin {
fn build(&self, app: &mut App) {
app
// load core assets (ie assets needed in the main menu, and everywhere else before loading more assets in game)
.add_loading_state(
LoadingState::new(AppState::CoreLoading).continue_to_state(AppState::MenuRunning),
)
.add_dynamic_collection_to_loading_state::<_, StandardDynamicAssetCollection>(
AppState::CoreLoading,
"assets_core.assets.ron",
)
.add_collection_to_loading_state::<_, CoreAssets>(AppState::CoreLoading)
// load game assets
.add_loading_state(
LoadingState::new(AppState::AppLoading).continue_to_state(AppState::AppRunning),
)
.add_dynamic_collection_to_loading_state::<_, StandardDynamicAssetCollection>(
AppState::AppLoading,
"assets_game.assets.ron",
)
.add_collection_to_loading_state::<_, GameAssets>(AppState::AppLoading);
}
}

View File

@ -0,0 +1,24 @@
use bevy::core_pipeline::bloom::{BloomCompositeMode, BloomSettings};
use bevy::core_pipeline::tonemapping::{DebandDither, Tonemapping};
use bevy::prelude::*;
use super::CameraTrackingOffset;
pub fn camera_replace_proxies(
mut commands: Commands,
mut added_cameras: Query<(Entity, &mut Camera), (Added<Camera>, With<CameraTrackingOffset>)>,
) {
for (entity, mut camera) in added_cameras.iter_mut() {
info!("detected added camera, updating proxy");
camera.hdr = true;
commands
.entity(entity)
.insert(DebandDither::Enabled)
.insert(Tonemapping::BlenderFilmic)
.insert(BloomSettings {
intensity: 0.01,
composite_mode: BloomCompositeMode::Additive,
..default()
});
}
}

View File

@ -0,0 +1,58 @@
use bevy::prelude::*;
#[derive(Component, Reflect, Debug)]
#[reflect(Component)]
/// Component for cameras, with an offset from the Trackable target
///
pub struct CameraTracking {
pub offset: Vec3,
}
impl Default for CameraTracking {
fn default() -> Self {
CameraTracking {
offset: Vec3::new(0.0, 6.0, 8.0),
}
}
}
#[derive(Component, Reflect, Debug, Deref, DerefMut)]
#[reflect(Component)]
/// Component for cameras, with an offset from the Trackable target
pub struct CameraTrackingOffset(Vec3);
impl Default for CameraTrackingOffset {
fn default() -> Self {
CameraTrackingOffset(Vec3::new(0.0, 6.0, 8.0))
}
}
impl CameraTrackingOffset {
fn new(input: Vec3) -> Self {
CameraTrackingOffset(input)
}
}
#[derive(Component, Reflect, Default, Debug)]
#[reflect(Component)]
/// Add this component to an entity if you want it to be tracked by a Camera
pub struct CameraTrackable;
pub fn camera_track(
mut tracking_cameras: Query<
(&mut Transform, &CameraTrackingOffset),
(
With<Camera>,
With<CameraTrackingOffset>,
Without<CameraTrackable>,
),
>,
camera_tracked: Query<&Transform, With<CameraTrackable>>,
) {
for (mut camera_transform, tracking_offset) in tracking_cameras.iter_mut() {
for tracked_transform in camera_tracked.iter() {
let target_position = tracked_transform.translation + tracking_offset.0;
let eased_position = camera_transform.translation.lerp(target_position, 0.1);
camera_transform.translation = eased_position; // + tracking.offset;// tracked_transform.translation + tracking.offset;
*camera_transform = camera_transform.looking_at(tracked_transform.translation, Vec3::Y);
}
}
}

View File

@ -0,0 +1,24 @@
pub mod camera_tracking;
pub use camera_tracking::*;
pub mod camera_replace_proxies;
pub use camera_replace_proxies::*;
use bevy::prelude::*;
use bevy_gltf_blueprints::GltfBlueprintsSet;
pub struct CameraPlugin;
impl Plugin for CameraPlugin {
fn build(&self, app: &mut App) {
app.register_type::<CameraTrackable>()
.register_type::<CameraTracking>()
.register_type::<CameraTrackingOffset>()
.add_systems(
Update,
(
camera_replace_proxies.after(GltfBlueprintsSet::AfterSpawn),
camera_track,
),
);
}
}

View File

@ -0,0 +1,25 @@
use bevy::prelude::*;
use bevy::pbr::{CascadeShadowConfig, CascadeShadowConfigBuilder};
// fixme might be too specific to might needs, should it be moved out ? also these are all for lights, not models
pub fn lighting_replace_proxies(
mut added_dirights: Query<(Entity, &mut DirectionalLight), Added<DirectionalLight>>,
mut added_spotlights: Query<&mut SpotLight, Added<SpotLight>>,
mut commands: Commands,
) {
for (entity, mut light) in added_dirights.iter_mut() {
light.illuminance *= 5.0;
light.shadows_enabled = true;
let shadow_config: CascadeShadowConfig = CascadeShadowConfigBuilder {
first_cascade_far_bound: 15.0,
maximum_distance: 135.0,
..default()
}
.into();
commands.entity(entity).insert(shadow_config);
}
for mut light in added_spotlights.iter_mut() {
light.shadows_enabled = true;
}
}

View File

@ -0,0 +1,18 @@
mod lighting_replace_proxies;
use lighting_replace_proxies::*;
use bevy::pbr::{DirectionalLightShadowMap, NotShadowCaster};
use bevy::prelude::*;
pub struct LightingPlugin;
impl Plugin for LightingPlugin {
fn build(&self, app: &mut App) {
app
.insert_resource(DirectionalLightShadowMap { size: 4096 })
// FIXME: adding these since they are missing
.register_type::<NotShadowCaster>()
.add_systems(PreUpdate, lighting_replace_proxies) // FIXME: you should actually run this in a specific state most likely
;
}
}

View File

@ -0,0 +1,35 @@
pub mod camera;
pub use camera::*;
pub mod lighting;
pub use lighting::*;
pub mod relationships;
pub use relationships::*;
pub mod physics;
pub use physics::*;
// pub mod save_load;
// pub use save_load::*;
use bevy::prelude::*;
use bevy_gltf_blueprints::*;
pub struct CorePlugin;
impl Plugin for CorePlugin {
fn build(&self, app: &mut App) {
app.add_plugins((
LightingPlugin,
CameraPlugin,
PhysicsPlugin,
// SaveLoadPlugin,
BlueprintsPlugin {
library_folder: "models/library".into(),
format: GltfFormat::GLB,
aabbs: true,
..Default::default()
},
));
}
}

View File

@ -0,0 +1,12 @@
use bevy::prelude::{info, ResMut};
use bevy_rapier3d::prelude::RapierConfiguration;
pub fn pause_physics(mut physics_config: ResMut<RapierConfiguration>) {
info!("pausing physics");
physics_config.physics_pipeline_active = false;
}
pub fn resume_physics(mut physics_config: ResMut<RapierConfiguration>) {
info!("unpausing physics");
physics_config.physics_pipeline_active = true;
}

View File

@ -0,0 +1,37 @@
pub mod physics_replace_proxies;
use bevy_rapier3d::{
prelude::{NoUserData, RapierPhysicsPlugin},
render::RapierDebugRenderPlugin,
};
pub use physics_replace_proxies::*;
pub mod utils;
pub mod controls;
pub use controls::*;
use crate::state::GameState;
use bevy::prelude::*;
// use super::blueprints::GltfBlueprintsSet;
use bevy_gltf_blueprints::GltfBlueprintsSet;
// use crate::Collider;
pub struct PhysicsPlugin;
impl Plugin for PhysicsPlugin {
fn build(&self, app: &mut App) {
app.add_plugins((
RapierPhysicsPlugin::<NoUserData>::default(),
RapierDebugRenderPlugin::default(),
))
.register_type::<AutoAABBCollider>()
.register_type::<physics_replace_proxies::Collider>()
// find a way to make serde's stuff serializable
// .register_type::<bevy_rapier3d::dynamics::CoefficientCombineRule>()
//bevy_rapier3d::dynamics::CoefficientCombineRule
.add_systems(
Update,
physics_replace_proxies.after(GltfBlueprintsSet::AfterSpawn),
)
.add_systems(OnEnter(GameState::InGame), resume_physics)
.add_systems(OnExit(GameState::InGame), pause_physics);
}
}

View File

@ -0,0 +1,101 @@
use bevy::prelude::*;
// use bevy::render::primitives::Aabb;
use bevy_rapier3d::geometry::Collider as RapierCollider;
use bevy_rapier3d::prelude::{ActiveCollisionTypes, ActiveEvents, ComputedColliderShape};
use super::utils::*;
#[derive(Component, Reflect, Default, Debug)]
#[reflect(Component)]
pub enum Collider {
Ball(f32),
Cuboid(Vec3),
Capsule(Vec3, Vec3, f32),
#[default]
Mesh,
}
#[derive(Component, Reflect, Default, Debug)]
#[reflect(Component)]
pub enum AutoAABBCollider {
#[default]
Cuboid,
Ball,
Capsule,
}
// replaces all physics stand-ins with the actual rapier types
pub fn physics_replace_proxies(
meshes: Res<Assets<Mesh>>,
mesh_handles: Query<&Handle<Mesh>>,
mut proxy_colliders: Query<
(Entity, &Collider, &Name, &mut Visibility),
(Without<RapierCollider>, Added<Collider>),
>,
// needed for tri meshes
children: Query<&Children>,
mut commands: Commands,
) {
for proxy_colider in proxy_colliders.iter_mut() {
let (entity, collider_proxy, name, mut visibility) = proxy_colider;
// we hide the collider meshes: perhaps they should be removed altogether once processed ?
if name.ends_with("_collider") || name.ends_with("_sensor") {
*visibility = Visibility::Hidden;
}
let mut rapier_collider: RapierCollider;
match collider_proxy {
Collider::Ball(radius) => {
info!("generating collider from proxy: ball");
rapier_collider = RapierCollider::ball(*radius);
commands.entity(entity)
.insert(rapier_collider)
.insert(ActiveEvents::COLLISION_EVENTS) // FIXME: this is just for demo purposes !!!
;
}
Collider::Cuboid(size) => {
info!("generating collider from proxy: cuboid");
rapier_collider = RapierCollider::cuboid(size.x, size.y, size.z);
commands.entity(entity)
.insert(rapier_collider)
.insert(ActiveEvents::COLLISION_EVENTS) // FIXME: this is just for demo purposes !!!
;
}
Collider::Capsule(a, b, radius) => {
info!("generating collider from proxy: capsule");
rapier_collider = RapierCollider::capsule(*a, *b, *radius);
commands.entity(entity)
.insert(rapier_collider)
.insert(ActiveEvents::COLLISION_EVENTS) // FIXME: this is just for demo purposes !!!
;
}
Collider::Mesh => {
info!("generating collider from proxy: mesh");
for (_, collider_mesh) in
Mesh::search_in_children(entity, &children, &meshes, &mesh_handles)
{
rapier_collider = RapierCollider::from_bevy_mesh(
collider_mesh,
&ComputedColliderShape::TriMesh,
)
.unwrap();
commands
.entity(entity)
.insert(rapier_collider)
// FIXME: this is just for demo purposes !!!
.insert(
ActiveCollisionTypes::default()
| ActiveCollisionTypes::KINEMATIC_STATIC
| ActiveCollisionTypes::STATIC_STATIC
| ActiveCollisionTypes::DYNAMIC_STATIC,
)
.insert(ActiveEvents::COLLISION_EVENTS);
// .insert(ActiveEvents::COLLISION_EVENTS)
// break;
// RapierCollider::convex_hull(points)
}
}
}
}
}

View File

@ -0,0 +1,175 @@
use bevy::prelude::*;
use bevy::render::mesh::{MeshVertexAttributeId, PrimitiveTopology, VertexAttributeValues};
// TAKEN VERBATIB FROM https://github.com/janhohenheim/foxtrot/blob/src/util/trait_extension.rs
pub(crate) trait Vec3Ext: Copy {
fn is_approx_zero(self) -> bool;
fn split(self, up: Vec3) -> SplitVec3;
}
impl Vec3Ext for Vec3 {
#[inline]
fn is_approx_zero(self) -> bool {
self.length_squared() < 1e-5
}
#[inline]
fn split(self, up: Vec3) -> SplitVec3 {
let vertical = up * self.dot(up);
let horizontal = self - vertical;
SplitVec3 {
vertical,
horizontal,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub(crate) struct SplitVec3 {
pub(crate) vertical: Vec3,
pub(crate) horizontal: Vec3,
}
pub(crate) trait Vec2Ext: Copy {
fn is_approx_zero(self) -> bool;
fn x0y(self) -> Vec3;
}
impl Vec2Ext for Vec2 {
#[inline]
fn is_approx_zero(self) -> bool {
self.length_squared() < 1e-5
}
#[inline]
fn x0y(self) -> Vec3 {
Vec3::new(self.x, 0., self.y)
}
}
pub(crate) trait MeshExt {
fn transform(&mut self, transform: Transform);
fn transformed(&self, transform: Transform) -> Mesh;
fn read_coords_mut(&mut self, id: impl Into<MeshVertexAttributeId>) -> &mut Vec<[f32; 3]>;
fn search_in_children<'a>(
parent: Entity,
children: &'a Query<&Children>,
meshes: &'a Assets<Mesh>,
mesh_handles: &'a Query<&Handle<Mesh>>,
) -> Vec<(Entity, &'a Mesh)>;
}
impl MeshExt for Mesh {
fn transform(&mut self, transform: Transform) {
for coords in self.read_coords_mut(Mesh::ATTRIBUTE_POSITION.clone()) {
let vec3 = (*coords).into();
let transformed = transform.transform_point(vec3);
*coords = transformed.into();
}
for normal in self.read_coords_mut(Mesh::ATTRIBUTE_NORMAL.clone()) {
let vec3 = (*normal).into();
let transformed = transform.rotation.mul_vec3(vec3);
*normal = transformed.into();
}
}
fn transformed(&self, transform: Transform) -> Mesh {
let mut mesh = self.clone();
mesh.transform(transform);
mesh
}
fn read_coords_mut(&mut self, id: impl Into<MeshVertexAttributeId>) -> &mut Vec<[f32; 3]> {
// Guaranteed by Bevy for the current usage
match self
.attribute_mut(id)
.expect("Failed to read unknown mesh attribute")
{
VertexAttributeValues::Float32x3(values) => values,
// Guaranteed by Bevy for the current usage
_ => unreachable!(),
}
}
fn search_in_children<'a>(
parent: Entity,
children_query: &'a Query<&Children>,
meshes: &'a Assets<Mesh>,
mesh_handles: &'a Query<&Handle<Mesh>>,
) -> Vec<(Entity, &'a Mesh)> {
if let Ok(children) = children_query.get(parent) {
let mut result: Vec<_> = children
.iter()
.filter_map(|entity| mesh_handles.get(*entity).ok().map(|mesh| (*entity, mesh)))
.map(|(entity, mesh_handle)| {
(
entity,
meshes
.get(mesh_handle)
.expect("Failed to get mesh from handle"),
)
})
.map(|(entity, mesh)| {
assert_eq!(mesh.primitive_topology(), PrimitiveTopology::TriangleList);
(entity, mesh)
})
.collect();
let mut inner_result = children
.iter()
.flat_map(|entity| {
Self::search_in_children(*entity, children_query, meshes, mesh_handles)
})
.collect();
result.append(&mut inner_result);
result
} else {
Vec::new()
}
}
}
pub(crate) trait F32Ext: Copy {
fn is_approx_zero(self) -> bool;
fn squared(self) -> f32;
fn lerp(self, other: f32, ratio: f32) -> f32;
}
impl F32Ext for f32 {
#[inline]
fn is_approx_zero(self) -> bool {
self.abs() < 1e-5
}
#[inline]
fn squared(self) -> f32 {
self * self
}
#[inline]
fn lerp(self, other: f32, ratio: f32) -> f32 {
self.mul_add(1. - ratio, other * ratio)
}
}
pub(crate) trait TransformExt: Copy {
fn horizontally_looking_at(self, target: Vec3, up: Vec3) -> Transform;
fn lerp(self, other: Transform, ratio: f32) -> Transform;
}
impl TransformExt for Transform {
fn horizontally_looking_at(self, target: Vec3, up: Vec3) -> Transform {
let direction = target - self.translation;
let horizontal_direction = direction - up * direction.dot(up);
let look_target = self.translation + horizontal_direction;
self.looking_at(look_target, up)
}
fn lerp(self, other: Transform, ratio: f32) -> Transform {
let translation = self.translation.lerp(other.translation, ratio);
let rotation = self.rotation.slerp(other.rotation, ratio);
let scale = self.scale.lerp(other.scale, ratio);
Transform {
translation,
rotation,
scale,
}
}
}

View File

@ -0,0 +1,75 @@
use bevy::prelude::*;
use bevy::render::mesh::{MeshVertexAttributeId, PrimitiveTopology, VertexAttributeValues};
// TAKEN VERBATIB FROM https://github.com/janhohenheim/foxtrot/blob/6e31fc02652fc9d085a4adde0a73ab007dbbb0dc/src/util/trait_extension.rs
pub trait Vec3Ext {
#[allow(clippy::wrong_self_convention)] // Because [`Vec3`] is [`Copy`]
fn is_approx_zero(self) -> bool;
fn x0z(self) -> Vec3;
}
impl Vec3Ext for Vec3 {
fn is_approx_zero(self) -> bool {
[self.x, self.y, self.z].iter().all(|&x| x.abs() < 1e-5)
}
fn x0z(self) -> Vec3 {
Vec3::new(self.x, 0., self.z)
}
}
pub trait MeshExt {
fn transform(&mut self, transform: Transform);
fn transformed(&self, transform: Transform) -> Mesh;
fn read_coords_mut(&mut self, id: impl Into<MeshVertexAttributeId>) -> &mut Vec<[f32; 3]>;
fn search_in_children<'a>(
children: &'a Children,
meshes: &'a Assets<Mesh>,
mesh_handles: &'a Query<&Handle<Mesh>>,
) -> (Entity, &'a Mesh);
}
impl MeshExt for Mesh {
fn transform(&mut self, transform: Transform) {
for attribute in [Mesh::ATTRIBUTE_POSITION, Mesh::ATTRIBUTE_NORMAL] {
for coords in self.read_coords_mut(attribute.clone()) {
let vec3 = (*coords).into();
let transformed = transform.transform_point(vec3);
*coords = transformed.into();
}
}
}
fn transformed(&self, transform: Transform) -> Mesh {
let mut mesh = self.clone();
mesh.transform(transform);
mesh
}
fn read_coords_mut(&mut self, id: impl Into<MeshVertexAttributeId>) -> &mut Vec<[f32; 3]> {
match self.attribute_mut(id).unwrap() {
VertexAttributeValues::Float32x3(values) => values,
// Guaranteed by Bevy
_ => unreachable!(),
}
}
fn search_in_children<'a>(
children: &'a Children,
meshes: &'a Assets<Mesh>,
mesh_handles: &'a Query<&Handle<Mesh>>,
) -> (Entity, &'a Mesh) {
let entity_handles: Vec<_> = children
.iter()
.filter_map(|entity| mesh_handles.get(*entity).ok().map(|mesh| (*entity, mesh)))
.collect();
assert_eq!(
entity_handles.len(),
1,
"Collider must contain exactly one mesh, but found {}",
entity_handles.len()
);
let (entity, mesh_handle) = entity_handles.first().unwrap();
let mesh = meshes.get(mesh_handle).unwrap();
assert_eq!(mesh.primitive_topology(), PrimitiveTopology::TriangleList);
(*entity, mesh)
}
}

View File

@ -0,0 +1,11 @@
pub mod relationships_insert_dependant_components;
pub use relationships_insert_dependant_components::*;
use bevy::prelude::*;
pub struct EcsRelationshipsPlugin;
impl Plugin for EcsRelationshipsPlugin {
fn build(&self, app: &mut App) {
app;
}
}

View File

@ -0,0 +1,15 @@
use bevy::prelude::*;
pub fn insert_dependant_component<
Dependant: Component,
Dependency: Component + std::default::Default,
>(
mut commands: Commands,
entities_without_depency: Query<(Entity, &Name), (With<Dependant>, Without<Dependency>)>,
) {
for (entity, name) in entities_without_depency.iter() {
let name = name.clone().to_string();
commands.entity(entity).insert(Dependency::default());
warn!("found an entity called {} with a {} component but without an {}, please check your assets", name.clone(), std::any::type_name::<Dependant>(), std::any::type_name::<Dependency>());
}
}

View File

@ -0,0 +1,218 @@
use bevy::prelude::*;
use bevy_gltf_blueprints::{clone_entity::CloneEntity, GameWorldTag, SpawnHere};
use crate::{
assets::GameAssets,
state::{AppState, GameState, InAppRunning},
};
use super::Saveable;
const SCENE_FILE_PATH: &str = "scenes/save.scn.ron";
#[derive(Component, Debug)]
pub struct TempLoadedSceneMarker;
#[derive(Component, Debug)]
pub struct SaveablesToRemove(Vec<(Entity, Name)>);
#[derive(Component, Event)]
pub struct LoadRequest {
pub path: String,
}
pub fn should_load(save_requested_events: EventReader<LoadRequest>) -> bool {
return save_requested_events.len() > 0;
}
pub fn load_prepare(
mut next_app_state: ResMut<NextState<AppState>>,
mut next_game_state: ResMut<NextState<GameState>>,
) {
next_app_state.set(AppState::LoadingGame);
next_game_state.set(GameState::None);
info!("--loading: prepare")
}
/// unload the level recursively
pub fn _unload_world_old(world: &mut World) {
let entities: Vec<Entity> = world
// .query_filtered::<Entity, Or<(With<Save>, With<Unload>)>>()
.query_filtered::<Entity, With<GameWorldTag>>() // our level/world contains this component
.iter(world)
.collect();
for entity in entities {
// Check the entity again in case it was despawned recursively
if world.get_entity(entity).is_some() {
world.entity_mut(entity).despawn_recursive();
}
}
}
pub fn unload_world(mut commands: Commands, gameworlds: Query<Entity, With<GameWorldTag>>) {
for e in gameworlds.iter() {
info!("--loading: despawn old world/level");
commands.entity(e).despawn_recursive();
}
}
// almost identical to setup_game, !!??
pub fn load_world(
mut commands: Commands,
game_assets: Res<GameAssets>,
// scenes: ResMut<Scene>,
) {
info!("--loading: loading world/level");
commands.spawn((
SceneBundle {
scene: game_assets.world.clone(),
..default()
},
bevy::prelude::Name::from("world"),
GameWorldTag,
InAppRunning,
));
}
pub fn load_saved_scene(mut commands: Commands, asset_server: Res<AssetServer>) {
commands.spawn((
DynamicSceneBundle {
// Scenes are loaded just like any other asset.
scene: asset_server.load(SCENE_FILE_PATH),
..default()
},
TempLoadedSceneMarker,
));
// commands.entity(world).add_child(child_scene);
info!("--loading: loaded saved scene");
}
pub fn process_loaded_scene(
loaded_scene: Query<(Entity, &Children), With<TempLoadedSceneMarker>>,
named_entities: Query<(Entity, &Name, &Parent)>, // FIXME: very inneficient
mut commands: Commands,
mut game_world: Query<(Entity, &Children), With<GameWorldTag>>,
saveables: Query<(Entity, &Name), With<Saveable>>,
asset_server: Res<AssetServer>,
) {
for (loaded_scene, children) in loaded_scene.iter() {
info!("--loading: post processing loaded scene");
let mut entities_to_load: Vec<(Entity, Name)> = vec![];
for loaded_entity in children.iter() {
if let Ok((source, name, _)) = named_entities.get(*loaded_entity) {
entities_to_load.push((source, name.clone()));
let mut found = false;
for (e, n, p) in named_entities.iter() {
// if we have an entity with the same name as in same file, overwrite
if e != source && name.as_str() == n.as_str() {
// println!("found entity with same name {} {} {:?} {:?}", name, n, source, e);
// source is entity within the newly loaded scene (source), e is within the existing world (destination)
info!("copying data from {:?} to {:?}", source, e);
commands.add(CloneEntity {
source: source,
destination: e,
});
// FIXME: issue with hierarchy & parenting, would be nice to be able to filter out components from CloneEntity
commands.entity(p.get()).add_child(e);
commands.entity(source).despawn_recursive();
found = true;
break;
}
}
// entity not found in the list of existing entities (ie entities that came as part of the level)
// so we spawn a new one
if !found {
info!("generating new entity");
let world = game_world.single_mut();
let world = world.1[0];
let new_entity = commands
.spawn((bevy::prelude::Name::from(name.clone()), SpawnHere))
.id();
commands.add(CloneEntity {
source: source,
destination: new_entity,
});
commands.entity(world).add_child(new_entity);
info!("copying data from {:?} to {:?}", source, new_entity);
}
}
}
commands.spawn(SaveablesToRemove(entities_to_load.clone()));
// if an entity is present in the world but NOT in the saved entities , it should be removed from the world
// ideally this should be run between spawning of the world/level AND spawn_placeholders
// remove the dynamic scene
info!("--loading: DESPAWNING LOADED SCENE");
commands.entity(loaded_scene).despawn_recursive();
asset_server.mark_unused_assets();
asset_server.free_unused_assets();
}
//for saveable in saveables.iter(){
// println!("SAVEABLE BEFORE {:?}", saveable)
//}
}
pub fn final_cleanup(
saveables_to_remove: Query<(Entity, &SaveablesToRemove)>,
mut commands: Commands,
saveables: Query<(Entity, &Name), With<Saveable>>,
mut next_app_state: ResMut<NextState<AppState>>,
mut next_game_state: ResMut<NextState<GameState>>,
) {
if let Ok((e, entities_to_load)) = saveables_to_remove.get_single() {
info!("saveables to remove {:?}", entities_to_load);
for (e, n) in saveables.iter() {
let mut found = false;
println!("SAVEABLE {}", n);
//let entities_to_load = saveables_to_remove.single();
for (en, na) in entities_to_load.0.iter() {
found = na.as_str() == n.as_str();
if found {
break;
}
}
if !found {
println!("REMOVING THIS ONE {}", n);
commands.entity(e).despawn_recursive();
}
}
// if there is a saveable that is NOT in the list of entities to load, despawn it
// despawn list
commands.entity(e).despawn_recursive();
info!("--loading: done, move to InGame state");
// next_app_state.set(AppState::AppRunning);
next_game_state.set(GameState::InGame);
}
}
fn process_loaded_scene_load_alt(
entities: Query<(Entity, &Children), With<TempLoadedSceneMarker>>,
named_entities: Query<(Entity, &Name, &Parent)>, // FIXME: very inneficient
mut commands: Commands,
) {
for (entity, children) in entities.iter() {
let mut entities_to_load: Vec<(Entity, Name)> = vec![];
for saved_source in children.iter() {
if let Ok((source, name, _)) = named_entities.get(*saved_source) {
println!("AAAAAAA {}", name);
entities_to_load.push((source, name.clone()));
}
}
println!("entities to load {:?}", entities_to_load);
commands.entity(entity).despawn_recursive();
}
}

View File

@ -0,0 +1,70 @@
pub mod saveable;
use bevy::asset::free_unused_assets_system;
use bevy_gltf_components::GltfComponentsSet;
pub use saveable::*;
pub mod saving;
pub use saving::*;
pub mod loading;
pub use loading::*;
use bevy::prelude::*;
use bevy::prelude::{App, IntoSystemConfigs, Plugin};
use bevy::utils::Uuid;
use bevy_gltf_blueprints::GltfBlueprintsSet;
#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)]
pub enum LoadingSet {
Load,
PostLoad,
}
pub struct SaveLoadPlugin;
impl Plugin for SaveLoadPlugin {
fn build(&self, app: &mut App) {
app
.register_type::<Uuid>()
.register_type::<Saveable>()
.add_event::<SaveRequest>()
.add_event::<LoadRequest>()
.configure_sets(
Update,
(LoadingSet::Load, LoadingSet::PostLoad)
.chain()
.before(GltfBlueprintsSet::Spawn)
.before(GltfComponentsSet::Injection)
)
.add_systems(PreUpdate, save_game.run_if(should_save))
.add_systems(Update,
(
load_prepare,
unload_world,
load_world,
load_saved_scene,
// process_loaded_scene
)
.chain()
.run_if(should_load) // .run_if(in_state(AppState::AppRunning))
.in_set(LoadingSet::Load)
)
.add_systems(Update,
(
process_loaded_scene,
apply_deferred,
final_cleanup,
apply_deferred,
free_unused_assets_system
)
.chain()
.in_set(LoadingSet::PostLoad)
)
// .add_systems(Update, bla)
;
}
}

View File

@ -0,0 +1,137 @@
const NEW_SCENE_FILE_PATH:&str="save.scn.ron";
use bevy::ecs::component::Components;
use bevy::ecs::entity::EntityMap;
use serde::{Deserialize, Serialize};
use std::io::Read;
use bevy::scene::serde::SceneDeserializer;
use ron::Deserializer;
use serde::de::DeserializeSeed;
#[derive(Debug, Deserialize)]
struct Components2;
#[derive(Debug, Deserialize)]
struct Fake {
resources: HashMap<u32, String>,
entities: HashMap<u32, Components2>
}
fn ron_test(){
let full_path = "/home/ckaos/projects/grappling-boom-bot/assets/save.ron";
match File::open(full_path) {
Ok(mut file) => {
let mut serialized_scene = Vec::new();
if let Err(why) = file.read_to_end(&mut serialized_scene) {
error!("file read failed: {why:?}");
}
match Deserializer::from_bytes(&serialized_scene) {
Ok(mut deserializer) => {
// deserializer.
let bla:Fake = ron::from_str("(
resources: {},
entities: {}
)").unwrap();
info!("testing {:?}", bla);
info!("YOYO DONE YO !")
}
Err(why) => {
error!("deserializer creation failed: {why:?}");
}
}
}
Err(why) => {
error!("load failed: {why:?}");
}
}
}
fn inject_component_data(world: &mut World, scene: DynamicScene){
let mut entity_map = EntityMap::default();
if let Err(why) = scene.write_to_world(world, &mut entity_map) {
panic!("world write failed: {why:?}");
}
println!("entity map {:?}", entity_map);
// TODO: EntityMap doesn't implement `iter()`
for old_entity in entity_map.keys() {
let entity = entity_map.get(old_entity).unwrap();
info!("entity update required: {old_entity:?} -> {entity:?}");
let e_mut = world
.entity_mut(entity);
}
info!("done loading scene");
}
fn post_load(world: &mut World){
let full_path = "/home/ckaos/projects/grappling-boom-bot/assets/save.ron";
match File::open(full_path) {
Ok(mut file) => {
let mut serialized_scene = Vec::new();
if let Err(why) = file.read_to_end(&mut serialized_scene) {
error!("file read failed: {why:?}");
}
match Deserializer::from_bytes(&serialized_scene) {
Ok(mut deserializer) => {
let result = SceneDeserializer {
type_registry: &world.resource::<AppTypeRegistry>().read(),
}
.deserialize(&mut deserializer);
info!("deserialize done");
match result {
Ok(scene) => {
info!("scene loaded");
// scene.write_to_world(world, entity_map)
// println!("{:?}", scene.entities);
inject_component_data(world, scene);
/*for dyn_ent in scene.entities.iter(){
// let mut query = scene.world.query::<(Entity, &Name, &GltfExtras, &Parent)>();
}*/
}
Err(why) => {
error!("deserialization failed: {why:?}");
}
}
}
Err(why) => {
error!("deserializer creation failed: {why:?}");
}
}
}
Err(why) => {
error!("load failed: {why:?}");
}
}
}
#[derive(Component, Reflect, Debug, Default )]
#[reflect(Component)]
pub struct Hackish;
/// unload saveables
fn unload_saveables(world: &mut World) {
let entities: Vec<Entity> = world
.query_filtered::<Entity, With<Saveable>>()// our level/world contains this component
.iter(world)
.collect();
for entity in entities {
// Check the entity again in case it was despawned recursively
if world.get_entity(entity).is_some() {
info!("despawning");
world.entity_mut(entity).despawn_recursive();
}
}
}

View File

@ -0,0 +1,14 @@
use bevy::prelude::*;
use bevy::utils::Uuid;
#[derive(Component, Reflect, Debug)]
#[reflect(Component)]
pub struct Saveable {
id: Uuid,
}
impl Default for Saveable {
fn default() -> Self {
Saveable { id: Uuid::new_v4() }
}
}

View File

@ -0,0 +1,87 @@
use bevy::pbr::{Clusters, VisiblePointLights};
use bevy::render::camera::CameraRenderGraph;
use bevy::render::view::VisibleEntities;
use bevy::tasks::IoTaskPool;
use bevy::{gltf::GltfExtras, prelude::*};
use bevy_rapier3d::prelude::RigidBody;
use std::fs::File;
use std::io::Write;
use crate::core::physics::Collider;
use crate::game::{Pickable, Player};
use super::Saveable;
const NEW_SCENE_FILE_PATH: &str = "save.scn.ron";
#[derive(Component, Event)]
pub struct SaveRequest {
pub path: String,
}
pub fn should_save(
// keycode: Res<Input<KeyCode>>,
save_requested_events: EventReader<SaveRequest>,
) -> bool {
return save_requested_events.len() > 0;
// return keycode.just_pressed(KeyCode::S)
}
pub fn save_game(
world: &mut World,
// save_requested_events: EventReader<SaveRequest>,
) {
info!("saving");
// world.
/*for bli in save_requested_events.iter(){
println!("SAAAAVE TO THISSSSS {:?}", bli.path)
}*/
let saveable_entities: Vec<Entity> = world
.query_filtered::<Entity, With<Saveable>>()
.iter(world)
.collect();
/*let static_entities: Vec<Entity> = world
.query_filtered::<Entity, Without<Saveable>>()
.iter(world)
.collect();*/
println!("saveable entities {}", saveable_entities.len());
let mut scene_builder = DynamicSceneBuilder::from_world(world);
scene_builder
.deny::<Children>()
.deny::<Parent>()
.deny::<ComputedVisibility>()
.deny::<Visibility>()
.deny::<GltfExtras>()
.deny::<GlobalTransform>()
.deny::<Collider>()
.deny::<RigidBody>()
.deny::<Saveable>()
// camera stuff
.deny::<Camera>()
.deny::<CameraRenderGraph>()
.deny::<Camera3d>()
.deny::<Clusters>()
.deny::<VisibleEntities>()
.deny::<VisiblePointLights>()
//.deny::<HasGizmoMarker>()
.extract_entities(saveable_entities.into_iter());
let dyn_scene = scene_builder.build();
let serialized_scene = dyn_scene
.serialize_ron(world.resource::<AppTypeRegistry>())
.unwrap();
#[cfg(not(target_arch = "wasm32"))]
IoTaskPool::get()
.spawn(async move {
// Write the scene RON data to file
File::create(format!("assets/scenes/{NEW_SCENE_FILE_PATH}"))
.and_then(|mut file| file.write(serialized_scene.as_bytes()))
.expect("Error while writing scene to file");
})
.detach();
}

View File

@ -0,0 +1,85 @@
use bevy::prelude::*;
use crate::{
assets::GameAssets,
state::{GameState, InAppRunning},
};
use bevy_gltf_blueprints::{BluePrintBundle, BlueprintName, GameWorldTag};
use bevy_rapier3d::prelude::Velocity;
use rand::Rng;
pub fn setup_game(
mut commands: Commands,
game_assets: Res<GameAssets>,
models: Res<Assets<bevy::gltf::Gltf>>,
mut next_game_state: ResMut<NextState<GameState>>,
) {
println!("setting up all stuff");
commands.insert_resource(AmbientLight {
color: Color::WHITE,
brightness: 0.2,
});
// here we actually spawn our game world/level
commands.spawn((
SceneBundle {
// note: because of this issue https://github.com/bevyengine/bevy/issues/10436, "world" is now a gltf file instead of a scene
scene: models
.get(game_assets.world.id())
.expect("main level should have been loaded")
.scenes[0]
.clone(),
..default()
},
bevy::prelude::Name::from("world"),
GameWorldTag,
InAppRunning,
));
next_game_state.set(GameState::InGame)
}
pub fn spawn_test(
keycode: Res<Input<KeyCode>>,
mut commands: Commands,
mut game_world: Query<(Entity, &Children), With<GameWorldTag>>,
) {
if keycode.just_pressed(KeyCode::T) {
let world = game_world.single_mut();
let world = world.1[0];
let mut rng = rand::thread_rng();
let range = 5.5;
let x: f32 = rng.gen_range(-range..range);
let y: f32 = rng.gen_range(-range..range);
let mut rng = rand::thread_rng();
let range = 0.8;
let vel_x: f32 = rng.gen_range(-range..range);
let vel_y: f32 = rng.gen_range(2.0..2.5);
let vel_z: f32 = rng.gen_range(-range..range);
let name_index: u64 = rng.gen();
let new_entity = commands
.spawn((
BluePrintBundle {
blueprint: BlueprintName("Health_Pickup".to_string()),
transform: TransformBundle::from_transform(Transform::from_xyz(x, 2.0, y)),
..Default::default()
},
bevy::prelude::Name::from(format!("test{}", name_index)),
// BlueprintName("Health_Pickup".to_string()),
// SpawnHere,
// TransformBundle::from_transform(Transform::from_xyz(x, 2.0, y)),
Velocity {
linvel: Vec3::new(vel_x, vel_y, vel_z),
angvel: Vec3::new(0.0, 0.0, 0.0),
},
))
.id();
commands.entity(world).add_child(new_entity);
}
}

View File

@ -0,0 +1,113 @@
use bevy::prelude::*;
use crate::state::{AppState, GameState, InMainMenu};
pub fn setup_main_menu(mut commands: Commands) {
commands.spawn((Camera2dBundle::default(), InMainMenu));
commands.spawn((
TextBundle::from_section(
"SOME GAME TITLE !!",
TextStyle {
//font: asset_server.load("fonts/FiraMono-Medium.ttf"),
font_size: 18.0,
color: Color::WHITE,
..Default::default()
},
)
.with_style(Style {
position_type: PositionType::Absolute,
top: Val::Px(100.0),
left: Val::Px(200.0),
..default()
}),
InMainMenu,
));
commands.spawn((
TextBundle::from_section(
"New Game (press Enter to start, press T once the game is started for demo spawning)",
TextStyle {
//font: asset_server.load("fonts/FiraMono-Medium.ttf"),
font_size: 18.0,
color: Color::WHITE,
..Default::default()
},
)
.with_style(Style {
position_type: PositionType::Absolute,
top: Val::Px(200.0),
left: Val::Px(200.0),
..default()
}),
InMainMenu,
));
/*
commands.spawn((
TextBundle::from_section(
"Load Game",
TextStyle {
//font: asset_server.load("fonts/FiraMono-Medium.ttf"),
font_size: 18.0,
color: Color::WHITE,
..Default::default()
},
)
.with_style(Style {
position_type: PositionType::Absolute,
top: Val::Px(250.0),
left: Val::Px(200.0),
..default()
}),
InMainMenu
));
commands.spawn((
TextBundle::from_section(
"Exit Game",
TextStyle {
//font: asset_server.load("fonts/FiraMono-Medium.ttf"),
font_size: 18.0,
color: Color::WHITE,
..Default::default()
},
)
.with_style(Style {
position_type: PositionType::Absolute,
top: Val::Px(300.0),
left: Val::Px(200.0),
..default()
}),
InMainMenu
));*/
}
pub fn teardown_main_menu(bla: Query<Entity, With<InMainMenu>>, mut commands: Commands) {
for bli in bla.iter() {
commands.entity(bli).despawn_recursive();
}
}
pub fn main_menu(
keycode: Res<Input<KeyCode>>,
mut next_app_state: ResMut<NextState<AppState>>,
// mut next_game_state: ResMut<NextState<GameState>>,
// mut save_requested_events: EventWriter<SaveRequest>,
// mut load_requested_events: EventWriter<LoadRequest>,
) {
if keycode.just_pressed(KeyCode::Return) {
next_app_state.set(AppState::AppLoading);
// next_game_state.set(GameState::None);
}
if keycode.just_pressed(KeyCode::L) {
next_app_state.set(AppState::AppLoading);
// load_requested_events.send(LoadRequest { path: "toto".into() })
}
if keycode.just_pressed(KeyCode::S) {
// save_requested_events.send(SaveRequest { path: "toto".into() })
}
}

View File

@ -0,0 +1,115 @@
pub mod in_game;
pub use in_game::*;
pub mod in_main_menu;
pub use in_main_menu::*;
pub mod picking;
pub use picking::*;
use crate::{
insert_dependant_component,
state::{AppState, GameState},
};
use bevy::prelude::*;
use bevy_rapier3d::prelude::*;
// this file is just for demo purposes, contains various types of components, systems etc
#[derive(Component, Reflect, Default, Debug)]
#[reflect(Component)]
pub enum SoundMaterial {
Metal,
Wood,
Rock,
Cloth,
Squishy,
#[default]
None,
}
#[derive(Component, Reflect, Default, Debug)]
#[reflect(Component)]
/// Demo marker component
pub struct Player;
#[derive(Component, Reflect, Default, Debug)]
#[reflect(Component)]
/// Demo component showing auto injection of components
pub struct ShouldBeWithPlayer;
#[derive(Component, Reflect, Default, Debug)]
#[reflect(Component)]
/// Demo marker component
pub struct Interactible;
fn player_move_demo(
keycode: Res<Input<KeyCode>>,
mut players: Query<&mut Transform, With<Player>>,
) {
let speed = 0.2;
if let Ok(mut player) = players.get_single_mut() {
if keycode.pressed(KeyCode::Left) {
player.translation.x += speed;
}
if keycode.pressed(KeyCode::Right) {
player.translation.x -= speed;
}
if keycode.pressed(KeyCode::Up) {
player.translation.z += speed;
}
if keycode.pressed(KeyCode::Down) {
player.translation.z -= speed;
}
}
}
// collision tests/debug
pub fn test_collision_events(
mut collision_events: EventReader<CollisionEvent>,
mut contact_force_events: EventReader<ContactForceEvent>,
) {
for collision_event in collision_events.read() {
println!("collision");
match collision_event {
CollisionEvent::Started(_entity1, _entity2, _) => {
println!("collision started")
}
CollisionEvent::Stopped(_entity1, _entity2, _) => {
println!("collision ended")
}
}
}
for contact_force_event in contact_force_events.read() {
println!("Received contact force event: {:?}", contact_force_event);
}
}
pub struct GamePlugin;
impl Plugin for GamePlugin {
fn build(&self, app: &mut App) {
app.add_plugins(PickingPlugin)
.register_type::<Interactible>()
.register_type::<SoundMaterial>()
.register_type::<Player>()
// little helper utility, to automatically inject components that are dependant on an other component
// ie, here an Entity with a Player component should also always have a ShouldBeWithPlayer component
// you get a warning if you use this, as I consider this to be stop-gap solution (usually you should have either a bundle, or directly define all needed components)
.add_systems(
Update,
(
// insert_dependant_component::<Player, ShouldBeWithPlayer>,
player_move_demo, //.run_if(in_state(AppState::Running)),
// test_collision_events
spawn_test,
)
.run_if(in_state(GameState::InGame)),
)
.add_systems(OnEnter(AppState::MenuRunning), setup_main_menu)
.add_systems(OnExit(AppState::MenuRunning), teardown_main_menu)
.add_systems(Update, (main_menu))
.add_systems(OnEnter(AppState::AppRunning), setup_game);
}
}

View File

@ -0,0 +1,34 @@
use super::Player;
use bevy::prelude::*;
use bevy_gltf_blueprints::GltfBlueprintsSet;
#[derive(Component, Reflect, Default, Debug)]
#[reflect(Component)]
pub struct Pickable;
// very simple, crude picking (as in picking up objects) implementation
pub fn picking(
players: Query<&GlobalTransform, With<Player>>,
pickables: Query<(Entity, &GlobalTransform), With<Pickable>>,
mut commands: Commands,
) {
for player_transforms in players.iter() {
for (pickable, pickable_transforms) in pickables.iter() {
let distance = player_transforms
.translation()
.distance(pickable_transforms.translation());
if distance < 2.5 {
commands.entity(pickable).despawn_recursive();
}
}
}
}
pub struct PickingPlugin;
impl Plugin for PickingPlugin {
fn build(&self, app: &mut App) {
app.register_type::<Pickable>()
.add_systems(Update, (picking.after(GltfBlueprintsSet::AfterSpawn),));
}
}

View File

@ -0,0 +1,33 @@
use bevy::prelude::*;
use bevy_editor_pls::prelude::*;
mod core;
use crate::core::*;
pub mod assets;
use assets::*;
pub mod state;
use state::*;
mod game;
use game::*;
mod test_components;
use test_components::*;
fn main() {
App::new()
.add_plugins((
DefaultPlugins.set(AssetPlugin::default()),
// editor
EditorPlugin::default(),
// our custom plugins
StatePlugin,
AssetsPlugin,
CorePlugin, // reusable plugins
GamePlugin, // specific to our game
ComponentsTestPlugin, // Showcases different type of components /structs
))
.run();
}

View File

@ -0,0 +1,54 @@
use bevy::app::AppExit;
use bevy::prelude::*;
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Default, States)]
pub enum AppState {
#[default]
CoreLoading,
MenuRunning,
AppLoading,
AppRunning,
AppEnding,
// FIXME: not sure
LoadingGame,
}
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Default, States)]
pub enum GameState {
#[default]
None,
InMenu,
InGame,
InGameOver,
InSaving,
InLoading,
}
// tag components for all entities within a certain state (for despawning them if needed) , FIXME: seems kinda hack-ish
#[derive(Component)]
pub struct InCoreLoading;
#[derive(Component, Default)]
pub struct InMenuRunning;
#[derive(Component)]
pub struct InAppLoading;
#[derive(Component)]
pub struct InAppRunning;
// components for tagging in game vs in game menu stuff
#[derive(Component, Default)]
pub struct InMainMenu;
#[derive(Component, Default)]
pub struct InMenu;
#[derive(Component, Default)]
pub struct InGame;
pub struct StatePlugin;
impl Plugin for StatePlugin {
fn build(&self, app: &mut App) {
app.add_state::<AppState>().add_state::<GameState>();
}
}

View File

@ -0,0 +1,93 @@
use bevy::prelude::*;
#[derive(Component, Reflect, Default, Debug)]
#[reflect(Component)]
struct Marker;
#[derive(Component, Reflect, Default, Debug)]
#[reflect(Component)]
struct Enemy;
#[derive(Component, Reflect, Default, Debug)]
#[reflect(Component)]
struct NestingTest;
#[derive(Component, Reflect, Default, Debug, Deref, DerefMut)]
#[reflect(Component)]
struct TuppleTestF32(f32);
#[derive(Component, Reflect, Default, Debug, Deref, DerefMut)]
#[reflect(Component)]
struct TuppleTestU64(u64);
#[derive(Component, Reflect, Default, Debug, Deref, DerefMut)]
#[reflect(Component)]
pub struct TuppleTestStr(String);
#[derive(Component, Reflect, Default, Debug)]
#[reflect(Component)]
struct TuppleTest2(f32, u64, String);
#[derive(Component, Reflect, Default, Debug)]
#[reflect(Component)]
struct TuppleTestBool(bool);
#[derive(Component, Reflect, Default, Debug)]
#[reflect(Component)]
struct TuppleVec2(Vec2);
#[derive(Component, Reflect, Default, Debug)]
#[reflect(Component)]
struct TuppleVec3(Vec3);
#[derive(Component, Reflect, Default, Debug)]
#[reflect(Component)]
struct TuppleVec(Vec<String>);
#[derive(Component, Reflect, Default, Debug)]
#[reflect(Component)]
struct TuppleTestColor(Color);
#[derive(Component, Reflect, Default, Debug)]
#[reflect(Component)]
struct BasicTest {
a: f32,
b: u64,
c: String,
}
#[derive(Component, Reflect, Default, Debug)]
#[reflect(Component)]
pub enum EnumTest {
Metal,
Wood,
Rock,
Cloth,
Squishy,
#[default]
None,
}
pub struct ComponentsTestPlugin;
impl Plugin for ComponentsTestPlugin {
fn build(&self, app: &mut App) {
app.register_type::<BasicTest>()
.register_type::<Marker>()
.register_type::<Enemy>()
.register_type::<NestingTest>()
.register_type::<TuppleTestF32>()
.register_type::<TuppleTestU64>()
.register_type::<TuppleTestStr>()
.register_type::<TuppleTestBool>()
.register_type::<TuppleTest2>()
.register_type::<TuppleVec2>()
.register_type::<TuppleVec3>()
.register_type::<EnumTest>()
.register_type::<TuppleTestColor>()
.register_type::<TuppleVec>()
.register_type::<Vec<String>>();
}
}

View File

@ -62,6 +62,9 @@ This [Blender addon](./)
- export blueprints: check this if you want to automatically export blueprints (default: True)
- blueprints path: the path to export blueprints to , relative to the main **export folder** (default: library)
- export nested blueprints: check this if you want to automatically export nested blueprints (collection instances inside blueprint collections)
as seperate blueprints (default: True)
please read dedicate section below for more information
- export materials library: check this if you want to automatically export material libraries (default: False)
please read the dedicated section below for more information
@ -109,6 +112,40 @@ You can enable this option to automatically replace all the **collection instanc
![exported collections](./docs/exported_collections.png)
#### Nested blueprints
To maximise reuse of meshes/components etc, you can also nest ***collections instances*** inside collections (as normally in Blender), but also export each nested Blueprint as a seperate blueprints.
> Don't forget to toggle the option in the exporter settings
- To make things clearer:
![nested-blueprints](./docs/nested_blueprints.png)
- **Player2** & **Enemy** both use the **Humanoid_cactus** nested collection/blueprint, so **Humanoid_cactus** gets exported as a Blueprint for re-use ...but
- **Humanoid_cactus** is also made up of a main mesh & two instances of **Hand** , so **Hand** gets exported as a Blueprint for re-use ...but
- **Hand** is also made up of a main mesh & three instances of **Finger**, so **Finger** gets exported as a Blueprint for re-use
- The exported models in this case end up being:
![nested_blueprints2](./docs/nested_blueprints2.png)
- Note how **Player2.glb** is tiny, because most of its data is actually sotred in **Humanoid_cactus.glb**
- **Enemy.glb** is slightly bigger because that blueprints contains additional meshes
- All the intermediary blueprints got exported automatically, and all instances have been replaced with "empties" (see explanation in the **Process section** ) to minimize file size
- Compare this to the output **WITHOUT** the nested export option:
![nested_blueprints3](./docs/nested_blueprints3.png)
- less blueprints as the sub collections that are not in use somewhere directly are not exported
- **Player2.glb** & **Enemy.glb** are significantly larger
TLDR: smaller, more reuseable blueprints which can share sub-parts with other entities !
### Materials
You can enable this option to automatically generate a **material library** file that combines all the materials in use in your blueprints.

View File

@ -1,7 +1,7 @@
bl_info = {
"name": "gltf_auto_export",
"author": "kaosigh",
"version": (0, 7, 1),
"version": (0, 8, 0),
"blender": (3, 4, 0),
"location": "File > Import-Export",
"description": "glTF/glb auto-export",
@ -43,6 +43,7 @@ bpy.context.window_manager['changed_objects_per_scene'] = {}
bpy.context.window_manager['previous_params'] = {}
bpy.context.window_manager['__gltf_auto_export_initialized'] = False
bpy.context.window_manager['__gltf_auto_export_gltf_params_changed'] = False
bpy.context.window_manager['__gltf_auto_export_saving'] = False
######################################################
""" there are two places where we load settings for auto_export from:
@ -60,6 +61,9 @@ def deps_update_handler(scene, depsgraph):
print("-------------")
changed = scene.name or ""
# only deal with changes if we are no in the mids of saving/exporting
#if not bpy.context.window_manager['__gltf_auto_export_saving']:
# depsgraph = bpy.context.evaluated_depsgraph_get()
if not 'changed_objects_per_scene' in bpy.context.window_manager:
bpy.context.window_manager['changed_objects_per_scene'] = {}
@ -71,6 +75,7 @@ def deps_update_handler(scene, depsgraph):
if isinstance(obj.id, bpy.types.Object):
# get the actual object
object = bpy.data.objects[obj.id.name]
print("changed object", obj.id.name)
bpy.context.window_manager['changed_objects_per_scene'][scene.name][obj.id.name] = object
bpy.context.window_manager.changedScene = changed
@ -79,10 +84,16 @@ def deps_update_handler(scene, depsgraph):
def save_handler(dummy):
print("-------------")
print("saved", bpy.data.filepath)
# mark saving as in progress, this is needed to ignore any changes from the depsgraph done during saving
# bpy.context.window_manager['__gltf_auto_export_saving'] = True
if not 'changed_objects_per_scene' in bpy.context.window_manager:
bpy.context.window_manager['changed_objects_per_scene'] = {}
changes_per_scene = bpy.context.window_manager['changed_objects_per_scene']
if not 'previous_params' in bpy.context.window_manager:
bpy.context.window_manager['previous_params'] = {}
#determine changed parameters
addon_prefs = bpy.context.preferences.addons["gltf_auto_export"].preferences
@ -113,6 +124,11 @@ def save_handler(dummy):
# reset whether there have been changed objects since the last save
bpy.context.window_manager['changed_objects_per_scene'] = {}
# all our logic is done, mark this as done
#bpy.context.window_manager['__gltf_auto_export_saving'] = False
print("EXPORT DONE")
def get_changedScene(self):
return self["changedScene"]
@ -153,6 +169,7 @@ def register():
bpy.app.handlers.depsgraph_update_post.append(deps_update_handler)
bpy.app.handlers.save_post.append(save_handler)
bpy.types.WindowManager.changedScene = bpy.props.StringProperty(get=get_changedScene, set=set_changedScene)
bpy.types.WindowManager.exportedCollections = bpy.props.CollectionProperty(type=CollectionsToExport)

View File

@ -48,8 +48,6 @@ def auto_export(changes_per_scene, changed_export_parameters):
print("error setting preferences from saved settings", error)
bpy.context.window_manager['__gltf_auto_export_initialized'] = True
# have the export parameters (not auto export, just gltf export) have changed: if yes (for example switch from glb to gltf, compression or not, animations or not etc), we need to re-export everything
print ("changed_export_parameters", changed_export_parameters)
try:
@ -63,6 +61,7 @@ def auto_export(changes_per_scene, changed_export_parameters):
export_materials_library = getattr(addon_prefs,"export_materials_library")
export_scene_settings = getattr(addon_prefs,"export_scene_settings")
export_nested_blueprints = getattr(addon_prefs,"export_nested_blueprints")
[main_scene_names, level_scenes, library_scene_names, library_scenes] = get_scenes(addon_prefs)
@ -74,11 +73,12 @@ def auto_export(changes_per_scene, changed_export_parameters):
# inject/ update scene components
upsert_scene_components(bpy.context.scene, world = bpy.context.scene.world)
# export everything everytime
# export
if export_blueprints:
print("EXPORTING")
# get a list of all collections actually in use
collections = get_exportable_collections(level_scenes, library_scenes)
scan_nested_collections = export_nested_blueprints
(collections, blueprint_hierarchy) = get_exportable_collections(level_scenes, library_scenes, scan_nested_collections)
# first check if all collections have already been exported before (if this is the first time the exporter is run
# in your current Blender session for example)
export_blueprints_path = os.path.join(folder_path, export_output_folder, getattr(addon_prefs,"export_blueprints_path")) if getattr(addon_prefs,"export_blueprints_path") != '' else folder_path
@ -96,10 +96,10 @@ def auto_export(changes_per_scene, changed_export_parameters):
object_collections = list(obj.users_collection)
object_collection_names = list(map(lambda collection: collection.name, object_collections))
if len(object_collection_names) > 1:
print("ERRROR, objects in multiple collections not supported")
print("ERRROR for",obj_name,"objects in multiple collections not supported")
else:
object_collection_name = object_collection_names[0] if len(object_collection_names) > 0 else None
print(" object ", obj, object_collection_name)
#print(" object ", obj, object_collection_name)
if object_collection_name in collections:
changed_collections.append(object_collection_name)
@ -111,6 +111,7 @@ def auto_export(changes_per_scene, changed_export_parameters):
# collections that do not come from a library should not be exported as seperate blueprints
# FIMXE: logic is erroneous, needs to be changed
library_collections = [name for sublist in collections_per_scene.values() for name in sublist]
collections_to_export = list(set(collections_to_export).intersection(set(library_collections)))
@ -151,7 +152,7 @@ def auto_export(changes_per_scene, changed_export_parameters):
print(" exporting collections from scene:", scene_name)
print(" collections to export", collections_to_export)
library_scene = bpy.data.scenes[scene_name]
export_blueprints_from_collections(collections_to_export, library_scene, folder_path, addon_prefs)
export_blueprints_from_collections(collections_to_export, library_scene, folder_path, addon_prefs, blueprint_hierarchy, collections)
# reset current scene from backup
bpy.context.window.scene = old_current_scene

View File

@ -0,0 +1,113 @@
import bpy
from .helpers_collections import (find_layer_collection_recursive)
from .helpers import (make_empty3, traverse_tree)
def generate_blueprint_hollow_scene(blueprint_collection, library_collections):
temp_scene = bpy.data.scenes.new(name="temp_scene_"+blueprint_collection.name)
temp_scene_root_collection = temp_scene.collection
# we set our active scene to be this one : this is needed otherwise the stand-in empties get generated in the wrong scene
bpy.context.window.scene = temp_scene
found = find_layer_collection_recursive(temp_scene_root_collection, bpy.context.view_layer.layer_collection)
if found:
# once it's found, set the active layer collection to the one we found
bpy.context.view_layer.active_layer_collection = found
original_names = []
# TODO also add the handling for "template" flags, so that instead of creating empties we link the data from the sub collection INTO the parent collection
# copies the contents of a collection into another one while replacing blueprint instances with empties
def copy_hollowed_collection_into(source_collection, destination_collection):
for object in source_collection.objects:
#FIXME: enum would be better
""" combine mode can be
- 'Split' (default): replace with an empty, creating links to sub blueprints
- 'Embed' : treat it as an embeded object and do not replace it with an empty
- 'Inject': inject components from sub collection instances into the curent object
"""
combineMode = 'Split' if not 'Combine' in object else object['Combine']
# TODO: implement
# print("COMBINE MODE", combineMode)
# embed = 'Embed' in object and object['Embed'] == True # if the object has the "embed" flag set to true, treat it as an embeded object and do not replace it with an empty
# merge = 'Merge' in object and object['Merge'] == True
if object.instance_type == 'COLLECTION' and (object.instance_collection.name in library_collections):
# if we have combine_mode set to "merge", we take all the custom attributed of the nested (1 level only ! unless we use 'deepMerge') custom attributes and copy them to this level
"""TODO: implement later
if merge:
foo = get_nested_components(object)
print("nested components", foo)
pass
else:
"""
collection_name = object.instance_collection.name
original_name = object.name
original_names.append(original_name)
object.name = original_name + "____bak"
empty_obj = make_empty3(original_name, object.location, object.rotation_euler, object.scale, destination_collection)
"""we inject the collection/blueprint name, as a component called 'BlueprintName', but we only do this in the empty, not the original object"""
empty_obj['BlueprintName'] = '"'+collection_name+'"'
empty_obj['SpawnHere'] = ''
for k, v in object.items():
empty_obj[k] = v
else:
destination_collection.objects.link(object)
# for every sub-collection of the source, copy its content into a new sub-collection of the destination
for collection in source_collection.children:
copy_collection = bpy.data.collections.new(collection.name + "____collection_export")
copy_hollowed_collection_into(collection, copy_collection)
destination_collection.children.link(copy_collection)
copy_hollowed_collection_into(blueprint_collection, temp_scene_root_collection)
return (temp_scene, original_names)
# clear & remove "hollow scene"
def clear_blueprint_hollow_scene(temp_scene, original_collection, original_names):
def restore_original_names(collection):
for object in collection.objects:
if object.instance_type == 'COLLECTION':
if object.name.endswith("____bak"):
object.name = object.name.replace("____bak", "")
for child_collection in collection.children:
restore_original_names(child_collection)
restore_original_names(original_collection)
# remove empties (only needed when we go via ops ????)
root_collection = temp_scene.collection
scene_objects = [o for o in root_collection.objects]
for object in scene_objects:
if object.type == 'EMPTY':
if hasattr(object, "SpawnHere"):
bpy.data.objects.remove(object, do_unlink=True)
else:
bpy.context.scene.collection.objects.unlink(object)
#bpy.data.objects.remove(object, do_unlink=True)
bpy.data.scenes.remove(temp_scene)
# TODO : add a flag to also search of deeply nested components
def get_nested_components(object):
if object.instance_type == 'COLLECTION':
collection_name = object.instance_collection.name
collection = bpy.data.collections[collection_name]
all_objects = collection.all_objects
result = []
for object in all_objects:
components = dict(object)
if len(components.keys()) > 0:
result += [(object, components)]
return result
return []
#for collection in traverse_tree(collection):
# for object in collection.all_objects

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

@ -37,7 +37,7 @@ def make_empty3(name, location, rotation, scale, collection):
bpy.context.view_layer.objects.active = original_active_object
return empty_obj
# traverse all collections
def traverse_tree(t):
yield t
for child in t.children:
@ -48,7 +48,7 @@ def check_if_blueprints_exist(collections, folder_path, extension):
not_found_blueprints = []
for collection_name in collections:
gltf_output_path = os.path.join(folder_path, collection_name + extension)
print("gltf_output_path", gltf_output_path)
# print("gltf_output_path", gltf_output_path)
found = os.path.exists(gltf_output_path) and os.path.isfile(gltf_output_path)
if not found:
not_found_blueprints.append(collection_name)

View File

@ -2,6 +2,7 @@ import bpy
from .helpers import traverse_tree
# returns the list of the collections in use for a given scene
# FIXME: this should also look into sub collections
def get_used_collections(scene):
root_collection = scene.collection
@ -11,13 +12,7 @@ def get_used_collections(scene):
for object in scene_objects:
#print("object ", object)
if object.instance_type == 'COLLECTION':
#print("THIS OBJECT IS A COLLECTION")
# print("instance_type" ,object.instance_type)
collection_name = object.instance_collection.name
#print("instance collection", object.instance_collection.name)
#object.instance_collection.users_scene
# del object['blueprint']
# object['BlueprintName'] = '"'+collection_name+'"'
if not collection_name in collection_names:
collection_names.add(collection_name)
used_collections.append(object.instance_collection)
@ -37,16 +32,87 @@ def get_marked_collections(scene):
collection_names.append(collection.name)
return (collection_names, marked_collections)
# gets all collections within collections that might also be relevant
def get_sub_collections(collections, parent, children_per_collection):
collection_names = set()
used_collections = []
for root_collection in collections:
node = Node(name=root_collection.name, parent=parent)
parent.children.append(node)
#print("root collection", root_collection.name)
for collection in traverse_tree(root_collection): # TODO: filter out COLLECTIONS that have the flatten flag (unlike the flatten flag on colleciton instances themselves)
node_name = collection.name
children_per_collection[node_name] = []
#print(" scanning", collection.name)
for object in collection.objects:
#print("FLATTEN", object.name, 'Flatten' in object)
if object.instance_type == 'COLLECTION' : # and not 'Flatten' in object:
collection_name = object.instance_collection.name
(sub_names, sub_collections) = get_sub_collections([object.instance_collection], node, children_per_collection)
if len(list(sub_names)) > 0:
children_per_collection[node_name] += (list(sub_names))
#print(" found sub collection in use", object.name, object.instance_collection)
if not collection_name in collection_names:
collection_names.add(collection_name)
used_collections.append(object.instance_collection)
collection_names.update(sub_names)
#for sub in traverse_tree(root_collection):
return (collection_names, used_collections)
# FIXME: get rid of this, ugh
def flatten_collection_tree(node, children_per_collection):
children_per_collection[node.name] = []
for child in node.children:
if not node.name in children_per_collection[node.name]:
children_per_collection[node.name].append(child.name)
flatten_collection_tree(child, children_per_collection)
children_per_collection[node.name] = list(set( children_per_collection[node.name]))
class Node :
def __init__(self, name="", parent=None):
self.name = name
self.children = []
self.changed = False
self.parent = parent
return
def __str__(self):
children = list(map(lambda child: str(child), self.children))
return "name: " +self.name + ", children:" + str(children)
# get exportable collections from lists of mains scenes and lists of library scenes
def get_exportable_collections(main_scenes, library_scenes):
def get_exportable_collections(main_scenes, library_scenes, scan_nested_collections):
all_collections = []
all_collection_names = []
root_node = Node()
root_node.name = "root"
children_per_collection = {}
for main_scene in main_scenes:
(collection_names, _) = get_used_collections(main_scene)
all_collections = all_collections + list(collection_names)
(collection_names, collections) = get_used_collections(main_scene)
all_collection_names = all_collection_names + list(collection_names)
all_collections = all_collections + collections
for library_scene in library_scenes:
marked_collections = get_marked_collections(library_scene)
all_collections = all_collections + marked_collections[0]
return all_collections
all_collection_names = all_collection_names + marked_collections[0]
all_collections = all_collections + marked_collections[1]
if scan_nested_collections:
(collection_names, collections) = get_sub_collections(all_collections, root_node, children_per_collection)
all_collection_names = all_collection_names + list(collection_names)
children_per_collection = {}
flatten_collection_tree(root_node, children_per_collection)
#print("ROOT NODE", children_per_collection) #
return (all_collection_names, children_per_collection)
def get_collections_per_scene(collection_names, library_scenes):
collections_per_scene = {}
@ -84,7 +150,6 @@ def find_layer_collection_recursive(find, col):
return c
return None
#Recursivly transverse layer_collection for a particular name
def recurLayerCollection(layerColl, collName):
found = None

View File

@ -1,9 +1,12 @@
import os
import bpy
from .preferences import (AutoExportGltfPreferenceNames)
from .helpers_scenes import (generate_hollow_scene, clear_hollow_scene)
from .helpers_collections import (recurLayerCollection)
from .blueprints import clear_blueprint_hollow_scene, generate_blueprint_hollow_scene
from .helpers import (traverse_tree)
######################################################
#### Export logic #####
@ -66,11 +69,13 @@ def get_source_scene(collection_name, library_scenes):
return match
# export collections: all the collections that have an instance in the main scene AND any marked collections, even if they do not have instances
def export_collections(collections, folder_path, library_scene, addon_prefs, gltf_export_preferences):
def export_collections(collections, folder_path, library_scene, addon_prefs, gltf_export_preferences, blueprint_hierarchy, library_collections):
# set active scene to be the library scene (hack for now)
bpy.context.window.scene = library_scene
# save current active collection
active_collection = bpy.context.view_layer.active_layer_collection
export_nested_blueprints = getattr(addon_prefs,"export_nested_blueprints")
export_materials_library = getattr(addon_prefs,"export_materials_library")
for collection_name in collections:
print("exporting collection", collection_name)
@ -83,17 +88,32 @@ def export_collections(collections, folder_path, library_scene, addon_prefs, glt
export_settings = { **gltf_export_preferences, 'use_active_scene': True, 'use_active_collection': True, 'use_active_collection_with_nested':True}
# if we are using the material library option, do not export materials, use placeholder instead
export_materials_library = getattr(addon_prefs,"export_materials_library")
if export_materials_library:
export_settings['export_materials'] = 'PLACEHOLDER'
export_gltf(gltf_output_path, export_settings)
#if relevant we replace sub collections instances with placeholders too
# this is not needed if a collection/blueprint does not have sub blueprints
if collection_name in blueprint_hierarchy and len(blueprint_hierarchy[collection_name]) > 0 and export_nested_blueprints :
print("generate hollow scene for nested blueprints", library_collections)
backup = bpy.context.window.scene
collection = bpy.data.collections[collection_name]
(hollow_scene, object_names) = generate_blueprint_hollow_scene(collection, library_collections)
export_gltf(gltf_output_path, export_settings)
clear_blueprint_hollow_scene(hollow_scene, collection, object_names)
bpy.context.window.scene = backup
else:
print("NORMAL")
export_gltf(gltf_output_path, export_settings)
# reset active collection to the one we save before
bpy.context.view_layer.active_layer_collection = active_collection
def export_blueprints_from_collections(collections, library_scene, folder_path, addon_prefs):
def export_blueprints_from_collections(collections, library_scene, folder_path, addon_prefs, blueprint_hierarchy, library_collections):
export_output_folder = getattr(addon_prefs,"export_output_folder")
gltf_export_preferences = generate_gltf_export_preferences(addon_prefs)
export_blueprints_path = os.path.join(folder_path, export_output_folder, getattr(addon_prefs,"export_blueprints_path")) if getattr(addon_prefs,"export_blueprints_path") != '' else folder_path
@ -102,7 +122,7 @@ def export_blueprints_from_collections(collections, library_scene, folder_path,
#print("LIBRARY EXPORT", export_blueprints_path )
try:
export_collections(collections, export_blueprints_path, library_scene, addon_prefs, gltf_export_preferences)
export_collections(collections, export_blueprints_path, library_scene, addon_prefs, gltf_export_preferences, blueprint_hierarchy, library_collections)
except Exception as error:
print("failed to export collections to gltf: ", error)
# TODO : rethrow

View File

@ -15,14 +15,15 @@ AutoExportGltfPreferenceNames = [
'export_main_scene_name',
'export_output_folder',
'export_library_scene_name',
'export_blueprints',
'export_blueprints_path',
'export_scene_settings',
'export_nested_blueprints',
'export_materials_library',
'export_materials_path',
'export_scene_settings',
'main_scenes',
'library_scenes',
@ -75,6 +76,12 @@ class AutoExportGltfAddonPreferences(AddonPreferences):
default='library'
)
export_nested_blueprints: BoolProperty(
name='Export nested Blueprints',
description='Collection instances within Collections are turned into blueprint instances',
default=True
)
export_materials_library: BoolProperty(
name='Export materials library',
description='remove materials from blueprints and use the material library instead',

View File

@ -130,7 +130,8 @@ class AutoExportGLTF(Operator, AutoExportGltfAddonPreferences, ExportHelper):
[main_scene_names, level_scenes, library_scene_names, library_scenes]=get_scenes(addon_prefs)
collections = get_exportable_collections(level_scenes, library_scenes)
scan_nested_collections = bpy.context.preferences.addons["gltf_auto_export"].preferences.export_nested_blueprints
(collections, _) = get_exportable_collections(level_scenes, library_scenes, scan_nested_collections)
try:
# we save this list of collections in the context
@ -275,6 +276,15 @@ class GLTF_PT_auto_export_blueprints(bpy.types.Panel):
return operator.bl_idname == "EXPORT_SCENES_OT_auto_gltf" #"EXPORT_SCENE_OT_gltf"
def draw_header(self, context):
layout = self.layout
sfile = context.space_data
operator = sfile.active_operator
layout.prop(operator, "export_blueprints", text="")
#self.layout.prop(operator, "auto_export", text="")
def draw(self, context):
layout = self.layout
layout.use_property_split = True
@ -283,8 +293,10 @@ class GLTF_PT_auto_export_blueprints(bpy.types.Panel):
sfile = context.space_data
operator = sfile.active_operator
layout.prop(operator, "export_blueprints")
layout.active = operator.export_blueprints
layout.prop(operator, "export_blueprints_path")
layout.prop(operator, "export_nested_blueprints")
# materials
layout.prop(operator, "export_materials_library")