feat(Gltf auto export): multiple blend files workflow, combine modes, bugfixes and more ! (#83)
* feat(multiple blend files): added example of multi blend file workflow * feat(tools/auto_export): * added collection_instances_combine_mode superseeds & replaces "export nested blueprints", with a lot more flexibilty * added preferences & ui settings * added (optional) use of marked assets as replacement for "autoExport" collection flag * added correct cleaning up of temporary sub_collections * feat(tools/auto_export): change detection added for material changes * feat(tools/auto_export): blueprints now also use the "combine" settings for nested collections * feat(tools/auto_export): added correct cleanup of data when removing hollow scenes, so that there is no orphan data left behind * feat(tools/auto_export): changes to nested collections are now triggering change detection of root (exportable collections) * feat(tools/auto_export): now actually useful tracebacks using traceback.format_exc() * various minor fixes, cleanups & UI description improvements * docs(README): updated & fleshed out docs * closes #87 * closes #86 * closes #85 * closes #79 * closes #61 * closes #68 * closes #41
12
Cargo.lock
generated
@ -857,6 +857,18 @@ dependencies = [
|
||||
"rand",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bevy_gltf_blueprints_multiple_levels_multiple_blendfiles"
|
||||
version = "0.3.0"
|
||||
dependencies = [
|
||||
"bevy",
|
||||
"bevy_asset_loader",
|
||||
"bevy_editor_pls",
|
||||
"bevy_gltf_blueprints",
|
||||
"bevy_rapier3d",
|
||||
"rand",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bevy_gltf_blueprints_nested_blueprints_example"
|
||||
version = "0.3.0"
|
||||
|
@ -9,8 +9,8 @@ members = [
|
||||
"examples/bevy_gltf_blueprints/nested_blueprints/",
|
||||
"examples/bevy_gltf_blueprints/animation/",
|
||||
"examples/bevy_gltf_blueprints/multiple_levels/",
|
||||
"examples/bevy_gltf_blueprints/multiple_levels_multiple_blendfiles",
|
||||
"examples/bevy_gltf_blueprints/materials/"
|
||||
|
||||
]
|
||||
resolver = "2"
|
||||
|
||||
|
@ -283,6 +283,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/nested_blueprints
|
||||
|
||||
https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/examples/multiple_levels_multiple_blendfiles
|
||||
|
||||
|
||||
## Compatible Bevy versions
|
||||
|
||||
|
@ -1,485 +0,0 @@
|
||||
(
|
||||
resources: {},
|
||||
entities: {
|
||||
20: (
|
||||
components: {
|
||||
"bevy_render::camera::projection::Projection": Perspective((
|
||||
fov: 0.3995965,
|
||||
aspect_ratio: 1.7777778,
|
||||
near: 0.1,
|
||||
far: 100.0,
|
||||
)),
|
||||
"bevy_render::primitives::Frustum": (),
|
||||
"bevy_transform::components::transform::Transform": (
|
||||
translation: (
|
||||
x: 34.821884,
|
||||
y: 49.024857,
|
||||
z: -36.79615,
|
||||
),
|
||||
rotation: (-0.1694689, 0.82838506, 0.40884802, 0.3433684),
|
||||
scale: (
|
||||
x: 1.0,
|
||||
y: 1.0,
|
||||
z: 1.0,
|
||||
),
|
||||
),
|
||||
"bevy_core_pipeline::tonemapping::Tonemapping": BlenderFilmic,
|
||||
"bevy_core_pipeline::tonemapping::DebandDither": Enabled,
|
||||
"bevy_render::view::ColorGrading": (
|
||||
exposure: 0.0,
|
||||
gamma: 1.0,
|
||||
pre_saturation: 1.0,
|
||||
post_saturation: 1.0,
|
||||
),
|
||||
"bevy_core::name::Name": (
|
||||
hash: 17702508670109176045,
|
||||
name: "Camera",
|
||||
),
|
||||
"advanced::core::camera::camera_tracking::CameraTrackingOffset": ((
|
||||
x: 26.0,
|
||||
y: 48.0,
|
||||
z: -26.0,
|
||||
)),
|
||||
"bevy_pbr::light::ClusterConfig": FixedZ(
|
||||
total: 4096,
|
||||
z_slices: 24,
|
||||
z_config: (
|
||||
first_slice_depth: 5.0,
|
||||
far_z_mode: MaxLightRange,
|
||||
),
|
||||
dynamic_resizing: true,
|
||||
),
|
||||
"bevy_core_pipeline::bloom::settings::BloomSettings": (
|
||||
intensity: 0.01,
|
||||
low_frequency_boost: 0.7,
|
||||
low_frequency_boost_curvature: 0.95,
|
||||
high_pass_frequency: 1.0,
|
||||
prefilter_settings: (
|
||||
threshold: 0.0,
|
||||
threshold_softness: 0.0,
|
||||
),
|
||||
composite_mode: Additive,
|
||||
),
|
||||
},
|
||||
),
|
||||
34: (
|
||||
components: {
|
||||
"bevy_transform::components::transform::Transform": (
|
||||
translation: (
|
||||
x: 4.697565,
|
||||
y: 1.5983224,
|
||||
z: 8.962274,
|
||||
),
|
||||
rotation: (0.000000000000000031724054, -0.00000000000000000000647681, -0.000013119204, 1.0),
|
||||
scale: (
|
||||
x: 1.0,
|
||||
y: 1.0,
|
||||
z: 1.0,
|
||||
),
|
||||
),
|
||||
"bevy_core::name::Name": (
|
||||
hash: 9837288155836662016,
|
||||
name: "Health_Pickup.001",
|
||||
),
|
||||
"bevy_gltf_blueprints::spawn_from_blueprints::BlueprintName": ("Health_Pickup"),
|
||||
"advanced::game::picking::Pickable": (),
|
||||
},
|
||||
),
|
||||
54: (
|
||||
components: {
|
||||
"bevy_transform::components::transform::Transform": (
|
||||
translation: (
|
||||
x: 8.799996,
|
||||
y: 1.02484,
|
||||
z: -10.799994,
|
||||
),
|
||||
rotation: (0.0, 0.0, 0.0, 1.0),
|
||||
scale: (
|
||||
x: 1.0,
|
||||
y: 1.0,
|
||||
z: 1.0,
|
||||
),
|
||||
),
|
||||
"bevy_core::name::Name": (
|
||||
hash: 17978181434632022651,
|
||||
name: "Player",
|
||||
),
|
||||
"advanced::core::camera::camera_tracking::CameraTrackable": (),
|
||||
"bevy_gltf_blueprints::spawn_from_blueprints::BlueprintName": ("Player"),
|
||||
"advanced::game::Player": (),
|
||||
"advanced::game::SoundMaterial": Wood,
|
||||
},
|
||||
),
|
||||
60: (
|
||||
components: {
|
||||
"bevy_transform::components::transform::Transform": (
|
||||
translation: (
|
||||
x: 3.6351967,
|
||||
y: 1.7298106,
|
||||
z: -7.313273,
|
||||
),
|
||||
rotation: (0.0, 0.0, 0.0, 1.0),
|
||||
scale: (
|
||||
x: 1.0,
|
||||
y: 1.0,
|
||||
z: 1.0,
|
||||
),
|
||||
),
|
||||
"bevy_core::name::Name": (
|
||||
hash: 7225506896223411979,
|
||||
name: "MagicTeapot.001",
|
||||
),
|
||||
"bevy_gltf_blueprints::spawn_from_blueprints::BlueprintName": ("MagicTeapot"),
|
||||
},
|
||||
),
|
||||
64: (
|
||||
components: {
|
||||
"bevy_transform::components::transform::Transform": (
|
||||
translation: (
|
||||
x: -4.6068983,
|
||||
y: 1.5983224,
|
||||
z: -10.579347,
|
||||
),
|
||||
rotation: (0.000000000000000031724054, 0.00000000000000000000647681, 0.000013119204, 1.0),
|
||||
scale: (
|
||||
x: 1.0,
|
||||
y: 1.0,
|
||||
z: 1.0,
|
||||
),
|
||||
),
|
||||
"bevy_core::name::Name": (
|
||||
hash: 3089896164553476909,
|
||||
name: "Health_Pickup.002",
|
||||
),
|
||||
"bevy_gltf_blueprints::spawn_from_blueprints::BlueprintName": ("Health_Pickup"),
|
||||
"advanced::game::picking::Pickable": (),
|
||||
},
|
||||
),
|
||||
72: (
|
||||
components: {
|
||||
"bevy_transform::components::transform::Transform": (
|
||||
translation: (
|
||||
x: -11.560788,
|
||||
y: 0.0,
|
||||
z: 7.6554174,
|
||||
),
|
||||
rotation: (0.0, 0.0, 0.0, 1.0),
|
||||
scale: (
|
||||
x: 1.0,
|
||||
y: 1.0,
|
||||
z: 1.0,
|
||||
),
|
||||
),
|
||||
"bevy_core::name::Name": (
|
||||
hash: 16961132108296874979,
|
||||
name: "Container.001",
|
||||
),
|
||||
"bevy_gltf_blueprints::spawn_from_blueprints::BlueprintName": ("Container"),
|
||||
"advanced::game::picking::Pickable": (),
|
||||
},
|
||||
),
|
||||
80: (
|
||||
components: {
|
||||
"bevy_transform::components::transform::Transform": (
|
||||
translation: (
|
||||
x: -21.397858,
|
||||
y: 0.3833189,
|
||||
z: -0.32418346,
|
||||
),
|
||||
rotation: (0.0, 0.0, 0.0, 1.0),
|
||||
scale: (
|
||||
x: 1.0,
|
||||
y: 1.0,
|
||||
z: 1.0,
|
||||
),
|
||||
),
|
||||
"bevy_core::name::Name": (
|
||||
hash: 5104740624378885265,
|
||||
name: "Container.002",
|
||||
),
|
||||
"bevy_gltf_blueprints::spawn_from_blueprints::BlueprintName": ("Container"),
|
||||
"advanced::game::picking::Pickable": (),
|
||||
},
|
||||
),
|
||||
82: (
|
||||
components: {
|
||||
"bevy_transform::components::transform::Transform": (
|
||||
translation: (
|
||||
x: 2.9156065,
|
||||
y: 1.4984571,
|
||||
z: 2.1909573,
|
||||
),
|
||||
rotation: (0.058853183, 0.0726243, 0.2048649, 0.97431636),
|
||||
scale: (
|
||||
x: 1.0,
|
||||
y: 1.0,
|
||||
z: 1.0,
|
||||
),
|
||||
),
|
||||
"bevy_core::name::Name": (
|
||||
hash: 107557640935939866,
|
||||
name: "test5159735758431545549",
|
||||
),
|
||||
"bevy_gltf_blueprints::spawn_from_blueprints::BlueprintName": ("Health_Pickup"),
|
||||
"advanced::game::picking::Pickable": (),
|
||||
"bevy_rapier3d::dynamics::rigid_body::Velocity": (
|
||||
linvel: (
|
||||
x: -1.2580805,
|
||||
y: -0.39687577,
|
||||
z: 0.4816798,
|
||||
),
|
||||
angvel: (
|
||||
x: 0.2979751,
|
||||
y: 0.07926611,
|
||||
z: 0.8434645,
|
||||
),
|
||||
),
|
||||
},
|
||||
),
|
||||
86: (
|
||||
components: {
|
||||
"bevy_transform::components::transform::Transform": (
|
||||
translation: (
|
||||
x: 0.26087752,
|
||||
y: 1.5525806,
|
||||
z: 1.5980839,
|
||||
),
|
||||
rotation: (0.059497803, -0.0000018232388, 0.13145457, 0.9895351),
|
||||
scale: (
|
||||
x: 1.0,
|
||||
y: 1.0,
|
||||
z: 1.0,
|
||||
),
|
||||
),
|
||||
"bevy_core::name::Name": (
|
||||
hash: 3398656236303073559,
|
||||
name: "test7470642598731063943",
|
||||
),
|
||||
"bevy_gltf_blueprints::spawn_from_blueprints::BlueprintName": ("Health_Pickup"),
|
||||
"advanced::game::picking::Pickable": (),
|
||||
"bevy_rapier3d::dynamics::rigid_body::Velocity": (
|
||||
linvel: (
|
||||
x: -0.9268077,
|
||||
y: -0.19806683,
|
||||
z: 0.41948256,
|
||||
),
|
||||
angvel: (
|
||||
x: 0.26946256,
|
||||
y: -0.000006710977,
|
||||
z: 0.5953494,
|
||||
),
|
||||
),
|
||||
},
|
||||
),
|
||||
90: (
|
||||
components: {
|
||||
"bevy_transform::components::transform::Transform": (
|
||||
translation: (
|
||||
x: 2.6515265,
|
||||
y: 1.5944021,
|
||||
z: -4.391837,
|
||||
),
|
||||
rotation: (-0.030030435, -0.0000006527225, 0.029748484, 0.9991062),
|
||||
scale: (
|
||||
x: 1.0,
|
||||
y: 1.0,
|
||||
z: 1.0,
|
||||
),
|
||||
),
|
||||
"bevy_core::name::Name": (
|
||||
hash: 12541900054595385134,
|
||||
name: "test3938024405863834719",
|
||||
),
|
||||
"bevy_gltf_blueprints::spawn_from_blueprints::BlueprintName": ("Health_Pickup"),
|
||||
"advanced::game::picking::Pickable": (),
|
||||
"bevy_rapier3d::dynamics::rigid_body::Velocity": (
|
||||
linvel: (
|
||||
x: -0.28430828,
|
||||
y: -0.022357654,
|
||||
z: -0.2870027,
|
||||
),
|
||||
angvel: (
|
||||
x: -0.17986917,
|
||||
y: -0.0000035613396,
|
||||
z: 0.17818078,
|
||||
),
|
||||
),
|
||||
},
|
||||
),
|
||||
94: (
|
||||
components: {
|
||||
"bevy_transform::components::transform::Transform": (
|
||||
translation: (
|
||||
x: -4.2356462,
|
||||
y: 1.596993,
|
||||
z: 0.7254991,
|
||||
),
|
||||
rotation: (-0.0221751, -0.0000000001891749, 0.011065631, 0.99969286),
|
||||
scale: (
|
||||
x: 1.0,
|
||||
y: 1.0,
|
||||
z: 1.0,
|
||||
),
|
||||
),
|
||||
"bevy_core::name::Name": (
|
||||
hash: 6757906322211730861,
|
||||
name: "test11007490954016878479",
|
||||
),
|
||||
"bevy_gltf_blueprints::spawn_from_blueprints::BlueprintName": ("Health_Pickup"),
|
||||
"advanced::game::picking::Pickable": (),
|
||||
"bevy_rapier3d::dynamics::rigid_body::Velocity": (
|
||||
linvel: (
|
||||
x: -0.21747473,
|
||||
y: -0.014912919,
|
||||
z: -0.43581253,
|
||||
),
|
||||
angvel: (
|
||||
x: -0.2727097,
|
||||
y: -0.0000000034594905,
|
||||
z: 0.13608481,
|
||||
),
|
||||
),
|
||||
},
|
||||
),
|
||||
98: (
|
||||
components: {
|
||||
"bevy_transform::components::transform::Transform": (
|
||||
translation: (
|
||||
x: 3.1525247,
|
||||
y: 1.5518407,
|
||||
z: -2.9611976,
|
||||
),
|
||||
rotation: (-0.09219627, 0.1602262, -0.11205085, 0.9763565),
|
||||
scale: (
|
||||
x: 1.0,
|
||||
y: 1.0,
|
||||
z: 1.0,
|
||||
),
|
||||
),
|
||||
"bevy_core::name::Name": (
|
||||
hash: 12588565107899185946,
|
||||
name: "test5980867849331267699",
|
||||
),
|
||||
"bevy_gltf_blueprints::spawn_from_blueprints::BlueprintName": ("Health_Pickup"),
|
||||
"advanced::game::picking::Pickable": (),
|
||||
"bevy_rapier3d::dynamics::rigid_body::Velocity": (
|
||||
linvel: (
|
||||
x: 0.8323179,
|
||||
y: -0.20597076,
|
||||
z: -0.68975484,
|
||||
),
|
||||
angvel: (
|
||||
x: -0.37971017,
|
||||
y: 0.49603412,
|
||||
z: -0.6079359,
|
||||
),
|
||||
),
|
||||
},
|
||||
),
|
||||
4294967310: (
|
||||
components: {
|
||||
"bevy_transform::components::transform::Transform": (
|
||||
translation: (
|
||||
x: 4.826278,
|
||||
y: 1.2710563,
|
||||
z: -3.1997645,
|
||||
),
|
||||
rotation: (-0.303028, 0.00000087800436, -0.23889118, 0.9225535),
|
||||
scale: (
|
||||
x: 1.0,
|
||||
y: 1.0,
|
||||
z: 1.0,
|
||||
),
|
||||
),
|
||||
"bevy_core::name::Name": (
|
||||
hash: 15533546218717453536,
|
||||
name: "test12380979123759326444",
|
||||
),
|
||||
"bevy_gltf_blueprints::spawn_from_blueprints::BlueprintName": ("Health_Pickup"),
|
||||
"advanced::game::picking::Pickable": (),
|
||||
"bevy_rapier3d::dynamics::rigid_body::Velocity": (
|
||||
linvel: (
|
||||
x: 1.2146912,
|
||||
y: -1.1640646,
|
||||
z: -1.5408095,
|
||||
),
|
||||
angvel: (
|
||||
x: -1.1932359,
|
||||
y: 0.000002945365,
|
||||
z: -0.94068503,
|
||||
),
|
||||
),
|
||||
},
|
||||
),
|
||||
4294967314: (
|
||||
components: {
|
||||
"bevy_transform::components::transform::Transform": (
|
||||
translation: (
|
||||
x: 3.9906094,
|
||||
y: 1.4824095,
|
||||
z: 2.4394412,
|
||||
),
|
||||
rotation: (0.06015042, 0.085218765, 0.2215642, 0.9695509),
|
||||
scale: (
|
||||
x: 1.0,
|
||||
y: 1.0,
|
||||
z: 1.0,
|
||||
),
|
||||
),
|
||||
"bevy_core::name::Name": (
|
||||
hash: 2466794778849297109,
|
||||
name: "test12475628281920299197",
|
||||
),
|
||||
"bevy_gltf_blueprints::spawn_from_blueprints::BlueprintName": ("Health_Pickup"),
|
||||
"advanced::game::picking::Pickable": (),
|
||||
"bevy_rapier3d::dynamics::rigid_body::Velocity": (
|
||||
linvel: (
|
||||
x: -1.0818624,
|
||||
y: -0.37798148,
|
||||
z: 0.45334253,
|
||||
),
|
||||
angvel: (
|
||||
x: 0.25961447,
|
||||
y: 0.14854014,
|
||||
z: 0.7426717,
|
||||
),
|
||||
),
|
||||
},
|
||||
),
|
||||
4294967321: (
|
||||
components: {
|
||||
"bevy_transform::components::transform::Transform": (
|
||||
translation: (
|
||||
x: 2.2306876,
|
||||
y: 0.989814,
|
||||
z: -1.3596333,
|
||||
),
|
||||
rotation: (0.30614096, 0.002587511, -0.42789298, 0.8503991),
|
||||
scale: (
|
||||
x: 1.0,
|
||||
y: 1.0,
|
||||
z: 1.0,
|
||||
),
|
||||
),
|
||||
"bevy_core::name::Name": (
|
||||
hash: 1545925632270385398,
|
||||
name: "test15780367212768138828",
|
||||
),
|
||||
"bevy_gltf_blueprints::spawn_from_blueprints::BlueprintName": ("Health_Pickup"),
|
||||
"advanced::game::picking::Pickable": (),
|
||||
"bevy_rapier3d::dynamics::rigid_body::Velocity": (
|
||||
linvel: (
|
||||
x: 1.3027526,
|
||||
y: -1.8947054,
|
||||
z: 1.6179247,
|
||||
),
|
||||
angvel: (
|
||||
x: 1.4565696,
|
||||
y: -0.16299045,
|
||||
z: -1.3631926,
|
||||
),
|
||||
),
|
||||
},
|
||||
),
|
||||
},
|
||||
)
|
4629
examples/bevy_gltf_blueprints/multiple_levels_multiple_blendfiles/Cargo.lock
generated
Normal file
@ -0,0 +1,13 @@
|
||||
[package]
|
||||
name = "bevy_gltf_blueprints_multiple_levels_multiple_blendfiles"
|
||||
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"
|
@ -0,0 +1,13 @@
|
||||
# Multiple levels from multiple blend files example/demo
|
||||
|
||||
This example showcases multiple levels, each created from a seperate Blend file , with all assets stored in the (common file)[./art/common.blend]
|
||||
It using triggers to transition to another level.
|
||||
It currently has NO state persistence between levels.
|
||||
You can press "D" to toggle the physics debug view.
|
||||
|
||||
|
||||
## Running this example
|
||||
|
||||
```
|
||||
cargo run --features bevy/dynamic_linking
|
||||
```
|
@ -0,0 +1 @@
|
||||
({})
|
@ -0,0 +1,12 @@
|
||||
({
|
||||
"world":File (path: "models/StartLevel.glb"),
|
||||
"level1":File (path: "models/Level1.glb"),
|
||||
"level2":File (path: "models/Level2.glb"),
|
||||
|
||||
"models": Folder (
|
||||
path: "models/library",
|
||||
),
|
||||
"materials": Folder (
|
||||
path: "materials",
|
||||
),
|
||||
})
|
@ -0,0 +1,5 @@
|
||||
use bevy::prelude::*;
|
||||
use bevy_asset_loader::prelude::*;
|
||||
|
||||
#[derive(AssetCollection, Resource)]
|
||||
pub struct CoreAssets {}
|
@ -0,0 +1,19 @@
|
||||
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 = "level1")]
|
||||
pub level1: Handle<Gltf>,
|
||||
#[asset(key = "level2")]
|
||||
pub level2: Handle<Gltf>,
|
||||
#[asset(key = "models", collection(typed, mapped))]
|
||||
pub models: HashMap<String, Handle<Gltf>>,
|
||||
|
||||
#[asset(key = "materials", collection(typed, mapped))]
|
||||
pub materials: HashMap<String, Handle<Gltf>>,
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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()
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,81 @@
|
||||
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;
|
||||
|
||||
// this system ensures that the camera is at the correct position immediatly after spawning
|
||||
pub fn init_camera_track(
|
||||
mut tracking_cameras: Query<
|
||||
(&mut Transform, &CameraTrackingOffset),
|
||||
(
|
||||
With<Camera>,
|
||||
With<CameraTrackingOffset>,
|
||||
Without<CameraTrackable>,
|
||||
),
|
||||
>,
|
||||
camera_tracked: Query<&Transform, (With<CameraTrackable>, Added<CameraTrackable>)>,
|
||||
) {
|
||||
for (mut camera_transform, tracking_offset) in tracking_cameras.iter_mut() {
|
||||
for tracked_transform in camera_tracked.iter() {
|
||||
println!("ADDED tracking camera");
|
||||
|
||||
let target_position = tracked_transform.translation + tracking_offset.0;
|
||||
camera_transform.translation = target_position;
|
||||
*camera_transform = camera_transform.looking_at(tracked_transform.translation, Vec3::Y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
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),
|
||||
init_camera_track,
|
||||
camera_track,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
use bevy::prelude::*;
|
||||
|
||||
use bevy::pbr::{CascadeShadowConfig, CascadeShadowConfigBuilder};
|
||||
|
||||
pub fn lighting_replace_proxies(
|
||||
mut added_dirights: Query<(Entity, &mut DirectionalLight), Added<DirectionalLight>>,
|
||||
mut added_spotlights: Query<&mut SpotLight, Added<SpotLight>>,
|
||||
mut added_pointlights: Query<&mut PointLight, Added<PointLight>>,
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
for mut light in added_pointlights.iter_mut() {
|
||||
light.intensity *= 0.001; // arbitrary/ eyeballed to match the levels of Blender
|
||||
light.shadows_enabled = true;
|
||||
}
|
||||
}
|
@ -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
|
||||
;
|
||||
}
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
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(),
|
||||
material_library: true,
|
||||
..Default::default()
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
use bevy::prelude::{info, Input, KeyCode, Res, ResMut};
|
||||
use bevy_rapier3d::{prelude::RapierConfiguration, render::DebugRenderContext};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
pub fn toggle_physics_debug(
|
||||
mut debug_config: ResMut<DebugRenderContext>,
|
||||
keycode: Res<Input<KeyCode>>,
|
||||
) {
|
||||
if keycode.just_pressed(KeyCode::D) {
|
||||
debug_config.enabled = !debug_config.enabled;
|
||||
}
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
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>()
|
||||
.add_systems(
|
||||
Update,
|
||||
physics_replace_proxies.after(GltfBlueprintsSet::AfterSpawn),
|
||||
)
|
||||
// physics controls
|
||||
.add_systems(OnEnter(GameState::InGame), resume_physics)
|
||||
.add_systems(OnExit(GameState::InGame), pause_physics)
|
||||
.add_systems(Update, toggle_physics_debug);
|
||||
}
|
||||
}
|
@ -0,0 +1,102 @@
|
||||
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;
|
||||
}
|
||||
// also entities marked with collider names are actually children colliders
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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>());
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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)
|
||||
;
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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() }
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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() })
|
||||
}
|
||||
}
|
@ -0,0 +1,100 @@
|
||||
use crate::{assets::GameAssets, state::InAppRunning};
|
||||
use bevy::prelude::*;
|
||||
use bevy_gltf_blueprints::GameWorldTag;
|
||||
use bevy_rapier3d::prelude::*;
|
||||
|
||||
use super::Player;
|
||||
|
||||
#[derive(Component, Reflect, Default, Debug)]
|
||||
#[reflect(Component)]
|
||||
pub struct LevelTransition {
|
||||
pub target: String,
|
||||
}
|
||||
|
||||
// very barebones example of triggering level transitions
|
||||
pub fn trigger_level_transition(
|
||||
mut collision_events: EventReader<CollisionEvent>,
|
||||
level_transition_triggers: Query<&LevelTransition>,
|
||||
parents: Query<&Parent>,
|
||||
players: Query<&Player>,
|
||||
|
||||
mut commands: Commands,
|
||||
|
||||
game_assets: Res<GameAssets>,
|
||||
models: Res<Assets<bevy::gltf::Gltf>>,
|
||||
game_world: Query<(Entity, &GameWorldTag)>,
|
||||
) {
|
||||
for collision_event in collision_events.read() {
|
||||
match collision_event {
|
||||
CollisionEvent::Started(entity1, entity2, _) => {
|
||||
// we need to accomodate for the fact that the collider may be a child of the level transition (FIXME: is this a missunderstanding on my part about rapier child colliders ?)
|
||||
let entity1_parent = parents.get(*entity1).unwrap();
|
||||
let entity2_parent = parents.get(*entity2).unwrap();
|
||||
|
||||
|
||||
if level_transition_triggers.get(*entity1).is_ok()
|
||||
|| level_transition_triggers.get(*entity2).is_ok()
|
||||
|| level_transition_triggers.get(entity1_parent.get()).is_ok()
|
||||
|| level_transition_triggers.get(entity2_parent.get()).is_ok()
|
||||
{
|
||||
println!("collision started, we can transition to level");
|
||||
let transition_trigger;
|
||||
if level_transition_triggers.get(*entity1).is_ok() {
|
||||
transition_trigger = level_transition_triggers.get(*entity1).unwrap();
|
||||
} else if level_transition_triggers.get(*entity2).is_ok() {
|
||||
transition_trigger = level_transition_triggers.get(*entity2).unwrap();
|
||||
} else if level_transition_triggers.get(entity1_parent.get()).is_ok() {
|
||||
transition_trigger =
|
||||
level_transition_triggers.get(entity1_parent.get()).unwrap();
|
||||
} else {
|
||||
transition_trigger =
|
||||
level_transition_triggers.get(entity2_parent.get()).unwrap();
|
||||
}
|
||||
|
||||
if players.get(*entity1).is_ok() || players.get(entity1_parent.get()).is_ok() || players.get(*entity2).is_ok() || players.get(entity2_parent.get()).is_ok() {
|
||||
println!("one entity is the player, we can enter")
|
||||
}
|
||||
else {
|
||||
// if none of our entities is a player, bail out, as only entities with player components should trigger a transition
|
||||
return;
|
||||
}
|
||||
|
||||
let current_game_world = game_world.single();
|
||||
|
||||
// remove current level/world
|
||||
info!("despawning current level");
|
||||
commands.entity(current_game_world.0).despawn_recursive();
|
||||
|
||||
let target_level = &transition_trigger.target;
|
||||
let level;
|
||||
println!("target level {}", target_level);
|
||||
if target_level == "Level1" {
|
||||
level = &game_assets.level1;
|
||||
} else if (target_level == "Level2") {
|
||||
level = &game_assets.level2;
|
||||
} else {
|
||||
level = &game_assets.world;
|
||||
}
|
||||
info!("spawning new 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(level.id())
|
||||
.expect("main level should have been loaded")
|
||||
.scenes[0]
|
||||
.clone(),
|
||||
..default()
|
||||
},
|
||||
bevy::prelude::Name::from("world"),
|
||||
GameWorldTag,
|
||||
InAppRunning,
|
||||
));
|
||||
}
|
||||
}
|
||||
CollisionEvent::Stopped(_entity1, _entity2, _) => {
|
||||
// println!("collision ended")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,121 @@
|
||||
pub mod in_game;
|
||||
pub use in_game::*;
|
||||
|
||||
pub mod in_main_menu;
|
||||
pub use in_main_menu::*;
|
||||
|
||||
pub mod picking;
|
||||
pub use picking::*;
|
||||
|
||||
pub mod level_transitions;
|
||||
pub use level_transitions::*;
|
||||
|
||||
use crate::{
|
||||
assets::GameAssets,
|
||||
insert_dependant_component,
|
||||
state::{AppState, GameState, InAppRunning},
|
||||
};
|
||||
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>()
|
||||
.register_type::<LevelTransition>()
|
||||
// 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,
|
||||
trigger_level_transition,
|
||||
)
|
||||
.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.run_if(in_state(AppState::MenuRunning)))
|
||||
.add_systems(OnEnter(AppState::AppRunning), setup_game);
|
||||
}
|
||||
}
|
@ -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),));
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
@ -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>();
|
||||
}
|
||||
}
|
@ -0,0 +1,80 @@
|
||||
use bevy::prelude::*;
|
||||
|
||||
#[derive(Component, Reflect, Default, Debug)]
|
||||
#[reflect(Component)]
|
||||
struct UnitTest;
|
||||
|
||||
#[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::<UnitTest>()
|
||||
.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>>();
|
||||
}
|
||||
}
|
@ -28,8 +28,32 @@ This [Blender addon](./)
|
||||
|
||||
![blender addon install](./docs/blender_addon_install2.png)
|
||||
|
||||
|
||||
|
||||
|
||||
## Usage:
|
||||
|
||||
> ***IMPORTANT***
|
||||
|
||||
if you have used a version of this add-on prior to v0.9, there was an issue that kept generating orphan (junk) data on every save !
|
||||
You can easilly clean up that data
|
||||
|
||||
- go to orphan data:
|
||||
|
||||
![purge orphan data](./docs/purge_orphan1_data1.png)
|
||||
|
||||
- click on purge
|
||||
|
||||
![purge orphan data](./docs/purge_orphan1_data2.png)
|
||||
|
||||
- validate
|
||||
|
||||
![purge orphan data](./docs/purge_orphan1_data3.png)
|
||||
|
||||
|
||||
|
||||
This issue has been resolved in v0.9.
|
||||
|
||||
|
||||
### Basics
|
||||
|
||||
@ -47,7 +71,8 @@ This [Blender addon](./)
|
||||
- export scene settings: exports "global"/scene settings like ambient color, bloom, ao, etc
|
||||
|
||||
This automatically generates additional components at the scene level
|
||||
- pick your main (level) scenes and library scenes (see the chapter about Blueprints below)
|
||||
|
||||
- pick your main (level) scenes and/or library scenes (see the chapter about [Blueprints](#blueprints) and [multiple Blend filles workflow](#multiple-blend-file-workflow) below)
|
||||
- click in the scene picker & select your scene
|
||||
|
||||
![select scene](./docs/blender_addon_add_scene.png)
|
||||
@ -62,12 +87,23 @@ 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
|
||||
- collection instances: select which option you want to use to deal with collection instances (aka combine mode) (both inside blueprint collections & main collections)
|
||||
|
||||
* split (default, highly recomended) : the addon will 'split out' any nested collections/ blueprints & export them
|
||||
* embed: choose this option if you want to keep everything inside a gltf file (less efficient, not recomended)
|
||||
* embedExternal: this will embed ONLY collection instances whose collections have not been found inside the current blend file
|
||||
|
||||
These options can also be **overridden** on a per collection instance basis: (if you want to split out most collection instances, but keep a few specific ones embeded
|
||||
inside your gltf file)
|
||||
|
||||
![combine override](./docs/combine_override.png)
|
||||
|
||||
- simply add a custom property called **_combine** to the collection instance, and set it to one of the options above
|
||||
|
||||
please read the dedicated [section](#collection-instances--nested-blueprints) 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
|
||||
please read the dedicated [section](#materials) below for more information
|
||||
|
||||
> This only works together with blueprints !
|
||||
|
||||
@ -112,12 +148,19 @@ You can enable this option to automatically replace all the **collection instanc
|
||||
|
||||
![exported collections](./docs/exported_collections.png)
|
||||
|
||||
- there are some workflow specificities for multi blend file [workflows](#multiple-blend-file-workflow)
|
||||
|
||||
#### Nested blueprints
|
||||
#### Collection instances & 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
|
||||
> Don't forget to choose the relevant option in the exporter settings (aka **"split"**)
|
||||
|
||||
> This replaces the previous "export nested blueprints" checkbox/ option
|
||||
|
||||
![instance combine mode](./docs/blender_addon_use4.png)
|
||||
|
||||
|
||||
|
||||
- To make things clearer:
|
||||
|
||||
@ -145,7 +188,6 @@ To maximise reuse of meshes/components etc, you can also nest ***collections ins
|
||||
|
||||
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.
|
||||
@ -166,9 +208,34 @@ options in **bevy_gltf_blueprints** for more information on that)
|
||||
TLDR: Use this option to make sure that each blueprint file does not contain a copy of the same materials
|
||||
|
||||
|
||||
#### Process
|
||||
### Multiple blend file workflow
|
||||
|
||||
This is the internal logic of the export process with blueprints
|
||||
If you want to use multiple blend files, use Blender's asset library etc, we got you coverred too !
|
||||
There are only a few things to keep in mind
|
||||
|
||||
#### Assets/library/blueprints files
|
||||
- mark your library scenes as specified above, but **do NOT** specify a **main** scene
|
||||
- mark any collection in your scenes as "assets" (more convenient) or add the "AutoExport" custom property to the collection
|
||||
- choose "split" for the combine mode (as you want your gltf blueprints to be saved for external use)
|
||||
- do your Blender things as normal
|
||||
- anytime you save your file, it will automatically export any relevant collections/blueprints
|
||||
- (optional) activate the **material library** option, so you only have one set of material per asset library (recomended)
|
||||
|
||||
#### Level/world files
|
||||
- mark your main scenes as specified above, but **do NOT** specify a **library** scene
|
||||
- configure your asset libraries as you would usually do , I recomend using the "link" mode so that any changes to asset files are reflected correctly
|
||||
- drag & drop any assets from the blueprints library (as you would normally do in Blender as well)
|
||||
- choose "split" for the combine mode (as you want your gltf blueprints to be external usually & use the gltf files generated from your assets library)
|
||||
- do your Blender things as normal
|
||||
- anytime you save your file, it will automatically export your level(s)
|
||||
|
||||
|
||||
Take a look at the [relevant](../../examples/bevy_gltf_blueprints/multiple_levels_multiple_blendfiles/) example for more [details](../../examples/bevy_gltf_blueprints/multiple_levels_multiple_blendfiles/art/)
|
||||
|
||||
|
||||
### Process
|
||||
|
||||
This is the internal logic of the export process with blueprints (simplified)
|
||||
|
||||
![process](./docs/process.svg)
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
bl_info = {
|
||||
"name": "gltf_auto_export",
|
||||
"author": "kaosigh",
|
||||
"version": (0, 8, 0),
|
||||
"version": (0, 9, 0),
|
||||
"blender": (3, 4, 0),
|
||||
"location": "File > Import-Export",
|
||||
"description": "glTF/glb auto-export",
|
||||
@ -77,6 +77,14 @@ def deps_update_handler(scene, depsgraph):
|
||||
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
|
||||
elif isinstance(obj.id, bpy.types.Material): # or isinstance(obj.id, bpy.types.ShaderNodeTree):
|
||||
print("changed material", obj.id, "scene", scene.name,)
|
||||
material = bpy.data.materials[obj.id.name]
|
||||
#now find which objects are using the material
|
||||
for obj in bpy.data.objects:
|
||||
for slot in obj.material_slots:
|
||||
if slot.material == material:
|
||||
bpy.context.window_manager['changed_objects_per_scene'][scene.name][obj.name] = obj
|
||||
|
||||
bpy.context.window_manager.changedScene = changed
|
||||
|
||||
|
@ -3,7 +3,7 @@ import bpy
|
||||
import traceback
|
||||
|
||||
from .helpers_scenes import (get_scenes, )
|
||||
from .helpers_collections import (get_exportable_collections, get_collections_per_scene)
|
||||
from .helpers_collections import (get_collections_in_library, get_exportable_collections, get_collections_per_scene, find_collection_ascendant_target_collection)
|
||||
from .helpers_export import (export_main_scene, export_blueprints_from_collections)
|
||||
from .helpers import (check_if_blueprints_exist, check_if_blueprint_on_disk)
|
||||
from .materials import cleanup_materials, clear_material_info, clear_materials_scene, export_materials, generate_materials_scenes, get_all_materials
|
||||
@ -61,8 +61,6 @@ 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)
|
||||
|
||||
@ -76,9 +74,17 @@ def auto_export(changes_per_scene, changed_export_parameters):
|
||||
# export
|
||||
if export_blueprints:
|
||||
print("EXPORTING")
|
||||
# create parent relations for all collections
|
||||
collection_parents = dict()
|
||||
for collection in bpy.data.collections:
|
||||
collection_parents[collection.name] = None
|
||||
for collection in bpy.data.collections:
|
||||
for ch in collection.children:
|
||||
collection_parents[ch.name] = collection.name
|
||||
|
||||
# get a list of all collections actually in use
|
||||
scan_nested_collections = export_nested_blueprints
|
||||
(collections, blueprint_hierarchy) = get_exportable_collections(level_scenes, library_scenes, scan_nested_collections)
|
||||
(collections, blueprint_hierarchy) = get_exportable_collections(level_scenes, library_scenes, addon_prefs)
|
||||
|
||||
# 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
|
||||
@ -89,19 +95,20 @@ def auto_export(changes_per_scene, changed_export_parameters):
|
||||
collections_not_on_disk = check_if_blueprints_exist(collections, export_blueprints_path, gltf_extension)
|
||||
changed_collections = []
|
||||
|
||||
print('changes_per_scene', changes_per_scene.items(), changes_per_scene.keys())
|
||||
for scene, bla in changes_per_scene.items():
|
||||
for scene, objects in changes_per_scene.items():
|
||||
print(" changed scene", scene)
|
||||
for obj_name, obj in bla.items():
|
||||
object_collections = list(obj.users_collection)
|
||||
for obj_name, obj in objects.items():
|
||||
object_collections = list(obj.users_collection) if hasattr(obj, 'users_collection') else []
|
||||
object_collection_names = list(map(lambda collection: collection.name, object_collections))
|
||||
|
||||
if len(object_collection_names) > 1:
|
||||
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)
|
||||
if object_collection_name in collections:
|
||||
changed_collections.append(object_collection_name)
|
||||
#recurse updwards until we find one of our collections (or not)
|
||||
matching_collection = find_collection_ascendant_target_collection(collection_parents, collections, object_collection_name)
|
||||
if matching_collection is not None:
|
||||
changed_collections.append(matching_collection)
|
||||
|
||||
collections_to_export = list(set(changed_collections + collections_not_on_disk))
|
||||
|
||||
@ -112,7 +119,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]
|
||||
library_collections = get_collections_in_library(library_scenes)
|
||||
collections_to_export = list(set(collections_to_export).intersection(set(library_collections)))
|
||||
|
||||
# since materials export adds components we need to call this before blueprints are exported
|
||||
@ -125,6 +132,7 @@ def auto_export(changes_per_scene, changed_export_parameters):
|
||||
print("collections: all:", collections)
|
||||
print("collections: changed:", changed_collections)
|
||||
print("collections: not found on disk:", collections_not_on_disk)
|
||||
print("collections: in library:", library_collections)
|
||||
print("collections: to export:", collections_to_export)
|
||||
print("collections: per_scene:", collections_per_scene)
|
||||
|
||||
@ -140,7 +148,7 @@ def auto_export(changes_per_scene, changed_export_parameters):
|
||||
do_export_main_scene = changed_export_parameters or scene_name in changes_per_scene.keys() or not check_if_blueprint_on_disk(scene_name, export_levels_path, gltf_extension)
|
||||
if do_export_main_scene:
|
||||
print(" exporting scene:", scene_name)
|
||||
export_main_scene(bpy.data.scenes[scene_name], folder_path, addon_prefs, collections)
|
||||
export_main_scene(bpy.data.scenes[scene_name], folder_path, addon_prefs, library_collections)
|
||||
|
||||
|
||||
# now deal with blueprints/collections
|
||||
@ -169,7 +177,8 @@ def auto_export(changes_per_scene, changed_export_parameters):
|
||||
export_main_scene(bpy.data.scenes[scene_name], folder_path, addon_prefs)
|
||||
|
||||
except Exception as error:
|
||||
traceback.print_stack()
|
||||
print(traceback.format_exc())
|
||||
|
||||
def error_message(self, context):
|
||||
self.layout.label(text="Failure during auto_export: Error: "+ str(error))
|
||||
|
||||
|
@ -3,7 +3,9 @@ from .helpers_collections import (find_layer_collection_recursive)
|
||||
from .helpers import (make_empty3, traverse_tree)
|
||||
|
||||
|
||||
def generate_blueprint_hollow_scene(blueprint_collection, library_collections):
|
||||
def generate_blueprint_hollow_scene(blueprint_collection, library_collections, addon_prefs):
|
||||
collection_instances_combine_mode = getattr(addon_prefs, "collection_instances_combine_mode")
|
||||
|
||||
temp_scene = bpy.data.scenes.new(name="temp_scene_"+blueprint_collection.name)
|
||||
temp_scene_root_collection = temp_scene.collection
|
||||
|
||||
@ -15,32 +17,48 @@ def generate_blueprint_hollow_scene(blueprint_collection, library_collections):
|
||||
bpy.context.view_layer.active_layer_collection = found
|
||||
|
||||
original_names = []
|
||||
temporary_collections = []
|
||||
root_objects = []
|
||||
special_properties= { # to be able to reset any special property afterwards
|
||||
"combine": [],
|
||||
}
|
||||
|
||||
# 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):
|
||||
# if we have combine_mode set to "Inject", we take all the custom attributed of the nested (1 level only ! unless we use 'deepMerge') custom attributes and copy them to this level
|
||||
def copy_hollowed_collection_into(source_collection, destination_collection, parent_empty=None):
|
||||
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:
|
||||
"""
|
||||
combine_mode = object['_combine'] if '_combine' in object else collection_instances_combine_mode
|
||||
|
||||
if object.instance_type == 'COLLECTION' and (combine_mode == 'Split' or (combine_mode == 'EmbedExternal' and (object.instance_collection.name in library_collections)) ):
|
||||
|
||||
# get the name of the collection this is an instance of
|
||||
collection_name = object.instance_collection.name
|
||||
"""
|
||||
blueprint_template = object['Template'] if 'Template' in object else False
|
||||
if blueprint_template and parent_empty is None: # ONLY WORKS AT ROOT LEVEL
|
||||
print("BLUEPRINT TEMPLATE", blueprint_template, destination_collection, parent_empty)
|
||||
for object in source_collection.objects:
|
||||
if object.type == 'EMPTY' and object.name.endswith("components"):
|
||||
original_collection = bpy.data.collections[collection_name]
|
||||
components_holder = object
|
||||
print("WE CAN INJECT into", object, "data from", original_collection)
|
||||
|
||||
# now we look for components inside the collection
|
||||
components = {}
|
||||
for object in original_collection.objects:
|
||||
if object.type == 'EMPTY' and object.name.endswith("components"):
|
||||
for component_name in object.keys():
|
||||
if component_name not in '_RNA_UI':
|
||||
print( component_name , "-" , object[component_name] )
|
||||
components[component_name] = object[component_name]
|
||||
|
||||
# copy template components into target object
|
||||
for key in components:
|
||||
print("copying ", key,"to", components_holder)
|
||||
if not key in components_holder:
|
||||
components_holder[key] = components[key]
|
||||
"""
|
||||
|
||||
original_name = object.name
|
||||
original_names.append(original_name)
|
||||
@ -51,29 +69,59 @@ def generate_blueprint_hollow_scene(blueprint_collection, library_collections):
|
||||
empty_obj['BlueprintName'] = '"'+collection_name+'"'
|
||||
empty_obj['SpawnHere'] = ''
|
||||
|
||||
|
||||
for k, v in object.items():
|
||||
if k != 'template' or k != '_combine': # do not copy these properties
|
||||
empty_obj[k] = v
|
||||
|
||||
if parent_empty is not None:
|
||||
empty_obj.parent = parent_empty
|
||||
else:
|
||||
# we backup special properties that we do not want to export, and remove them
|
||||
if '_combine' in object:
|
||||
special_properties["combine"].append((object, object['_combine']))
|
||||
del object['_combine']
|
||||
|
||||
if parent_empty is not None:
|
||||
object.parent = parent_empty
|
||||
destination_collection.objects.link(object)
|
||||
else:
|
||||
root_objects.append(object)
|
||||
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:
|
||||
original_name = collection.name
|
||||
collection.name = original_name + "____bak"
|
||||
collection_placeholder = make_empty3(original_name, [0,0,0], [0,0,0], [1,1,1], destination_collection)
|
||||
|
||||
if parent_empty is not None:
|
||||
collection_placeholder.parent = parent_empty
|
||||
|
||||
copy_hollowed_collection_into(collection, destination_collection, collection_placeholder)
|
||||
|
||||
"""
|
||||
copy_collection = bpy.data.collections.new(collection.name + "____collection_export")
|
||||
# save the newly created collection for later reuse
|
||||
temporary_collections.append(copy_collection)
|
||||
|
||||
# copy & link objects
|
||||
copy_hollowed_collection_into(collection, copy_collection)
|
||||
destination_collection.children.link(copy_collection)
|
||||
destination_collection.children.link(copy_collection)"""
|
||||
|
||||
copy_hollowed_collection_into(blueprint_collection, temp_scene_root_collection)
|
||||
|
||||
|
||||
return (temp_scene, original_names)
|
||||
return (temp_scene, temporary_collections, root_objects, special_properties)
|
||||
|
||||
|
||||
|
||||
|
||||
# clear & remove "hollow scene"
|
||||
def clear_blueprint_hollow_scene(temp_scene, original_collection, original_names):
|
||||
def clear_blueprint_hollow_scene(temp_scene, original_collection, temporary_collections, root_objects, special_properties):
|
||||
|
||||
def restore_original_names(collection):
|
||||
if collection.name.endswith("____bak"):
|
||||
collection.name = collection.name.replace("____bak", "")
|
||||
for object in collection.objects:
|
||||
if object.instance_type == 'COLLECTION':
|
||||
if object.name.endswith("____bak"):
|
||||
@ -84,18 +132,32 @@ def clear_blueprint_hollow_scene(temp_scene, original_collection, original_names
|
||||
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:
|
||||
temp_root_collection = temp_scene.collection
|
||||
temp_scene_objects = [o for o in temp_root_collection.objects]
|
||||
for object in temp_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)
|
||||
if object in root_objects:
|
||||
pass
|
||||
else:
|
||||
bpy.data.objects.remove(object, do_unlink=True)
|
||||
else:
|
||||
bpy.context.scene.collection.objects.unlink(object)
|
||||
|
||||
# put back special properties
|
||||
for (object, value) in special_properties["combine"]:
|
||||
object['_combine'] = value
|
||||
|
||||
# remove temporary collections
|
||||
for collection in temporary_collections:
|
||||
bpy.data.collections.remove(collection)
|
||||
|
||||
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':
|
||||
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 17 KiB |
BIN
tools/gltf_auto_export/docs/blender_addon_use4.png
Normal file
After Width: | Height: | Size: 4.2 KiB |
BIN
tools/gltf_auto_export/docs/combine_override.png
Normal file
After Width: | Height: | Size: 2.7 KiB |
1159
tools/gltf_auto_export/docs/options.svg
Normal file
After Width: | Height: | Size: 51 KiB |
BIN
tools/gltf_auto_export/docs/purge_orphan1_data1.png
Normal file
After Width: | Height: | Size: 8.2 KiB |
BIN
tools/gltf_auto_export/docs/purge_orphan1_data2.png
Normal file
After Width: | Height: | Size: 3.9 KiB |
BIN
tools/gltf_auto_export/docs/purge_orphan1_data3.png
Normal file
After Width: | Height: | Size: 5.5 KiB |
@ -21,7 +21,9 @@ def get_used_collections(scene):
|
||||
return (collection_names, used_collections)
|
||||
|
||||
# gets all collections that should ALWAYS be exported to their respective gltf files, even if they are not used in the main scene/level
|
||||
def get_marked_collections(scene):
|
||||
def get_marked_collections(scene, addon_prefs):
|
||||
export_marked_assets = getattr(addon_prefs,"export_marked_assets")
|
||||
|
||||
# print("checking library for marked collections")
|
||||
root_collection = scene.collection
|
||||
marked_collections = []
|
||||
@ -30,6 +32,10 @@ def get_marked_collections(scene):
|
||||
if 'AutoExport' in collection and collection['AutoExport'] == True:
|
||||
marked_collections.append(collection)
|
||||
collection_names.append(collection.name)
|
||||
# if you have marked collections as assets you can auto export them too
|
||||
if export_marked_assets and collection.asset_data is not None:
|
||||
marked_collections.append(collection)
|
||||
collection_names.append(collection.name)
|
||||
return (collection_names, marked_collections)
|
||||
|
||||
# gets all collections within collections that might also be relevant
|
||||
@ -88,7 +94,8 @@ class Node :
|
||||
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, scan_nested_collections):
|
||||
def get_exportable_collections(main_scenes, library_scenes, addon_prefs):
|
||||
|
||||
all_collections = []
|
||||
all_collection_names = []
|
||||
root_node = Node()
|
||||
@ -101,11 +108,10 @@ def get_exportable_collections(main_scenes, library_scenes, scan_nested_collecti
|
||||
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)
|
||||
marked_collections = get_marked_collections(library_scene, addon_prefs)
|
||||
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 = {}
|
||||
@ -126,6 +132,25 @@ def get_collections_per_scene(collection_names, library_scenes):
|
||||
|
||||
return collections_per_scene
|
||||
|
||||
def get_collections_in_library(library_scenes):
|
||||
"""all_collections = []
|
||||
all_collection_names = []
|
||||
for main_scene in main_scenes:
|
||||
(collection_names, collections) = get_used_collections(main_scene)
|
||||
all_collection_names = all_collection_names + list(collection_names)
|
||||
all_collections = all_collections + collections"""
|
||||
|
||||
# now that we have the collections that are in use by collection instances, check if those collections are actully present in the library scenes
|
||||
collections = []
|
||||
collection_names = []
|
||||
for library_scene in library_scenes:
|
||||
root_collection = library_scene.collection
|
||||
|
||||
for collection in traverse_tree(root_collection):
|
||||
collections.append(collection)
|
||||
collection_names.append(collection.name)
|
||||
return collection_names
|
||||
|
||||
|
||||
def get_collection_hierarchy(root_col, levels=1):
|
||||
"""Read hierarchy of the collections in the scene"""
|
||||
@ -159,4 +184,15 @@ def recurLayerCollection(layerColl, collName):
|
||||
found = recurLayerCollection(layer, collName)
|
||||
if found:
|
||||
return found
|
||||
# traverse the collection hierarchy updward until you find one collection inside target_collections
|
||||
def find_collection_ascendant_target_collection(collection_parents, target_collections, collection):
|
||||
if collection == None:
|
||||
return None
|
||||
if collection in target_collections:
|
||||
return collection
|
||||
if collection in collection_parents:
|
||||
parent = collection_parents[collection]
|
||||
else:
|
||||
return None
|
||||
return find_collection_ascendant_target_collection(collection_parents, target_collections, parent)
|
||||
|
||||
|
@ -74,7 +74,6 @@ def export_collections(collections, folder_path, library_scene, addon_prefs, glt
|
||||
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:
|
||||
@ -93,19 +92,21 @@ def export_collections(collections, folder_path, library_scene, addon_prefs, glt
|
||||
|
||||
|
||||
#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)
|
||||
# this is not needed if a collection/blueprint does not have sub blueprints or sub collections
|
||||
collection_in_blueprint_hierarchy = collection_name in blueprint_hierarchy and len(blueprint_hierarchy[collection_name]) > 0
|
||||
collection_has_child_collections = len(bpy.data.collections[collection_name].children) > 0
|
||||
if collection_in_blueprint_hierarchy or collection_has_child_collections:
|
||||
#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)
|
||||
(hollow_scene, temporary_collections, root_objects, special_properties) = generate_blueprint_hollow_scene(collection, library_collections, addon_prefs)
|
||||
|
||||
export_gltf(gltf_output_path, export_settings)
|
||||
|
||||
clear_blueprint_hollow_scene(hollow_scene, collection, object_names)
|
||||
clear_blueprint_hollow_scene(hollow_scene, collection, temporary_collections, root_objects, special_properties)
|
||||
bpy.context.window.scene = backup
|
||||
else:
|
||||
print("NORMAL")
|
||||
#print("standard export")
|
||||
export_gltf(gltf_output_path, export_settings)
|
||||
|
||||
|
||||
@ -134,13 +135,12 @@ def export_main_scenes(scenes, folder_path, addon_prefs):
|
||||
export_main_scene(scene, folder_path, addon_prefs)
|
||||
|
||||
def export_main_scene(scene, folder_path, addon_prefs, library_collections):
|
||||
export_output_folder = getattr(addon_prefs,"export_output_folder")
|
||||
gltf_export_preferences = generate_gltf_export_preferences(addon_prefs)
|
||||
|
||||
export_output_folder = getattr(addon_prefs,"export_output_folder")
|
||||
export_blueprints = getattr(addon_prefs,"export_blueprints")
|
||||
|
||||
if export_blueprints :
|
||||
(hollow_scene, object_names) = generate_hollow_scene(scene, library_collections)
|
||||
(hollow_scene, temporary_collections, root_objects, special_properties) = generate_hollow_scene(scene, library_collections, addon_prefs)
|
||||
#except Exception:
|
||||
# print("failed to create hollow scene")
|
||||
|
||||
@ -162,7 +162,7 @@ def export_main_scene(scene, folder_path, addon_prefs, library_collections):
|
||||
export_gltf(gltf_output_path, export_settings)
|
||||
|
||||
if export_blueprints :
|
||||
clear_hollow_scene(hollow_scene, scene, object_names)
|
||||
clear_hollow_scene(hollow_scene, scene, temporary_collections, root_objects, special_properties)
|
||||
|
||||
|
||||
#https://docs.blender.org/api/current/bpy.ops.export_scene.html#bpy.ops.export_scene.gltf
|
||||
|
@ -3,9 +3,10 @@ from .helpers_collections import (find_layer_collection_recursive)
|
||||
from .helpers import (make_empty3)
|
||||
|
||||
# generate a copy of a scene that replaces collection instances with empties
|
||||
# alternative: copy original names before creating a new scene, & reset them
|
||||
# or create empties, hide original ones, and do the same renaming trick
|
||||
def generate_hollow_scene(scene, library_collections):
|
||||
# copy original names before creating a new scene, & reset them
|
||||
def generate_hollow_scene(scene, library_collections, addon_prefs):
|
||||
collection_instances_combine_mode = getattr(addon_prefs, "collection_instances_combine_mode")
|
||||
|
||||
root_collection = scene.collection
|
||||
temp_scene = bpy.data.scenes.new(name="temp_scene")
|
||||
copy_root_collection = temp_scene.collection
|
||||
@ -18,13 +19,22 @@ def generate_hollow_scene(scene, library_collections):
|
||||
# once it's found, set the active layer collection to the one we found
|
||||
bpy.context.view_layer.active_layer_collection = found
|
||||
|
||||
#original_names = {}
|
||||
original_names = []
|
||||
temporary_collections = []
|
||||
root_objects = []
|
||||
special_properties= { # to be able to reset any special property afterwards
|
||||
"combine": [],
|
||||
}
|
||||
|
||||
# copies the contents of a collection into another one while replacing library instances with empties
|
||||
def copy_hollowed_collection_into(source_collection, destination_collection):
|
||||
def copy_hollowed_collection_into(source_collection, destination_collection, parent_empty=None):
|
||||
for object in source_collection.objects:
|
||||
if object.instance_type == 'COLLECTION' and (object.instance_collection.name in library_collections):
|
||||
#check if a specific collection instance does not have an ovveride for combine_mode
|
||||
combine_mode = object['_combine'] if '_combine' in object else collection_instances_combine_mode
|
||||
|
||||
if object.instance_type == 'COLLECTION' and (combine_mode == 'Split' or (combine_mode == 'EmbedExternal' and (object.instance_collection.name in library_collections)) ):
|
||||
#print("creating empty for", object.name, object.instance_collection.name, library_collections, combine_mode)
|
||||
|
||||
collection_name = object.instance_collection.name
|
||||
|
||||
original_name = object.name
|
||||
@ -37,28 +47,55 @@ def generate_hollow_scene(scene, library_collections):
|
||||
empty_obj['SpawnHere'] = ''
|
||||
|
||||
for k, v in object.items():
|
||||
if k != 'template' or k != '_combine': # do not copy these properties
|
||||
empty_obj[k] = v
|
||||
if parent_empty is not None:
|
||||
empty_obj.parent = parent_empty
|
||||
else:
|
||||
# we backup special properties that we do not want to export, and remove them
|
||||
if '_combine' in object:
|
||||
special_properties["combine"].append((object, object['_combine']))
|
||||
del object['_combine']
|
||||
|
||||
if parent_empty is not None:
|
||||
object.parent = parent_empty
|
||||
destination_collection.objects.link(object)
|
||||
else:
|
||||
root_objects.append(object)
|
||||
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:
|
||||
original_name = collection.name
|
||||
collection.name = original_name + "____bak"
|
||||
collection_placeholder = make_empty3(original_name, [0,0,0], [0,0,0], [1,1,1], destination_collection)
|
||||
|
||||
if parent_empty is not None:
|
||||
collection_placeholder.parent = parent_empty
|
||||
|
||||
copy_hollowed_collection_into(collection, destination_collection, collection_placeholder)
|
||||
|
||||
|
||||
"""
|
||||
copy_collection = bpy.data.collections.new(collection.name + "____collection_export")
|
||||
# save the newly created collection for later reuse
|
||||
temporary_collections.append(copy_collection)
|
||||
|
||||
# copy & link objects
|
||||
copy_hollowed_collection_into(collection, copy_collection)
|
||||
destination_collection.children.link(copy_collection)
|
||||
"""
|
||||
|
||||
copy_hollowed_collection_into(root_collection, copy_root_collection)
|
||||
|
||||
# objs = bpy.data.objects
|
||||
#objs.remove(objs["Cube"], do_unlink=True)
|
||||
return (temp_scene, original_names)
|
||||
return (temp_scene, temporary_collections, root_objects, special_properties)
|
||||
|
||||
# clear & remove "hollow scene"
|
||||
def clear_hollow_scene(temp_scene, original_scene, original_names):
|
||||
# reset original names
|
||||
root_collection = original_scene.collection
|
||||
def clear_hollow_scene(temp_scene, original_scene, temporary_collections, root_objects, special_properties):
|
||||
|
||||
def restore_original_names(collection):
|
||||
if collection.name.endswith("____bak"):
|
||||
collection.name = collection.name.replace("____bak", "")
|
||||
for object in collection.objects:
|
||||
if object.instance_type == 'COLLECTION':
|
||||
if object.name.endswith("____bak"):
|
||||
@ -66,19 +103,35 @@ def clear_hollow_scene(temp_scene, original_scene, original_names):
|
||||
for child_collection in collection.children:
|
||||
restore_original_names(child_collection)
|
||||
|
||||
# reset original names
|
||||
root_collection = original_scene.collection
|
||||
restore_original_names(root_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:
|
||||
temp_root_collection = temp_scene.collection
|
||||
temp_scene_objects = [o for o in temp_root_collection.objects]
|
||||
for object in temp_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)
|
||||
if object in root_objects:
|
||||
pass
|
||||
else:
|
||||
bpy.data.objects.remove(object, do_unlink=True)
|
||||
else:
|
||||
bpy.context.scene.collection.objects.unlink(object)
|
||||
|
||||
# remove temporary collections
|
||||
for collection in temporary_collections:
|
||||
bpy.data.collections.remove(collection)
|
||||
|
||||
# put back special properties
|
||||
for (object, value) in special_properties["combine"]:
|
||||
object['_combine'] = value
|
||||
|
||||
# remove the temporary scene
|
||||
bpy.data.scenes.remove(temp_scene)
|
||||
|
||||
|
||||
|
@ -18,7 +18,9 @@ AutoExportGltfPreferenceNames = [
|
||||
|
||||
'export_blueprints',
|
||||
'export_blueprints_path',
|
||||
'export_nested_blueprints',
|
||||
|
||||
'export_marked_assets',
|
||||
'collection_instances_combine_mode',
|
||||
|
||||
'export_materials_library',
|
||||
'export_materials_path',
|
||||
@ -61,7 +63,7 @@ class AutoExportGltfAddonPreferences(AddonPreferences):
|
||||
export_scene_settings: BoolProperty(
|
||||
name='Export scene settings',
|
||||
description='Export scene settings ie AmbientLighting, Bloom, AO etc',
|
||||
default=True
|
||||
default=False
|
||||
)
|
||||
|
||||
# blueprint settings
|
||||
@ -76,12 +78,6 @@ 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',
|
||||
@ -93,6 +89,32 @@ class AutoExportGltfAddonPreferences(AddonPreferences):
|
||||
default='materials'
|
||||
)
|
||||
|
||||
|
||||
""" 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
|
||||
- 'EmbedExternal': embed any instance of a non local collection (ie external assets)
|
||||
|
||||
- 'Inject': inject components from sub collection instances into the curent object => this is now a seperate custom property that you can apply to a collecion instance
|
||||
"""
|
||||
|
||||
collection_instances_combine_mode : EnumProperty(
|
||||
name='Collection instances',
|
||||
items=(
|
||||
('Split', 'Split', 'replace collection instances with an empty + blueprint, creating links to sub blueprints (Default, Recomended)'),
|
||||
('Embed', 'Embed', 'treat collection instances as embeded objects and do not replace them with an empty'),
|
||||
('EmbedExternal', 'EmbedExternal', 'treat instances of external (not specifified in the current blend file) collections (aka assets etc) as embeded objects and do not replace them with empties'),
|
||||
#('Inject', 'Inject', 'inject components from sub collection instances into the curent object')
|
||||
),
|
||||
default='Split'
|
||||
)
|
||||
|
||||
export_marked_assets: BoolProperty(
|
||||
name='Auto export marked assets',
|
||||
description='Collections that have been marked as assets will be systematically exported, even if not in use in another scene',
|
||||
default=True
|
||||
)
|
||||
|
||||
main_scenes: CollectionProperty(name="main scenes", type=CUSTOM_PG_sceneName)
|
||||
main_scenes_index: IntProperty(name = "Index for main scenes list", default = 0)
|
||||
|
||||
@ -102,7 +124,8 @@ class AutoExportGltfAddonPreferences(AddonPreferences):
|
||||
#####
|
||||
export_format: EnumProperty(
|
||||
name='Format',
|
||||
items=(('GLB', 'glTF Binary (.glb)',
|
||||
items=(
|
||||
('GLB', 'glTF Binary (.glb)',
|
||||
'Exports a single file, with all data packed in binary form. '
|
||||
'Most efficient and portable, but more difficult to edit later'),
|
||||
('GLTF_EMBEDDED', 'glTF Embedded (.gltf)',
|
||||
@ -110,7 +133,8 @@ class AutoExportGltfAddonPreferences(AddonPreferences):
|
||||
'Less efficient than binary, but easier to edit later'),
|
||||
('GLTF_SEPARATE', 'glTF Separate (.gltf + .bin + textures)',
|
||||
'Exports multiple files, with separate JSON, binary and texture data. '
|
||||
'Easiest to edit later')),
|
||||
'Easiest to edit later')
|
||||
),
|
||||
description=(
|
||||
'Output format and embedding options. Binary is most efficient, '
|
||||
'but JSON (embedded or separate) may be easier to edit later'
|
||||
|
@ -42,7 +42,8 @@ class AutoExportGLTF(Operator, AutoExportGltfAddonPreferences, ExportHelper):
|
||||
exceptional = [
|
||||
# options that don't start with 'export_'
|
||||
'main_scenes',
|
||||
'library_scenes'
|
||||
'library_scenes',
|
||||
'collection_instances_combine_mode',
|
||||
]
|
||||
all_props = self.properties
|
||||
export_props = {
|
||||
@ -65,7 +66,8 @@ class AutoExportGLTF(Operator, AutoExportGltfAddonPreferences, ExportHelper):
|
||||
exceptional = [
|
||||
# options that don't start with 'export_'
|
||||
'main_scenes',
|
||||
'library_scenes'
|
||||
'library_scenes',
|
||||
'collection_instances_combine_mode',
|
||||
]
|
||||
all_props = self.properties
|
||||
export_props = {
|
||||
@ -117,6 +119,10 @@ class AutoExportGLTF(Operator, AutoExportGltfAddonPreferences, ExportHelper):
|
||||
item = library_scenes.add()
|
||||
item.name = item_name
|
||||
|
||||
if hasattr(self, 'collection_instances_combine_mode'):
|
||||
bpy.context.preferences.addons["gltf_auto_export"].preferences.collection_instances_combine_mode = self.collection_instances_combine_mode
|
||||
|
||||
|
||||
except (AttributeError, TypeError):
|
||||
self.report({"ERROR"}, "Loading export settings failed. Removed corrupted settings")
|
||||
del context.scene[self.scene_key]
|
||||
@ -127,11 +133,8 @@ class AutoExportGLTF(Operator, AutoExportGltfAddonPreferences, ExportHelper):
|
||||
|
||||
addon_prefs = bpy.context.preferences.addons["gltf_auto_export"].preferences
|
||||
|
||||
|
||||
|
||||
[main_scene_names, level_scenes, library_scene_names, library_scenes]=get_scenes(addon_prefs)
|
||||
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)
|
||||
(collections, _) = get_exportable_collections(level_scenes, library_scenes, addon_prefs)
|
||||
|
||||
try:
|
||||
# we save this list of collections in the context
|
||||
@ -295,14 +298,19 @@ class GLTF_PT_auto_export_blueprints(bpy.types.Panel):
|
||||
|
||||
layout.active = operator.export_blueprints
|
||||
|
||||
# collections/blueprints
|
||||
layout.prop(operator, "export_blueprints_path")
|
||||
layout.prop(operator, "export_nested_blueprints")
|
||||
|
||||
layout.prop(operator, "collection_instances_combine_mode")
|
||||
layout.prop(operator, "export_marked_assets")
|
||||
layout.separator()
|
||||
# materials
|
||||
layout.prop(operator, "export_materials_library")
|
||||
layout.prop(operator, "export_materials_path")
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class GLTF_PT_auto_export_collections_list(bpy.types.Panel):
|
||||
bl_space_type = 'FILE_BROWSER'
|
||||
bl_region_type = 'TOOL_PROPS'
|
||||
|