feat(): Blueprints, crates, enhanced Blender tooling & more (#5)

* feat(bevy_gltf_components):
	* create crate 
	* added SystemSet (GltfComponentsSet) to run process_loaded_scenes (where components are injected)
	in a specific systemset & allow ordering other systems relative to it

* feat(bevy_gltf_blueprints): 
	* created crate
	* made the blueprint library path configurable
	* added BluePrintBundle helper
	* added SystemSet (GltfBlueprintsSet) for better system ordering
	* integrated into advanced demo

* feat(tools-blender-auto-export): 
	* renamed blender tool to gltf_auto_export
   	* rewritten auto_export
	* added blueprint / prefab support
		* creates scene with empties with BlueprintName components in the scene
		* export of the main scene now exports this scene instead of real main scene
		* changes collection stand in names in original scene & sets them back after export
			to have correctly named collection instance exports
		* also added an additional 'SpawnHere' component to not conflate BlueprintNames & spawning requests
   		* toggling & blueprint library output parameters added
   		* added correct handling/ restoring of saved selection when using blueprints

* feat(examples): 
	* added advanced example
	* general example renamed to "basic", and cleaned up

* feat(various): a lot of experiments with saving & loading etc

* chore(assets): updated blend & generated assets

* fix(examples-advanced): disabling hot reloading as it messes up scenes in experiments with save & loading

* docs(): 
	* added & fleshing out docs for the various crates & main README
	* added process doc image & tweaks to README
 	* added missing licence info where relevant
 	* fixed broken links
 	* clarified some aspects
 	* added updated screenshots where relevant
 	* added tweaks & improvements etc
This commit is contained in:
Mark Moissette 2023-09-28 14:10:45 +02:00 committed by GitHub
parent d9060a4d6b
commit 528e13a250
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
95 changed files with 6178 additions and 491 deletions

350
Cargo.lock generated
View File

@ -309,10 +309,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
[[package]] [[package]]
name = "bevy" name = "base64"
version = "0.11.0" version = "0.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04a90fe8e9c03fa2d30acf39a5178a48526df00c1ccea2fc43fa6d9ca4d8a168" checksum = "9ba43ea6f343b788c8764558649e08df62f86c6ef251fdaeb1ffd010a9ae50a2"
[[package]]
name = "bevy"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18e71a9143ac21bed247c30129399af8be170309e7ff5983a1bd37e87d3da520"
dependencies = [ dependencies = [
"bevy_internal", "bevy_internal",
] ]
@ -353,14 +359,14 @@ checksum = "0edba455601861b8e8b76128ae5d46dd968114edde60f0ac3d2c21535a947548"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.27", "syn 2.0.37",
] ]
[[package]] [[package]]
name = "bevy_a11y" name = "bevy_a11y"
version = "0.11.0" version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f758f437d9d862bf10a8e3a0f76b426095c19a87d118c945dcb935358d856076" checksum = "3d87d5753cefaeb5f5c5d5e937844f5386eabaf9ab95f3013e86d8fb438d424e"
dependencies = [ dependencies = [
"accesskit", "accesskit",
"bevy_app", "bevy_app",
@ -370,9 +376,9 @@ dependencies = [
[[package]] [[package]]
name = "bevy_animation" name = "bevy_animation"
version = "0.11.0" version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d130cb8b7e2b81304591c5c8e511accd2df58b8d8185ab4836ed2f377e6a61f" checksum = "cc510d47ec4813359b7e715edc6976380d4244833feae124977468994554a0ab"
dependencies = [ dependencies = [
"bevy_app", "bevy_app",
"bevy_asset", "bevy_asset",
@ -389,9 +395,9 @@ dependencies = [
[[package]] [[package]]
name = "bevy_app" name = "bevy_app"
version = "0.11.0" version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1817e8d5b1146ea9e7730a7264d3470394840e0754d15abded26473f867967a0" checksum = "3cb660188d5d4ceaead6d5861ce22ecedc08b68c385cc8edf0a3c0c0285560bf"
dependencies = [ dependencies = [
"bevy_derive", "bevy_derive",
"bevy_ecs", "bevy_ecs",
@ -405,11 +411,12 @@ dependencies = [
[[package]] [[package]]
name = "bevy_asset" name = "bevy_asset"
version = "0.11.0" version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e12f951d4af2ad4ad230cd7bcb05248149c415eec17c34bf26731c4cd8b897f" checksum = "6733d20a22bb10da928785fdbf6fad6cfb1c7da4a8c170ab3adbba5862c375d5"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-channel",
"bevy_app", "bevy_app",
"bevy_diagnostic", "bevy_diagnostic",
"bevy_ecs", "bevy_ecs",
@ -432,10 +439,35 @@ dependencies = [
] ]
[[package]] [[package]]
name = "bevy_audio" name = "bevy_asset_loader"
version = "0.11.0" version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6bade3f5389f9463e150af874aebe672b5101df4268d28b0109a66f9cdce56e" checksum = "67504fe2bfe56b0c58a2e042a7f5b1dd242a9b9f28e78e3fe63e2c3d62692bea"
dependencies = [
"anyhow",
"bevy",
"bevy_asset_loader_derive",
"bevy_common_assets",
"path-slash",
"serde",
]
[[package]]
name = "bevy_asset_loader_derive"
version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf0d73cdac61fd6f9cb4df9c936ffe6d1343a63470c91c6a6823e1664d298fcb"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "bevy_audio"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8f505704c3a8396c2d9dde898e19aee114143584b11bdb7189fcafc231b6e7a"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bevy_app", "bevy_app",
@ -452,10 +484,22 @@ dependencies = [
] ]
[[package]] [[package]]
name = "bevy_core" name = "bevy_common_assets"
version = "0.11.0" version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "263b6a943ecba176c8390a1100615021f61a3b2d7a87e8eecf4009b6ed4457e0" checksum = "a0e5659f20aeaa1703e76d87c62d66f92aaa56e431fbed71bb38345b576aa6f0"
dependencies = [
"anyhow",
"bevy",
"ron",
"serde",
]
[[package]]
name = "bevy_core"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1d47b435bdebeefedda95de98a419c4d3b4c160ed8ce3470ea358a16aad6038"
dependencies = [ dependencies = [
"bevy_app", "bevy_app",
"bevy_ecs", "bevy_ecs",
@ -469,9 +513,9 @@ dependencies = [
[[package]] [[package]]
name = "bevy_core_pipeline" name = "bevy_core_pipeline"
version = "0.11.0" version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50c70113b5c4106855b888f96d8574697eb9082713f976c9b6487c1f5ab28589" checksum = "a01c2652f5a6d24e0ab465e6feca8a034dfb33dfefbcdc19e436fec879a362a8"
dependencies = [ dependencies = [
"bevy_app", "bevy_app",
"bevy_asset", "bevy_asset",
@ -490,20 +534,20 @@ dependencies = [
[[package]] [[package]]
name = "bevy_derive" name = "bevy_derive"
version = "0.11.0" version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1477347b17df781756ba0dfd677e2975e57e930752cd3cd42e6cdd8fdaa3223" checksum = "c5cc78985f4d0ad1fd7b8ead06dcfaa192685775a7b1be158191c788c7d52298"
dependencies = [ dependencies = [
"bevy_macro_utils", "bevy_macro_utils",
"quote", "quote",
"syn 2.0.27", "syn 2.0.37",
] ]
[[package]] [[package]]
name = "bevy_diagnostic" name = "bevy_diagnostic"
version = "0.11.0" version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37a594f970c261007cdd3edeccd61651c2cb4513de3d0b8b35d93f5d9c32c059" checksum = "ee4e366724d233fdc7e282e1415f4cd570e97fbb8443e5a8ff3f8cc5ae253ffd"
dependencies = [ dependencies = [
"bevy_app", "bevy_app",
"bevy_core", "bevy_core",
@ -516,9 +560,9 @@ dependencies = [
[[package]] [[package]]
name = "bevy_ecs" name = "bevy_ecs"
version = "0.11.0" version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "032c81ba7d919c1004b0abc33cc6c588c8f896a4d7c55a7c7aa1e46382242f43" checksum = "fb6fd0ec64cd32b8fcf16157173431ba0e675b29c4643a8d6c9a9eeef6f93c32"
dependencies = [ dependencies = [
"async-channel", "async-channel",
"bevy_ecs_macros", "bevy_ecs_macros",
@ -537,14 +581,14 @@ dependencies = [
[[package]] [[package]]
name = "bevy_ecs_macros" name = "bevy_ecs_macros"
version = "0.11.0" version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a15ff7fcafdb8fe464ddd300b4860a76d5c6f9d684472e4bf21852d6f0ff3991" checksum = "e13b0fd864433db6ff825efd0eb86b2690e208151905b184cc9bfd2c3ac66c3b"
dependencies = [ dependencies = [
"bevy_macro_utils", "bevy_macro_utils",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.27", "syn 2.0.37",
] ]
[[package]] [[package]]
@ -600,9 +644,9 @@ dependencies = [
[[package]] [[package]]
name = "bevy_encase_derive" name = "bevy_encase_derive"
version = "0.11.0" version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6bdf808dbdc68a0c519e09026c627bda85250205a40ac02794866bff254d6b56" checksum = "da8ffb3d214ee4d8b7e851bc8409768a0f18c872c3a25065c248611ff832180e"
dependencies = [ dependencies = [
"bevy_macro_utils", "bevy_macro_utils",
"encase_derive_impl", "encase_derive_impl",
@ -610,9 +654,9 @@ dependencies = [
[[package]] [[package]]
name = "bevy_gilrs" name = "bevy_gilrs"
version = "0.11.0" version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b877a371caa64edd6ec5d66b47c67b9e9e9acff2f3bcc51e31e175463e89f6ba" checksum = "b84a2fbca3811261bcf02908132096595b81e5ec033169f922d6077f57baabb7"
dependencies = [ dependencies = [
"bevy_app", "bevy_app",
"bevy_ecs", "bevy_ecs",
@ -626,9 +670,9 @@ dependencies = [
[[package]] [[package]]
name = "bevy_gizmos" name = "bevy_gizmos"
version = "0.11.0" version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7938b43b4bdf9d039b7d3b310f871ed5ffa5a185e861a9c85731c40182019f8d" checksum = "64c08196fcb36b7175577443cbe2c1193d596a15eb0fa210bae378e6e1478876"
dependencies = [ dependencies = [
"bevy_app", "bevy_app",
"bevy_asset", "bevy_asset",
@ -646,12 +690,12 @@ dependencies = [
[[package]] [[package]]
name = "bevy_gltf" name = "bevy_gltf"
version = "0.11.0" version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f09b699698a2f5843ef63064010a5e7783403f99a697a04f41a2f8141cb4245d" checksum = "478c832d8b132198ca9485c636c97eaea7a1fe393dabadbcabc693ef4437e0db"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"base64", "base64 0.13.1",
"bevy_animation", "bevy_animation",
"bevy_app", "bevy_app",
"bevy_asset", "bevy_asset",
@ -676,21 +720,53 @@ dependencies = [
] ]
[[package]] [[package]]
name = "bevy_gltf_components" name = "bevy_gltf_blueprints"
version = "0.2.0" version = "0.1.0"
dependencies = [ dependencies = [
"bevy", "bevy",
"bevy_gltf_components 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "bevy_gltf_components"
version = "0.1.0"
dependencies = [
"bevy",
"ron",
"serde",
]
[[package]]
name = "bevy_gltf_components"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "168c88211436e833f6f5104d2f2db8a9f3d705b6c6ae22b921f1be6b80b7d4cb"
dependencies = [
"bevy",
"ron",
"serde",
]
[[package]]
name = "bevy_gltf_flow"
version = "0.3.0"
dependencies = [
"bevy",
"bevy_asset_loader",
"bevy_editor_pls", "bevy_editor_pls",
"bevy_gltf_blueprints",
"bevy_gltf_components 0.1.0",
"bevy_rapier3d", "bevy_rapier3d",
"rand",
"ron", "ron",
"serde", "serde",
] ]
[[package]] [[package]]
name = "bevy_hierarchy" name = "bevy_hierarchy"
version = "0.11.0" version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba41e1bb0c367b31e59b53ab858de56764c78bee87c121843c1ff033efa0086c" checksum = "402789ee53acf345243cf2c86a895d6cf8631e533780ed261c6ecf891eb050b8"
dependencies = [ dependencies = [
"bevy_app", "bevy_app",
"bevy_core", "bevy_core",
@ -703,9 +779,9 @@ dependencies = [
[[package]] [[package]]
name = "bevy_input" name = "bevy_input"
version = "0.11.0" version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7221091c7b219a63a1f3f019512e8b72bed673230b97c3fcbca37ba566b1cffb" checksum = "b9dfd9c768cf153f3fc807661996b2db44b824299860ba198fb3b92dd731bdd8"
dependencies = [ dependencies = [
"bevy_app", "bevy_app",
"bevy_ecs", "bevy_ecs",
@ -718,9 +794,9 @@ dependencies = [
[[package]] [[package]]
name = "bevy_internal" name = "bevy_internal"
version = "0.11.0" version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f232e7bd2566abd05656789e3c6278a5ca2a24f1232dff525e5b0233a99a610" checksum = "1006f2c501bceb1aef5cc18ed07eb822f295763227b83ba6887e6743698af9f8"
dependencies = [ dependencies = [
"bevy_a11y", "bevy_a11y",
"bevy_animation", "bevy_animation",
@ -757,9 +833,9 @@ dependencies = [
[[package]] [[package]]
name = "bevy_log" name = "bevy_log"
version = "0.11.0" version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "487dfd1fc75fada8f3f2f4773addf3fbba53a2a91cb913616e6dc6c26dd62995" checksum = "f0b1e5ca5b217dd384a3bf9186df0d0da757035f9f06d8eec0ebe62cffc05c34"
dependencies = [ dependencies = [
"android_log-sys", "android_log-sys",
"bevy_app", "bevy_app",
@ -773,21 +849,21 @@ dependencies = [
[[package]] [[package]]
name = "bevy_macro_utils" name = "bevy_macro_utils"
version = "0.11.0" version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd3868e555723249fde3786891f35893b3001b2be4efb51f431467cb7fc378cd" checksum = "d1cd460205fe05634d58b32d9bb752b1b4eaf32b2d29cbd4161ba35eb44a2f8c"
dependencies = [ dependencies = [
"quote", "quote",
"rustc-hash", "rustc-hash",
"syn 2.0.27", "syn 2.0.37",
"toml_edit", "toml_edit",
] ]
[[package]] [[package]]
name = "bevy_math" name = "bevy_math"
version = "0.11.0" version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25088c6598fe0b8ded992c781dc49e613993c7a4e6a731c0f2ab0408add6afdb" checksum = "267f2ec44aa948051768b1320c2dbff0871799e0a3b746f5fe5b6ee7258fbaf5"
dependencies = [ dependencies = [
"glam", "glam",
"serde", "serde",
@ -795,9 +871,9 @@ dependencies = [
[[package]] [[package]]
name = "bevy_mikktspace" name = "bevy_mikktspace"
version = "0.11.0" version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99dde80034660f7dbb473141c31f0a746acc7229f5a06ce769aba5f16fd592ab" checksum = "0d98eaa7f40b97bb9b2d681ecf9fd439830a7eb88ad2846680d4f4acd285cf84"
dependencies = [ dependencies = [
"glam", "glam",
] ]
@ -819,9 +895,9 @@ dependencies = [
[[package]] [[package]]
name = "bevy_pbr" name = "bevy_pbr"
version = "0.11.0" version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3efec2ae4b4f9fd38b82b93350499dac2dc6f07e63ef50a03c00c52075e2dea8" checksum = "9386944248ac8fcaaabec2302b0e7cd8196ef5d7b7a2e63b10ace3eeb813d3de"
dependencies = [ dependencies = [
"bevy_app", "bevy_app",
"bevy_asset", "bevy_asset",
@ -842,9 +918,9 @@ dependencies = [
[[package]] [[package]]
name = "bevy_ptr" name = "bevy_ptr"
version = "0.11.0" version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c74fcf37593a0053f539c3b088f34f268cbefed031d8eb8ff0fb10d175160242" checksum = "15702dff420fac72c2ab92428a8600e079ae89c5845401c4e39b843665a3d2d0"
[[package]] [[package]]
name = "bevy_rapier3d" name = "bevy_rapier3d"
@ -862,9 +938,9 @@ dependencies = [
[[package]] [[package]]
name = "bevy_reflect" name = "bevy_reflect"
version = "0.11.0" version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "362492a6b66f676176705cc06017b012320fa260a9cf4baf3513387e9c05693e" checksum = "3ac66cccbf1457c5cfc004a0e83569bd4ddc5d6310701f4af6aa81001fe2964a"
dependencies = [ dependencies = [
"bevy_math", "bevy_math",
"bevy_ptr", "bevy_ptr",
@ -883,23 +959,23 @@ dependencies = [
[[package]] [[package]]
name = "bevy_reflect_derive" name = "bevy_reflect_derive"
version = "0.11.0" version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e974d78eaf1b45e1b4146711b5c16e37c24234e12f3a52f5f2e28332c969d3c" checksum = "e5a2a1fa784e9a22560b9de350246a159cd59239eb61c7b66824031b3b28abb0"
dependencies = [ dependencies = [
"bevy_macro_utils", "bevy_macro_utils",
"bit-set", "bit-set",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.27", "syn 2.0.37",
"uuid", "uuid",
] ]
[[package]] [[package]]
name = "bevy_render" name = "bevy_render"
version = "0.11.0" version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46e4b6a82c3a2be1c0d0cbecf62debb8251b72c0ae76285f66265aabc5bf2d37" checksum = "b2e125de06ffaed8100623843b447fb524dc16f2a2120adce81143d7307278be"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-channel", "async-channel",
@ -947,21 +1023,21 @@ dependencies = [
[[package]] [[package]]
name = "bevy_render_macros" name = "bevy_render_macros"
version = "0.11.0" version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07c4d937f966644f5e1e3c9157736acdd36286bcce06142ff9ad25cd71348c09" checksum = "e283eb4156285d0d4b85ce9b959d080b652165848f0b3f1a8770af6cfad41c34"
dependencies = [ dependencies = [
"bevy_macro_utils", "bevy_macro_utils",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.27", "syn 2.0.37",
] ]
[[package]] [[package]]
name = "bevy_scene" name = "bevy_scene"
version = "0.11.0" version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf1e00eb30e2053d9fff0802b2f557350b4e66bac58d531de30882048b4e3232" checksum = "a77172c572239e741283e585433e986dd7f1bfdd7f7489ff10933f59e93cbb04"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bevy_app", "bevy_app",
@ -981,9 +1057,9 @@ dependencies = [
[[package]] [[package]]
name = "bevy_sprite" name = "bevy_sprite"
version = "0.11.0" version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03f64119444ef9788dcdd05012a60f0fa3b7ddb396d434ebcfc3edefd76c91b5" checksum = "d7811ade4df81ffa6bae0e293c42d784ad88ce84d2b10fa05801e3c368714581"
dependencies = [ dependencies = [
"bevy_app", "bevy_app",
"bevy_asset", "bevy_asset",
@ -1006,9 +1082,9 @@ dependencies = [
[[package]] [[package]]
name = "bevy_tasks" name = "bevy_tasks"
version = "0.11.0" version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "faab904296a3d6976bb8a12bc0f42f6c98fb6cd87a96244e0151d359f684ec2d" checksum = "9200e7b42d49c787d9a08675c425d8bd6393ba3beed2eac64be6027a44a01870"
dependencies = [ dependencies = [
"async-channel", "async-channel",
"async-executor", "async-executor",
@ -1020,9 +1096,9 @@ dependencies = [
[[package]] [[package]]
name = "bevy_text" name = "bevy_text"
version = "0.11.0" version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b52a19b3d0caf20acd1bdb47b6a00717accc834b46c4f204a63de15cea45ec4b" checksum = "822e2cdb1c46338b85b7648d866f0b631cab23bd8f24395bb8ee7842dde024f0"
dependencies = [ dependencies = [
"ab_glyph", "ab_glyph",
"anyhow", "anyhow",
@ -1043,9 +1119,9 @@ dependencies = [
[[package]] [[package]]
name = "bevy_time" name = "bevy_time"
version = "0.11.0" version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d09225ad2ffef14da000080143730b36ba225844ae479e4791cdb9d08066d06a" checksum = "2ba50bf25c4dc40296b744f77de10d39c8981b710d8dce609da9de5e54ef164b"
dependencies = [ dependencies = [
"bevy_app", "bevy_app",
"bevy_ecs", "bevy_ecs",
@ -1058,9 +1134,9 @@ dependencies = [
[[package]] [[package]]
name = "bevy_transform" name = "bevy_transform"
version = "0.11.0" version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da8a0cd3780e120e20be333cc48d41cb74620d798dc61bc18eb2a82d3545e184" checksum = "86043ec5cc3cf406d33c0e4d6920171601e186b1288e9b4f5ae54682a0564032"
dependencies = [ dependencies = [
"bevy_app", "bevy_app",
"bevy_ecs", "bevy_ecs",
@ -1072,9 +1148,9 @@ dependencies = [
[[package]] [[package]]
name = "bevy_ui" name = "bevy_ui"
version = "0.11.0" version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb85992eb5809936b3326940dc8d6e7b219af3dde1ecbca5948addd6a78694cc" checksum = "105c82a79df1dfcdb75941fa1f58dbdbd8f702a740bc39e7bf719c3d51d55286"
dependencies = [ dependencies = [
"bevy_a11y", "bevy_a11y",
"bevy_app", "bevy_app",
@ -1102,9 +1178,9 @@ dependencies = [
[[package]] [[package]]
name = "bevy_utils" name = "bevy_utils"
version = "0.11.0" version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10bfde141f0cdd15e07bca72f4439a9db80877c283738f581d061972ef483b1b" checksum = "829eb8d0d06a0baeabc2e8bad74136ed3329b055aa1e11c5d9df09ebb9be3d85"
dependencies = [ dependencies = [
"ahash 0.8.3", "ahash 0.8.3",
"bevy_utils_proc_macros", "bevy_utils_proc_macros",
@ -1119,20 +1195,20 @@ dependencies = [
[[package]] [[package]]
name = "bevy_utils_proc_macros" name = "bevy_utils_proc_macros"
version = "0.11.0" version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e37f2e885b0e8af59dc19871c313d3cf2a2495db35bb4d4ae0a61b3f87d5401" checksum = "0d104f29e231123c703e8b394e2341d2425c33c5a2e9ab8cc8d0a554bdb62a41"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.27", "syn 2.0.37",
] ]
[[package]] [[package]]
name = "bevy_window" name = "bevy_window"
version = "0.11.0" version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0528832361e3d942df287c90537ef6fafb726c4934468a7c3a5d53d659bfbf54" checksum = "4a66f9963152093220fc5ffd2244cadcc7f79cf2c23dd02076f4d0cbb7f5162f"
dependencies = [ dependencies = [
"bevy_app", "bevy_app",
"bevy_ecs", "bevy_ecs",
@ -1146,9 +1222,9 @@ dependencies = [
[[package]] [[package]]
name = "bevy_winit" name = "bevy_winit"
version = "0.11.0" version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24c6709dc70cfee1eb94d5f125d29612c4a9345dfc1a70dd3189af927b2fd503" checksum = "003e20cff652a364f1f98b0d5ba24f53140dc77643241afe4a9b848bdde66184"
dependencies = [ dependencies = [
"accesskit_winit", "accesskit_winit",
"approx", "approx",
@ -1218,6 +1294,9 @@ name = "bitflags"
version = "2.3.3" version = "2.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42" checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "block" name = "block"
@ -1278,7 +1357,7 @@ checksum = "fdde5c9cd29ebd706ce1b35600920a33550e402fc998a2e53ad3b42c3c47a192"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.27", "syn 2.0.37",
] ]
[[package]] [[package]]
@ -1720,7 +1799,7 @@ checksum = "3fe2568f851fd6144a45fa91cfed8fe5ca8fc0b56ba6797bfc1ed2771b90e37c"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.27", "syn 2.0.37",
] ]
[[package]] [[package]]
@ -2001,7 +2080,7 @@ dependencies = [
"inflections", "inflections",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.27", "syn 2.0.37",
] ]
[[package]] [[package]]
@ -2835,7 +2914,7 @@ dependencies = [
"proc-macro-crate", "proc-macro-crate",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.27", "syn 2.0.37",
] ]
[[package]] [[package]]
@ -3048,6 +3127,12 @@ version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c"
[[package]]
name = "path-slash"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e91099d4268b0e11973f036e885d652fb0b21fedcf69738c627f94db6a44f42"
[[package]] [[package]]
name = "peeking_take_while" name = "peeking_take_while"
version = "0.1.2" version = "0.1.2"
@ -3104,6 +3189,12 @@ dependencies = [
"unicode-xid", "unicode-xid",
] ]
[[package]]
name = "ppv-lite86"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
[[package]] [[package]]
name = "pretty-type-name" name = "pretty-type-name"
version = "1.0.1" version = "1.0.1"
@ -3146,9 +3237,9 @@ dependencies = [
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.66" version = "1.0.67"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" checksum = "3d433d9f1a3e8c1263d9456598b16fec66f4acc9a74dacffd35c7bb09b3a1328"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
@ -3174,6 +3265,36 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17fd96390ed3feda12e1dfe2645ed587e0bea749e319333f104a33ff62f77a0b" checksum = "17fd96390ed3feda12e1dfe2645ed587e0bea749e319333f104a33ff62f77a0b"
[[package]]
name = "rand"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
"rand_chacha",
"rand_core",
]
[[package]]
name = "rand_chacha"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom",
]
[[package]] [[package]]
name = "range-alloc" name = "range-alloc"
version = "0.1.3" version = "0.1.3"
@ -3305,13 +3426,14 @@ dependencies = [
[[package]] [[package]]
name = "ron" name = "ron"
version = "0.8.0" version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "300a51053b1cb55c80b7a9fde4120726ddf25ca241a1cbb926626f62fb136bff" checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94"
dependencies = [ dependencies = [
"base64", "base64 0.21.4",
"bitflags 1.3.2", "bitflags 2.3.3",
"serde", "serde",
"serde_derive",
] ]
[[package]] [[package]]
@ -3369,22 +3491,22 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.175" version = "1.0.188"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d25439cd7397d044e2748a6fe2432b5e85db703d6d097bd014b3c0ad1ebff0b" checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e"
dependencies = [ dependencies = [
"serde_derive", "serde_derive",
] ]
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.175" version = "1.0.188"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b23f7ade6f110613c0d63858ddb8b94c1041f550eab58a16b371bdf2c9c80ab4" checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.27", "syn 2.0.37",
] ]
[[package]] [[package]]
@ -3522,9 +3644,9 @@ dependencies = [
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.27" version = "2.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b60f673f44a8255b9c8c657daf66a596d435f2da81a555b06dc644d080ba45e0" checksum = "7303ef2c05cd654186cb250d29049a24840ca25d2747c25c0381c8d9e2f582e8"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -3603,7 +3725,7 @@ checksum = "090198534930841fab3a5d1bb637cde49e339654e606195f8d9c76eeb081dc96"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.27", "syn 2.0.37",
] ]
[[package]] [[package]]
@ -3679,7 +3801,7 @@ checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.27", "syn 2.0.37",
] ]
[[package]] [[package]]
@ -3869,7 +3991,7 @@ dependencies = [
"once_cell", "once_cell",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.27", "syn 2.0.37",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
@ -3903,7 +4025,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.27", "syn 2.0.37",
"wasm-bindgen-backend", "wasm-bindgen-backend",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]

View File

@ -1,22 +1,38 @@
[package] [package]
name = "bevy_gltf_components" name = "bevy_gltf_flow"
version = "0.2.0" version = "0.3.0"
edition = "2021" edition = "2021"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
[workspace]
members = [
"crates/bevy_gltf_components",
"crates/bevy_gltf_blueprints",
]
[dev-dependencies] [dev-dependencies]
bevy="0.11" bevy="0.11.2"
bevy_rapier3d = { version = "0.22.0", features = [ "serde-serialize", "debug-render-3d", "enhanced-determinism"] } bevy_rapier3d = { version = "0.22.0", features = [ "serde-serialize", "debug-render-3d", "enhanced-determinism"] }
bevy_editor_pls = { git="https://github.com/jakobhellermann/bevy_editor_pls.git" } bevy_editor_pls = { git="https://github.com/jakobhellermann/bevy_editor_pls.git" }
bevy_asset_loader = { version = "0.17.0", features = ["standard_dynamic_assets" ]} #version = "0.16",
rand = "0.8.5"
[dependencies] [dependencies]
bevy = { version = "0.11", default-features = false, features = ["bevy_asset", "bevy_scene", "bevy_gltf"] } bevy = { version = "0.11.2", default-features = false, features = ["bevy_asset", "bevy_scene", "bevy_gltf"] }
bevy_gltf_components = { path = "crates/bevy_gltf_components" }
bevy_gltf_blueprints = { path = "crates/bevy_gltf_blueprints" }
serde = "*" serde = "*"
ron="*" ron="*"
[[example]] [[example]]
name = "general" name = "basic"
path = "examples/general/main.rs" path = "examples/basic/main.rs"
[[example]]
name = "advanced"
path = "examples/advanced/main.rs"
#### --------------------Dev/ debug------------------------------- #### --------------------Dev/ debug-------------------------------
# Enable high optimizations for dependencies (incl. Bevy), but not for our code: # Enable high optimizations for dependencies (incl. Bevy), but not for our code:

4
LICENCE.md Normal file
View File

@ -0,0 +1,4 @@
This crate is available under either:
* The [MIT License](./LICENSE_MIT)
* The [Apache License, Version 2.0](./LICENSE_APACHE)

289
README.md
View File

@ -1,11 +1,12 @@
[![Bevy tracking](https://img.shields.io/badge/Bevy%20tracking-released%20version-lightblue)](https://github.com/bevyengine/bevy/blob/main/docs/plugins_guidelines.md#main-branch-tracking) [![Bevy tracking](https://img.shields.io/badge/Bevy%20tracking-released%20version-lightblue)](https://github.com/bevyengine/bevy/blob/main/docs/plugins_guidelines.md#main-branch-tracking)
[![License](https://img.shields.io/crates/l/bevy_gltf_components)](https://github.com/kaosat-dev/Blender_bevy_components_worklflow/blob/main/License.md)
# bevy_gltf_components # Blender_bevy_components_worklflow
![demo](./docs/blender_gltf_components.png) ![demo](./docs/blender_gltf_components.png)
A plugin & tools for adding components from gltf files in the [bevy](https://bevyengine.org/) game engine. Crates & tools for adding components from gltf files in the [Bevy](https://bevyengine.org/) game engine.
It enables minimalistic [Blender](https://www.blender.org/) (gltf) centric workflow for Bevy, ie defining entites & their components It enables minimalistic [Blender](https://www.blender.org/) (gltf) centric workflow for Bevy, ie defining entites & their components
inside Blender using Blender's objects **custom properties**. inside Blender using Blender's objects **custom properties**.
@ -16,219 +17,46 @@ It also allows you to setup 'blueprints' in Blender by using collections (the re
## Features ## Features
* Useful if you want to use Blender (or any editor allowing to export gltf with configurable gltf_extras) as your Editor * Useful if you want to use Blender (or any editor allowing to export gltf with configurable gltf_extras) as your Editor
* define Bevy components as custom properties in Blender (RON, though an older JSON version is also available) * define Bevy components as custom properties in Blender (some visually , some using RON, though an older JSON version is also available)
* no plugin or extra tools needed in Blender (but I provide a little Blender plugin to auto-export to gltf on save if you want !) * no plugin or extra tools needed in Blender (but I provide a [little Blender plugin](./tools/gltf_auto_export/README.md) to auto-export to gltf on save (and more !) if you want !)
* define components in Blender Collections & override any of them in your collection instances if you want * define components in Blender Collections & override any of them in your collection instances if you want
* ability to automatically turn your Blender collections into [gltf Blueprints](./crates/bevy_gltf_blueprints/README.md) for reuse
* minimal setup & code, you can have something basic running fast * minimal setup & code, you can have something basic running fast
* minimal dependencies: Bevy, Serde & Ron only !
## Usage
***important*** : the plugin for processing gltf files runs in ***preUpdate*** , so you cannot use the components directly if you spawn your scene from gltf in ***setup*** (the additional components will not show up)
Please see the included example or use bevy_asset_loader for a reliable workflow
1. Add the crate to your dependencies
```toml
[dependencies]
bevy_gltf_components = { git="https://github.com/kaosat-dev/Blender_bevy_components_worklflow.git" }
```
(not on crates.io yet !)
2. Import
```rust
use bevy_gltf_components::ComponentsFromGltfPlugin;
```
3. Add the plugin
```rust
.add_plugin(ComponentsFromGltfPlugin)
```
### Example (without bevy_asset_loader)
See [here](./examples/general/main.rs) for more details
```rust
use bevy::prelude::*;
use bevy_gltf_components::ComponentsFromGltfPlugin;
#[derive(Component, Reflect, Default, Debug, Deref, DerefMut)]
#[reflect(Component)]
struct TuppleTestF32(f32);
#[derive(Component, Reflect, Default, Debug, )]
#[reflect(Component)]
/// helper marker component, for demo only
pub struct LoadedMarker;
#[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Hash, States)]
enum AppState {
#[default]
Loading,
Running,
}
fn main(){
App::new()
.add_plugins((
DefaultPlugins,
ComponentsFromGltfPlugin,
))
.add_state::<AppState>()
.add_systems(Startup, setup)
.add_systems(Update, (
spawn_level.run_if(in_state(AppState::Loading)),
))
.run();
}
#[derive(Resource)]
struct AssetLoadHelper(Handle<Scene>);
// we preload the data here, but this is for DEMO PURPOSES ONLY !! Please use https://github.com/NiklasEi/bevy_asset_loader or a similar logic to seperate loading / pre processing
// of assets from the spawning
// AssetLoadHelper is also just for the same purpose, you do not need it in a real scenario
// the states here are also for demo purposes only,
fn setup(
mut commands: Commands,
asset_server: Res<AssetServer>,
) {
let tmp: Handle<Scene> = asset_server.load("models/level1.glb#Scene0");
commands.insert_resource(AssetLoadHelper(tmp));
}
fn spawn_level(
mut commands: Commands,
scene_markers: Query<&LoadedMarker>,
preloaded_scene: Res<AssetLoadHelper>,
mut asset_event_reader: EventReader<AssetEvent<Gltf>>,
mut next_state: ResMut<NextState<AppState>>,
){
if let Some(asset_event) = asset_event_reader.iter().next() {
match asset_event {
AssetEvent::Created { handle: _ } => {
info!("GLTF loaded");
if scene_markers.is_empty() {
info!("spawning scene");
commands.spawn(
(
SceneBundle {
scene: preloaded_scene.0.clone(),
..default()
},
LoadedMarker,
Name::new("Level1")
)
);
next_state.set(AppState::Running);
}
}
_ => ()
}
}
}
```
### Example (with bevy_asset_loader, recommended for ease of use)
- follow [bevy_asset_loader's](https://github.com/NiklasEi/bevy_asset_loader) docs to setup your gltf asset loading
- also add this plugin
```rust
use bevy::prelude::*;
use bevy_gltf_components::ComponentsFromGltfPlugin;
// replace this with your actual states !
#[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Hash, States)]
enum AppState {
#[default]
Loading,
Running,
}
fn main(){
App::new()
.add_plugins((
DefaultPlugins,
ComponentsFromGltfPlugin,
))
// do the setup for bevy_asset_loader
.add_system(setup_game.in_schedule(OnEnter(AppState::Running)))
.run();
}
pub fn setup_game(
mut commands: Commands,
game_assets: Res<GameAssets>, // assets with your "world" or "level" gltf for example
) {
// more fast approach, load the game seperatly
commands.spawn((
SceneBundle {
scene: game_assets.world.clone(),
..default()
},
bevy::prelude::Name::from("world"),
));
}
```
# Workflow with blender / demo information
This example, is actually closer to a boilerplate + tooling showcases how to use a minimalistic [Blender](https://www.blender.org/) (gltf) centric workflow for [Bevy](https://bevyengine.org/), ie defining entites & their components
inside Blender using Blender's objects **custom properties**.
Aka "Blender as editor for Bevy"
It also allows you to setup 'blueprints' in Blender by using collections (the recomended way to go most of the time), or directly on single use objects .
## Features
* Useful if you want to use Blender (or any editor allowing to export gltf with configurable gltf_extras) as your Editor
* define Bevy components as custom properties in Blender (RON, though an older JSON version is also available)
* no plugin or extra tools needed in Blender (but I provide a little Blender plugin to auto-export to gltf on save if you want !)
* define components in Blender Collections & override any of them in your collection instances if you want
* code to auto add additional required components in Bevy (if B is needed with A but B is not present, it adds it: optional & configurable)
* minimal setup & code, you can have something basic running fast
* opensource * opensource
There is a [video tutorial/explanation](https://youtu.be/-lcScjQCA3c) if you want, or you can skip to the text version ahead
## Crates
- [bevy_gltf_components]('./crates/bevy_gltf_components/) This crate allows you to define components direclty inside gltf files and instanciate/inject the components on the Bevy side.
There is a [video tutorial/explanation](https://youtu.be/-lcScjQCA3c) if you want, or you can read the crate docs/ basic example
- [bevy_gltf_blueprints]('./crates/bevy_gltf_blueprints/) This crate adds the ability to define Blueprints/Prefabs for Bevy inside gltf files and spawn them in Bevy. With the ability to override and add components when spawning.
## Tools
### Blender: gltf_auto_export
- for convenience I also added a [Blender addon](./tools/gltf_auto_export/README.md) that automatically exports your level/world from Blender to gltf whenever you save your Blend file
- it also supports automatical exports of collections as [Gltf blueprints](./crates/bevy_gltf_blueprints/README.md) & more !
Please read the [README]((./tools/gltf_auto_export/README.md)) of the add-on for installation & use instructions
## Examples
- [basic](./examples/basic/) use of ```bevy_gltf_components``` only, to spawn entities with components defined inside gltf files
- [advanced](./examples/advanced/) more advanced example : use of ```bevy_gltf_blueprints``` to spawn a level and then populate it with entities coming from different gltf files, live (at runtime) spawning of entities etc
## Workflow ## Workflow
The workflow goes as follows (once you got your Bevy code setup) The workflow goes as follows (once you got your Bevy code setup)
### All core/helper modules
see the [example](./examples/general/) for more information on how to set things up
### Then...
- create & register all your components you want to be able to set from the Blender side (this is basic Bevy, no specific work needed) - create & register all your components you want to be able to set from the Blender side (this is basic Bevy, no specific work needed)
![component registration](./docs/component_registration.png) ![component registration](./docs/component_registration.png)
- Create an object / collection (for reuse) in Blender
- Create a mesh/ collection (for reuse) in Blender
- Go to object properties => add a property, and add your component data - Go to object properties => add a property, and add your component data
- unit structs, enums, and more complex strucs / components are all supported, (if the fields are basic data types at least, - unit structs, enums, and more complex strucs / components are all supported, (if the fields are basic data types at least,
have not tried more complex ones yet, but should also work) have not tried more complex ones yet, but should also work)
@ -242,7 +70,7 @@ see the [example](./examples/general/) for more information on how to set things
![unit struct components in Bevy](./docs/demo_simple_components.png) ![unit struct components in Bevy](./docs/demo_simple_components.png)
(the Rust struct for these components for reference is [here](./examples/general/game.rs#34) ) (the Rust struct for these components for reference is [here](./examples/basic/game.rs#34) )
![complex components in Blender](./docs/components_blender_parameters.png) ![complex components in Blender](./docs/components_blender_parameters.png)
@ -251,9 +79,9 @@ see the [example](./examples/general/) for more information on how to set things
![complex components in Blender](./docs/camera_tracking_component.png) ![complex components in Blender](./docs/camera_tracking_component.png)
(the Rust struct for this component for reference is [here](./examples/general/core/camera/camera_tracking.rs#21) ) (the Rust struct for this component for reference is [here](./examples/basic/core/camera/camera_tracking.rs#21) )
There is an other examples of using various Component types: Enums, Tupple structs, strucs with fields etc [here](./examples/general/test_components.rs), There is an other examples of using various Component types: Enums, Tupple structs, strucs with fields etc [here](./examples/basic/test_components.rs),
even colors, Vecs (arrays), Vec2, Vec3 etc are all supported even colors, Vecs (arrays), Vec2, Vec3 etc are all supported
![complex components in Blender](./docs/components_blender_parameters2.png) ![complex components in Blender](./docs/components_blender_parameters2.png)
@ -263,17 +91,20 @@ see the [example](./examples/general/) for more information on how to set things
* the leaf collections are the assets you use in your level * the leaf collections are the assets you use in your level
* add an empty called xxxx_components * add an empty called xxxx_components
* add the components as explained in the previous part * add the components as explained in the previous part
![blender collection asset](./docs/blender_collections.png) ![blender collection asset](./docs/blender_collections.png)
* In the Level/world itself, just create an instance of the collection (standard Blender, ie Shift+A -> collection instance -> pick the collection) * In the Level/world itself, just create an instance of the collection (standard Blender, ie Shift+A -> collection instance -> pick the collection)
- export your level as a glb/gltf file : - export your level as a glb/gltf file :
!!**IMPORTANT** you need to check the following: - using Blender's default gltf exporter
- custom properties !!**IMPORTANT** you need to check the following:
- cameras & lights if you want a complete level (as in this example) - custom properties
- cameras & lights if you want a complete level (as in this example)
![gltf_export](./docs/gltf_export.png)
- or much better, using [gltf_auto_export](./tools/gltf_auto_export/)
![gltf_export](./docs/gltf_export.png)
- load it in Bevy (see the demo main file for this) - load it in Bevy (see the demo main file for this)
@ -286,51 +117,20 @@ see the [example](./examples/general/) for more information on how to set things
> note: you get a warning if there are any unregistered components in your gltf file (they get ignored) > note: you get a warning if there are any unregistered components in your gltf file (they get ignored)
you will get a warning **per entity** you will get a warning **per entity**
![missing components warnings](./docs/component_warnings.png) ![missing components warnings](./docs/component_warnings.png)
### Additional notes
* You usually define either the Components directly or use ```Proxy components``` that get replaced in Bevy systems with the actual Components that you want (usually when for some reason, ie external crates with unregistered components etc) you cannot use the components directly.
Included are the following modules / tools
* [```process_gltf```](./src/process_gltfs.rs) the most important module: this is the one extracting ```component``` information from the gltf files
* [```insert_dependant_component```](./examples/general/core/relationships/relationships_insert_dependant_components.rs) a small utility to automatically inject
components that are dependant on an other component
for example an Entity with a Player component should also always have a ShouldBeWithPlayer component
you get a warning if you use this though, as I consider this to be stop-gap solution (usually you should have either a bundle, or directly define all needed components)
* [```camera```](./examples/general/core/camera/) an example post process/replace proxies plugin, for Camera that also adds CameraTracking functions (to enable a camera to follow an object, ie the player)
* [```lighting```](./examples/general/core/lighting/) an other example post process/replace proxies plugin for lighting, that toggles shadows, lighting config, etc so that things look closer to the original Blender data
* [```physics```](./examples/general/core/physics/) an other example post process/replace proxies plugin for physics, that add [Rapier](https://rapier.rs/docs/user_guides/bevy_plugin/getting_started_bevy) Colliders, Rigidbodies etc . Most of these do not need proxies these days, as the most Rapier components are in the Registry & can be used directly
Feel free to use as you want, rip it appart, use any/all parts that you need !
This tooling and workflow has enabled me to go from a blank Bevy + Blender setup to a working barebones level in very little time (30 minutes or so ?) !
You can then add your own components & systems for your own gameplay very easilly
## Information
- the Bevy/ Rust code is [here](./examples/general/main.rs)
- the Blender file is [here](./assets/models/level.blend)
- I added [bevy_editor_pls](https://github.com/jakobhellermann/bevy_editor_pls) as a dependency for convenience so you can inspect your level/components
## Limitations / issues ## Limitations / issues
- the components have to be defined in ```text``` in Blender, might try using the AppTypeRegistry and some Python code on the Blender side for a nicer UI (although this loses the "fast & easy, no tooling" approach) - some components have to be defined in ```text``` in Blender, might try using the AppTypeRegistry and some Python code on the Blender side for a nicer UI (although this loses the "fast & easy, no tooling" approach)
- the asset management in this example is stripped down for demo purposes, I normally use https://github.com/NiklasEi/bevy_asset_loader to define, organise and preload assets
(in a different state that comes before the actual game/level), so you will see some of the changes to the level/scene "flash by"
- Some of `bevy_rapier`/physics code / ways to define colliders could perhaps be done better within Blender (currently it also goes via RON) - Some of `bevy_rapier`/physics code / ways to define colliders could perhaps be done better within Blender (currently it also goes via RON)
- there seem to be some random system ordering issues that I am still investigating (only when replacing proxy components, no breaking bugs, just restarting your Bevy app is enough)
## Future work ## Future work
- I have a number of other tools/ code helpers that I have not yet included here, because they need cleanup/ might make this example too complex - I have a number of other tools/ code helpers that I have not yet included here, because they need cleanup/ might make this example too complex
* gltf spawning tools where you just need to preload gltf files then you can spawn 1...n entities defined in gltf files by name (for example enemies, powerups, etc)
* simplified animation logic: ie instead of having to manually specify the animations you need from a gltf file, it is integrated with the spawning system above, which creates a ```Animations``` component in all entities that have an ```AnimationPlayer``` and you can simply query for both to easilly control your animations per entity. * simplified animation logic: ie instead of having to manually specify the animations you need from a gltf file, it is integrated with the spawning system above, which creates a ```Animations``` component in all entities that have an ```AnimationPlayer``` and you can simply query for both to easilly control your animations per entity.
## Blender gltf_auto_export
- for convenience I also added a [Blender addon](./tools/blender_auto_export/README.md) that automatically exports your level/world from Blender to gltf whenever you save your Blend file
(actually when you save inside your level/world scene or in the "library" scene, where I personally usually store all collections to instanciate).
It is **very** barebones and messy, but it does a minimal ok job. Please read the README of the add-on for installation instructions
## Credits ## Credits
@ -338,8 +138,7 @@ It is **very** barebones and messy, but it does a minimal ok job. Please read th
## License ## License
This example, all its code, contents & assets is Dual-licensed under either of This repo, all its code, contents & assets is Dual-licensed under either of
- Apache License, Version 2.0, ([LICENSE-APACHE](/LICENSE_APACHE) or https://www.apache.org/licenses/LICENSE-2.0)
- MIT license ([LICENSE-MIT](/LICENSE_MIT) or https://opensource.org/licenses/MIT)
- Apache License, Version 2.0, ([LICENSE-APACHE](./LICENSE_APACHE.md) or https://www.apache.org/licenses/LICENSE-2.0)
- MIT license ([LICENSE-MIT](./LICENSE_MIT.md) or https://opensource.org/licenses/MIT)

Binary file not shown.

View File

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

View File

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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,485 @@
(
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,
),
),
},
),
},
)

View File

@ -1,3 +0,0 @@
((
blender_worklfow::Player: ()
))

View File

@ -0,0 +1,18 @@
[package]
name = "bevy_gltf_blueprints"
version = "0.1.0"
authors = ["Mark 'kaosat-dev' Moissette"]
description = "Adds the ability to define Blueprints/Prefabs for [Bevy](https://bevyengine.org/) inside gltf files and spawn them in Bevy."
homepage = "https://github.com/kaosat-dev/Blender_bevy_components_worklflow"
repository = "https://github.com/kaosat-dev/Blender_bevy_components_worklflow"
keywords = ["gamedev", "bevy", "gltf", "blueprint", "prefab"]
categories = ["game-development"]
edition = "2021"
license = "MIT OR Apache-2.0"
[dev-dependencies]
bevy="0.11.2"
[dependencies]
bevy_gltf_components = "0.1.0"
bevy = { version = "0.11.2", default-features = false, features = ["bevy_asset", "bevy_scene", "bevy_gltf", "bevy_animation"] }

View File

@ -0,0 +1,4 @@
This crate is available under either:
* The [MIT License](./LICENSE_MIT)
* The [Apache License, Version 2.0](./LICENSE_APACHE)

View File

@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [2023] [Mark "kaosat-dev" Moissette]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 Mark "kaosat-dev" Moissette
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,146 @@
[![Crates.io](https://img.shields.io/crates/v/bevy_gltf_blueprints)](https://crates.io/crates/bevy_gltf_blueprints)
[![Docs](https://img.shields.io/docsrs/bevy_gltf_blueprints)](https://docs.rs/bevy_gltf_blueprints/latest/bevy_gltf_blueprints/)
[![License](https://img.shields.io/crates/l/bevy_gltf_blueprints)](https://github.com/kaosat-dev/Blender_bevy_components_worklflow/blob/main/crates/bevy_gltf_blueprints/License.md)
[![Bevy tracking](https://img.shields.io/badge/Bevy%20tracking-released%20version-lightblue)](https://github.com/bevyengine/bevy/blob/main/docs/plugins_guidelines.md#main-branch-tracking)
# bevy_gltf_blueprints
Built upon [bevy_gltf_components](https://crates.io/crates/bevy_gltf_components) this crate adds the ability to define Blueprints/Prefabs for [Bevy](https://bevyengine.org/) inside gltf files and spawn them in Bevy.
A blueprint is a set of **overrideable** components + a hierarchy: ie
* just a Gltf file with Gltf_extras specifying components
* a component called BlueprintName
Particularly useful when using [Blender](https://www.blender.org/) as an editor for the [Bevy](https://bevyengine.org/) game engine, combined with the [Blender plugin](https://github.com/kaosat-dev/Blender_bevy_components_worklflow/tree/main/tools/gltf_auto_export) that does a lot of the work for you
## Usage
Here's a minimal usage example:
```toml
# Cargo.toml
[dependencies]
bevy_gltf_blueprints = { version = "0.1.0"}
```
```rust no_run
use bevy::prelude::*;
use bevy_gltf_blueprints::*;
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_plugin(BlueprintsPlugin)
.run();
}
// not shown here: any other setup that is not specific to blueprints
fn spawn_blueprint(
mut commands: Commands,
keycode: Res<Input<KeyCode>>,
){
if keycode.just_pressed(KeyCode::S) {
let new_entity = commands.spawn((
BlueprintName("Health_Pickup".to_string()), // mandatory !!
SpawnHere, // mandatory !!
TransformBundle::from_transform(Transform::from_xyz(x, 2.0, y)), // VERY important !!
// any other component you want to insert
));
}
}
```
## Setup
- configure your "library"/"blueprints" path:
advanced/models/library/
## Spawning entities from blueprints
You can spawn entities from blueprints like this:
```rust no_run
commands.spawn((
BlueprintName("Health_Pickup".to_string()), // mandatory !!
SpawnHere, // mandatory !!
TransformBundle::from_transform(Transform::from_xyz(x, 2.0, y)), // VERY important !!
// any other component you want to insert
))
```
Once spawning of the actual entity is done, the spawned Blueprint will be *gone/merged* with the contents of Blueprint !
> Important :
you can **add** or **override** components present inside your Blueprint when spawning the BluePrint itself: ie
### Adding components not specified inside the blueprint
you can just add any additional components you need when spawning :
```rust no_run
commands.spawn((
BlueprintName("Health_Pickup".to_string()),
SpawnHere,
TransformBundle::from_transform(Transform::from_xyz(x, 2.0, y)),
// from Rapier/bevy_xpbd: this means the entity will also have a velocity component when inserted into the world
Velocity {
linvel: Vec3::new(vel_x, vel_y, vel_z),
angvel: Vec3::new(0.0, 0.0, 0.0),
},
))
```
### Overriding components specified inside the blueprint
any component you specify when spawning the Blueprint that is also specified **within** the Blueprint will **override** that component in the final spawned entity
for example
```rust no_run
commands.spawn((
BlueprintName("Health_Pickup".to_string()),
SpawnHere,
TransformBundle::from_transform(Transform::from_xyz(x, 2.0, y)),
HealthPowerUp(20)// if this is component is also present inside the "Health_Pickup" blueprint, that one will be replaced with this component during spawning
))
```
### BluePrintBundle
There is also a bundle for convenience , which just has
* a ```BlueprintName``` component
* a ```SpawnHere``` component
* a ```TransformBundle``` sub-bundle (so we know where to spawn)
[```BluePrintBundle```](./src/lib.rs#22)
## SystemSet
the ordering of systems is very important !
For example to replace your proxy components (stand-in components when you cannot/ do not want to use real components in the gltf file) with actual ones, which should happen **AFTER** the Blueprint based spawning,
so ```bevy_gltf_blueprints``` provides a **SystemSet** for that purpose:[```GltfBlueprintsSet```](./src/lib.rs#16)
Typically , the order of systems should be
***bevy_gltf_components (GltfComponentsSet::Injection)*** => ***bevy_gltf_blueprints (GltfBlueprintsSet::Spawn, GltfBlueprintsSet::AfterSpawn)*** => ***replace_proxies***
see https://github.com/kaosat-dev/Blender_bevy_components_worklflow/tree/main/examples/advanced for how to set it up correctly
## Examples
https://github.com/kaosat-dev/Blender_bevy_components_worklflow/tree/main/examples/advanced
## License
This crate, all its code, contents & assets is Dual-licensed under either of
- Apache License, Version 2.0, ([LICENSE-APACHE](./LICENSE_APACHE.md) or https://www.apache.org/licenses/LICENSE-2.0)
- MIT license ([LICENSE-MIT](./LICENSE_MIT.md) or https://opensource.org/licenses/MIT)

View File

@ -0,0 +1,66 @@
use bevy::prelude::*;
use bevy::ecs::system::Command;
// modified version from https://github.com/bevyengine/bevy/issues/1515,
// more specifically https://gist.github.com/nwtnni/85d6b87ae75337a522166c500c9a8418
// to work with Bevy 0.11
pub struct CloneEntity {
pub source: Entity,
pub destination: Entity,
}
impl CloneEntity {
// Copy all components from an entity to another.
// Using an entity with no components as the destination creates a copy of the source entity.
// Panics if:
// - the components are not registered in the type registry,
// - the world does not have a type registry
// - the source or destination entity do not exist
fn clone_entity(self, world: &mut World) {
let components = {
let registry = world.get_resource::<AppTypeRegistry>().unwrap().read();
world
.get_entity(self.source)
.unwrap()
.archetype()
.components()
.map(|component_id| {
world
.components()
.get_info(component_id)
.unwrap()
.type_id()
.unwrap()
})
.map(|type_id| {
// println!("type_id {:?}", type_id);
registry
.get(type_id)
.unwrap()
.data::<ReflectComponent>()
.unwrap()
.clone()
})
.collect::<Vec<_>>()
};
for component in components {
let source = component
.reflect(world.get_entity(self.source).unwrap())
.unwrap()
.clone_value();
let mut destination = world.get_entity_mut(self.destination).unwrap();
component.apply_or_insert(&mut destination, &*source);
}
}
}
// This allows the command to be used in systems
impl Command for CloneEntity {
fn apply(self, world: &mut World) {
self.clone_entity(world)
}
}

View File

@ -0,0 +1,90 @@
pub mod spawn_from_blueprints;
use bevy_gltf_components::GltfComponentsSet;
pub use spawn_from_blueprints::*;
pub mod spawn_post_process;
pub use spawn_post_process::*;
pub mod clone_entity;
pub use clone_entity::*;
use bevy::prelude::*;
#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)]
/// set for the two stages of blueprint based spawning :
pub enum GltfBlueprintsSet{
Spawn,
AfterSpawn,
}
#[derive(Bundle)]
pub struct BluePrintBundle {
pub blueprint: BlueprintName,
pub spawn_here: SpawnHere,
pub transform: TransformBundle
}
impl Default for BluePrintBundle {
fn default() -> Self {
BluePrintBundle {
blueprint: BlueprintName("default".into()),
spawn_here: SpawnHere,
transform: TransformBundle::default()
}
}
}
#[derive(Clone, Resource)]
pub(crate) struct BluePrintsConfig {
pub(crate) library_folder: String,
}
#[derive(Debug, Clone)]
pub struct BlueprintsPlugin {
/// The base folder where library/blueprints assets are loaded from, relative to the executable.
pub library_folder: String,
}
impl Default for BlueprintsPlugin {
fn default() -> Self {
Self {
library_folder: "assets/models/library".to_string(),
}
}
}
impl Plugin for BlueprintsPlugin {
fn build(&self, app: &mut App) {
app
.register_type::<BlueprintName>()
.register_type::<SpawnHere>()
.insert_resource(BluePrintsConfig{library_folder: self.library_folder.clone()})
.configure_sets(
Update,
(GltfBlueprintsSet::Spawn, GltfBlueprintsSet::AfterSpawn).chain().after(GltfComponentsSet::Injection)
)
.add_systems(Update,
(spawn_from_blueprints)
// .run_if(in_state(AppState::AppRunning).or_else(in_state(AppState::LoadingGame))) // FIXME: how to replace this with a crate compatible version ?
.in_set(GltfBlueprintsSet::Spawn),
)
.add_systems(
Update,
(
// spawn_entities,
update_spawned_root_first_child,
apply_deferred,
cleanup_scene_instances,
apply_deferred,
)
.chain()
// .run_if(in_state(AppState::LoadingGame).or_else(in_state(AppState::AppRunning))) // FIXME: how to replace this with a crate compatible version ?
.in_set(GltfBlueprintsSet::AfterSpawn),
)
;
}
}

View File

@ -0,0 +1,81 @@
use bevy::{prelude::*, gltf::Gltf};
use crate::BluePrintsConfig;
/// this is a flag component for our levels/game world
#[derive(Component)]
pub struct GameWorldTag;
/// Main component for the blueprints
#[derive(Component, Reflect, Default, Debug, )]
#[reflect(Component)]
pub struct BlueprintName(pub String);
/// flag component needed to signify the intent to spawn a Blueprint
#[derive(Component, Reflect, Default, Debug, )]
#[reflect(Component)]
pub struct SpawnHere;
#[derive(Component)]
/// FlagComponent for spawned entity
pub struct Spawned;
#[derive(Component)]
/// helper component, just to transfer some data
pub(crate) struct Original(pub Entity);
#[derive(Component)]
/// FlagComponent for dynamically spawned scenes
pub struct SpawnedRoot;
/// main spawning functions,
/// * also takes into account the already exisiting "override" components, ie "override components" > components from blueprint
pub(crate) fn spawn_from_blueprints(
spawn_placeholders: Query<(Entity, &Name, &BlueprintName, &Transform), (Added<BlueprintName>, Added<SpawnHere>, Without<Spawned>, Without<SpawnedRoot>)>,
mut commands: Commands,
mut game_world: Query<(Entity, &Children), With<GameWorldTag>>,
assets_gltf: Res<Assets<Gltf>>,
asset_server: Res<AssetServer>,
blueprints_config: Res<BluePrintsConfig>
){
for (entity, name, blupeprint_name, global_transform) in spawn_placeholders.iter() {
info!("need to spawn {:?}", blupeprint_name.0);
let what = &blupeprint_name.0;
let model_path = format!("{}/{}.glb", &blueprints_config.library_folder, &what); // FIXME: needs to be platform agnostic
let scene:Handle<Gltf> = asset_server.load(&model_path);
// let scene = game_assets.models.get(&model_path).expect(&format!("no matching model {:?} found", model_path));
info!("attempting to spawn {:?}",model_path);
let world = game_world.single_mut();
let world = world.1[0]; // FIXME: dangerous hack because our gltf data have a single child like this, but might not always be the case
let gltf = assets_gltf.get(&scene).expect("this gltf should have been loaded");
// WARNING we work under the assumtion that there is ONLY ONE named scene, and that the first one is the right one
let main_scene_name =gltf.named_scenes.keys().nth(0).expect("there should be at least one named scene in the gltf file to spawn");
let scene = &gltf.named_scenes[main_scene_name];
//spawn_requested_events.send(SpawnRequestedEvent { what: "enemy".into(), position, amount: 1, spawner_id: None });
let child_scene = commands.spawn(
(
SceneBundle {
scene: scene.clone(),
transform: global_transform.clone(),
..Default::default()
},
bevy::prelude::Name::from(["scene_wrapper", &name.clone()].join("_") ),
// Parent(world) // FIXME/ would be good if this worked directly
SpawnedRoot,
/*AnimationHelper{ // TODO: insert this at the ENTITY level, not the scene level
named_animations: gltf.named_animations.clone(),
// animations: gltf.named_animations.values().clone()
},*/
Original(entity)
)).id();
commands.entity(world).add_child(child_scene);
}
}

View File

@ -0,0 +1,136 @@
use bevy::prelude::*;
use bevy::utils::HashMap;
use super::{CloneEntity, SpawnHere};
use super::{Original, SpawnedRoot};
// FIXME: move to more relevant module
#[derive(Component, Reflect, Default, Debug, )]
#[reflect(Component)]
pub struct AnimationHelper {
pub named_animations: HashMap<String, Handle<AnimationClip>>,
}
#[derive(Component)]
/// FlagComponent for dynamically spawned scenes
pub(crate) struct SpawnedRootProcessed;
/// this system updates the first (and normally only) child of a scene flaged SpawnedRoot
/// - adds a name based on parent component (spawned scene) which is named on the scene name/prefab to be instanciated
/// - adds the initial physics impulse (FIXME: we would need to add a temporary physics component to those who do not have it)
// FIXME: updating hierarchy does not work in all cases ! this is sadly dependant on the structure of the exported blend data, disablin for now
// - blender root-> object with properties => WORKS
// - scene instance -> does not work
// it might be due to how we add components to the PARENT item in gltf to components
pub(crate) fn update_spawned_root_first_child(
// all_children: Query<(Entity, &Children)>,
unprocessed_entities :Query<(Entity, &Children, &Name, &Parent, &Original ), (With<SpawnedRoot>, Without<SpawnedRootProcessed>)>,
mut commands: Commands,
// FIXME: should be done at a more generic gltf level
animation_helpers: Query<&AnimationHelper>,
added_animation_helpers : Query<(Entity, &AnimationPlayer), Added<AnimationPlayer>>
){
/*
currently we have
- scene instance
- root node ?
- the actual stuff
we want to remove the root node
so we wend up with
- scene instance
- the actual stuff
so
- get root node
- add its children to the scene instance
- remove root node
Another issue is, the scene instance become empty if we have a pickabke as the "actual stuff", so that would lead to a lot of
empty scenes if we spawn pickables
- perhaps another system that cleans up empty scene instances ?
FIME: this is all highly dependent on the hierachy ;..
*/
for (scene_instance, children, name, parent, original) in unprocessed_entities.iter() {
println!("children of scene {:?}", children);
if children.len() == 0 {
continue;
}
// the root node is the first & normally only child inside a scene, it is the one that has all relevant components
let root_entity = children.first().unwrap(); //FIXME: and what about childless ones ??
// let root_entity_data = all_children.get(*root_entity).unwrap();
// fixme : randomization should be controlled via parameters, perhaps even the seed could be specified ?
// use this https://rust-random.github.io/book/guide-seeding.html#a-simple-number, blenders seeds are also uInts
// also this is not something we want every time, this should be a settable parameter when requesting a spawn
// add missing name of entity, based on the wrapper's name
let name= name.clone().replace("scene_wrapper_", "");
// this is our new actual entity
commands.entity(*root_entity).insert((
bevy::prelude::Name::from(name.clone()),
// ItemType {name},
// Spawned, // FIXME: not sure
));
// flag the spawned_root as being processed
commands.entity(scene_instance).insert(SpawnedRootProcessed);
// let original_transforms =
// parent is either the world or an entity with a marker (BlueprintName)
commands.entity(parent.get()).add_child(*root_entity);
// commands.entity(*root_entity).despawn_recursive();
// commands.entity(parent.get()).push_children(&actual_stuff);
//commands.entity(*root_entity).log_components();
let matching_animation_helper = animation_helpers.get(scene_instance);
// println!("WE HAVE SOME ADDED ANIMATION PLAYERS {:?}", matching_animation_helper);
if let Ok(anim_helper) = matching_animation_helper{
for (added, _) in added_animation_helpers.iter(){
commands.entity(added).insert(
AnimationHelper{ // TODO: insert this at the ENTITY level, not the scene level
named_animations: anim_helper.named_animations.clone(),
// animations: gltf.named_animations.values().clone()
},
);
}
}
commands.add(CloneEntity {
source: original.0,
destination: *root_entity,
});
// remove the original entity, now that we have cloned it into the spawned scenes first child
commands.entity(original.0).despawn_recursive();
commands.entity(*root_entity).remove::<SpawnHere>();
}
}
/// cleans up dynamically spawned scenes so that they get despawned if they have no more children
// FIXME: this can run too early , and since withouth_children matches not yet completed scene instances, boom, it removes components/children before they can be used
pub(crate) fn cleanup_scene_instances(
scene_instances: Query<(Entity, &Children), With<SpawnedRootProcessed>>,
without_children: Query<Entity, (With<SpawnedRootProcessed>, Without<Children>)>,// if there are not children left, bevy removes Children ?
mut commands: Commands
){
for (entity, children) in scene_instances.iter(){
if children.len() == 0{ // it seems this does not happen ?
info!("cleaning up emptied spawned scene instance");
commands.entity(entity).despawn_recursive();
}
}
for entity in without_children.iter() {
info!("cleaning up emptied spawned scene instance");
commands.entity(entity).despawn_recursive();
}
}

View File

@ -0,0 +1,19 @@
[package]
name = "bevy_gltf_components"
version = "0.1.0"
authors = ["Mark 'kaosat-dev' Moissette"]
description = "Allows you to define [Bevy](https://bevyengine.org/) components direclty inside gltf files and instanciate the components on the Bevy side."
homepage = "https://github.com/kaosat-dev/Blender_bevy_components_worklflow"
repository = "https://github.com/kaosat-dev/Blender_bevy_components_worklflow"
keywords = ["gamedev", "bevy", "assets", "gltf", "components"]
categories = ["game-development"]
edition = "2021"
license = "MIT OR Apache-2.0"
[dev-dependencies]
bevy="0.11.2"
[dependencies]
bevy = { version = "0.11.2", default-features = false, features = ["bevy_asset", "bevy_scene", "bevy_gltf"] }
serde = "1.0.188"
ron="0.8.1"

View File

@ -0,0 +1,4 @@
This crate is available under either:
* The [MIT License](./LICENSE_MIT)
* The [Apache License, Version 2.0](./LICENSE_APACHE)

View File

@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [2023] [Mark "kaosat-dev" Moissette]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 Mark "kaosat-dev" Moissette
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,83 @@
[![Crates.io](https://img.shields.io/crates/v/bevy_gltf_components)](https://crates.io/crates/bevy_gltf_components)
[![Docs](https://img.shields.io/docsrs/bevy_gltf_components)](https://docs.rs/bevy_gltf_components/latest/bevy_gltf_components/)
[![License](https://img.shields.io/crates/l/bevy_gltf_components)](https://github.com/kaosat-dev/Blender_bevy_components_worklflow/blob/main/crates/bevy_gltf_components/License.md)
[![Bevy tracking](https://img.shields.io/badge/Bevy%20tracking-released%20version-lightblue)](https://github.com/bevyengine/bevy/blob/main/docs/plugins_guidelines.md#main-branch-tracking)
# bevy_gltf_components
This crate allows you to define [Bevy](https://bevyengine.org/) components direclty inside gltf files and instanciate the components on the Bevy side.
## Usage
***important*** : the plugin for processing gltf files runs in ***update*** , so you cannot use the components directly if you spawn your scene from gltf in ***setup*** (the additional components will not show up)
Please see the
* [example](https://github.com/kaosat-dev/Blender_bevy_components_worklflow/examples/basic)
* or use [```bevy_asset_loader```](https://github.com/NiklasEi/bevy_asset_loader) for a reliable workflow.
* alternatively, use the [```bevy_gltf_blueprints```](https://github.com/kaosat-dev/Blender_bevy_components_worklflow/blob/main/crates/bevy_gltf_blueprints) crate, build on this crate's features,
that allows you to directly spawn entities from gltf based blueprints.
Here's a minimal usage example:
```toml
# Cargo.toml
[dependencies]
bevy="0.11.2"
bevy_gltf_components = { version = "0.1.0"}
```
```rust no_run
//too barebones of an example to be meaningfull, please see https://github.com/kaosat-dev/Blender_bevy_components_worklflow/examples/basic for a real example
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_plugin(ComponentsFromGltfPlugin)
.add_system(spawn_level)
.run();
}
fn spawn_level(
asset_server: Res<AssetServer>,
mut commands: bevy::prelude::Commands,
keycode: Res<Input<KeyCode>>,
){
if keycode.just_pressed(KeyCode::Return) {
commands.spawn(SceneBundle {
scene: asset_server.load("basic/models/level1.glb#Scene0"),
transform: Transform::from_xyz(2.0, 0.0, -5.0),
..Default::default()
});
}
}
```
## SystemSet
the ordering of systems is very important !
For example to replace your proxy components (stand-in components when you cannot/ do not want to use real components in the gltf file) with actual ones,
which should happen **AFTER** the components from the gltf files have been injected,
so ```bevy_gltf_components``` provides a **SystemSet** for that purpose:[```GltfComponentsSet```](./src/lib.rs#46)
Typically , the order of systems should be
***bevy_gltf_components (GltfComponentsSet::Injection)*** => ***replace_proxies***
## Examples
https://github.com/kaosat-dev/Blender_bevy_components_worklflow/tree/main/examples/basic
## License
This crate, all its code, contents & assets is Dual-licensed under either of
- Apache License, Version 2.0, ([LICENSE-APACHE](./LICENSE_APACHE.md) or https://www.apache.org/licenses/LICENSE-2.0)
- MIT license ([LICENSE-MIT](./LICENSE_MIT.md) or https://opensource.org/licenses/MIT)

View File

@ -13,6 +13,7 @@ use bevy::gltf::{Gltf, GltfExtras};
use super::capitalize_first_letter; use super::capitalize_first_letter;
/// main function: injects components into each entity in gltf files that have gltf_extras, using reflection
pub fn gltf_extras_to_components( pub fn gltf_extras_to_components(
gltf: &mut Gltf, gltf: &mut Gltf,
scenes: &mut ResMut<Assets<Scene>>, scenes: &mut ResMut<Assets<Scene>>,
@ -21,7 +22,7 @@ pub fn gltf_extras_to_components(
){ ){
let mut added_components = 0; let mut added_components = 0;
for (_name, scene) in &gltf.named_scenes { for (_name, scene) in &gltf.named_scenes {
info!("gltf: {:?} scene name {:?}", gltf_name, _name); debug!("gltf: {:?} scene name {:?}", gltf_name, _name);
let scene = scenes.get_mut(scene).unwrap(); let scene = scenes.get_mut(scene).unwrap();
@ -49,7 +50,6 @@ pub fn gltf_extras_to_components(
if entity_components.contains_key(&target_entity) { if entity_components.contains_key(&target_entity) {
let mut updated_components: Vec<Box<dyn Reflect>> = Vec::new(); let mut updated_components: Vec<Box<dyn Reflect>> = Vec::new();
let current_components = &entity_components[&target_entity]; let current_components = &entity_components[&target_entity];
// first inject the current components // first inject the current components
for component in current_components { for component in current_components {
updated_components.push(component.clone_value()); updated_components.push(component.clone_value());
@ -74,7 +74,6 @@ pub fn gltf_extras_to_components(
debug!("-----value {:?}", &extras.value); debug!("-----value {:?}", &extras.value);
} }
// println!("FOUND ASSET {:?}", foob);
// GltfNode // GltfNode
// find a way to link this name to the current entity ? => WOULD BE VERY USEFULL for animations & co !! // find a way to link this name to the current entity ? => WOULD BE VERY USEFULL for animations & co !!
debug!("done pre-processing components, now adding them to entities"); debug!("done pre-processing components, now adding them to entities");
@ -98,10 +97,9 @@ pub fn gltf_extras_to_components(
// scene.world.components(). // scene.world.components().
// TODO: how can we insert any additional components "by hand" here ? // TODO: how can we insert any additional components "by hand" here ?
} }
let entity_mut = scene.world.entity_mut(entity); // let entity_mut = scene.world.entity_mut(entity);
let archetype = entity_mut.archetype().clone(); // let archetype = entity_mut.archetype().clone();
let _all_components = archetype.components(); // let _all_components = archetype.components();
// println!("All components {:?}", all_components);
if added_components > 0 { if added_components > 0 {
debug!("------done adding {} components", added_components); debug!("------done adding {} components", added_components);
@ -120,7 +118,6 @@ pub fn gltf_extras_to_components(
for (key, value) in lookup.into_iter() { for (key, value) in lookup.into_iter() {
let type_string = key.replace("component: ", "").trim().to_string(); let type_string = key.replace("component: ", "").trim().to_string();
let capitalized_type_name = capitalize_first_letter(type_string.as_str()); let capitalized_type_name = capitalize_first_letter(type_string.as_str());
// println!("capitalized_type_name {}", capitalized_type_name);
let mut parsed_value:String; let mut parsed_value:String;
match value.clone() { match value.clone() {
@ -140,8 +137,6 @@ pub fn gltf_extras_to_components(
if info.field_len() == 1 { if info.field_len() == 1 {
let field = info.field_at(0).expect("we should always have at least one field here"); let field = info.field_at(0).expect("we should always have at least one field here");
let field_name = field.type_name(); let field_name = field.type_name();
// println!("field name {}", field_name);
// let bla = TypeId::of::<f32>();
// TODO: find a way to cast with typeId instead of this matching // TODO: find a way to cast with typeId instead of this matching
/*match field.type_id(){ /*match field.type_id(){
TypeId::of::<f32>() => { TypeId::of::<f32>() => {

View File

@ -0,0 +1,74 @@
pub mod utils;
pub use utils::*;
pub mod gltf_to_components;
pub use gltf_to_components::*;
pub mod process_gltfs;
pub use process_gltfs::*;
use bevy::prelude::{
App,Plugin, Update, SystemSet, IntoSystemConfigs
};
/// A Bevy plugin for extracting components from gltf files and automatically adding them to the relevant entities
/// It will automatically run every time you load a gltf file
/// Add this plugin to your Bevy app to get access to this feature
/// ```
/// # use bevy::prelude::*;
/// # use bevy::gltf::*;
/// # use bevy_gltf_components::ComponentsFromGltfPlugin;
///
/// //too barebones of an example to be meaningfull, please see https://github.com/kaosat-dev/Blender_bevy_components_worklflow/examples/basic for a real example
/// fn main() {
/// App::new()
/// .add_plugins(DefaultPlugins)
/// .add_plugin(ComponentsFromGltfPlugin)
/// .add_system(spawn_level)
/// .run();
/// }
///
/// fn spawn_level(
/// asset_server: Res<AssetServer>,
/// mut commands: bevy::prelude::Commands,
/// keycode: Res<Input<KeyCode>>,
/// ){
/// if keycode.just_pressed(KeyCode::Return) {
/// commands.spawn(SceneBundle {
/// scene: asset_server.load("basic/models/level1.glb#Scene0"),
/// transform: Transform::from_xyz(2.0, 0.0, -5.0),
/// ..Default::default()
/// });
/// }
///}
/// ```
#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)]
/// systemset to order your systems after the component injection when needed
pub enum GltfComponentsSet{
Injection,
}
#[derive(Default)]
pub struct ComponentsFromGltfPlugin;
impl Plugin for ComponentsFromGltfPlugin {
fn build(&self, app: &mut App) {
app
.insert_resource(GltfLoadingTracker::new())
.add_systems(Update, (
track_new_gltf,
process_loaded_scenes,
))
.add_systems(Update,
(process_loaded_scenes)
.in_set(GltfComponentsSet::Injection)
)
;
}
}

View File

@ -5,7 +5,8 @@ use bevy::gltf::Gltf;
use super::gltf_extras_to_components; use super::gltf_extras_to_components;
#[derive(Resource)] #[derive(Resource)]
pub struct GltfLoadingTracker{ /// component to keep track of gltfs' loading state
pub struct GltfLoadingTracker{
pub loading_gltfs: HashSet<Handle<Gltf>>, pub loading_gltfs: HashSet<Handle<Gltf>>,
pub loaded_gltfs: HashSet<Handle<Gltf>> pub loaded_gltfs: HashSet<Handle<Gltf>>
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

After

Width:  |  Height:  |  Size: 66 KiB

BIN
docs/blender_addon_use3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

881
docs/process.svg Normal file
View File

@ -0,0 +1,881 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="210mm"
height="297mm"
viewBox="0 0 210 297"
version="1.1"
id="svg1"
inkscape:version="1.3 (0e150ed6c4, 2023-07-21)"
sodipodi:docname="process.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
inkscape:zoom="0.72426347"
inkscape:cx="-439.06674"
inkscape:cy="597.15838"
inkscape:window-width="2560"
inkscape:window-height="1011"
inkscape:window-x="0"
inkscape:window-y="32"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs1">
<marker
style="overflow:visible"
id="RoundedArrow"
refX="0"
refY="0"
orient="auto-start-reverse"
inkscape:stockid="Rounded arrow"
markerWidth="1"
markerHeight="1"
viewBox="0 0 1 1"
inkscape:isstock="true"
inkscape:collect="always"
preserveAspectRatio="xMidYMid">
<path
transform="scale(0.7)"
d="m -0.21114562,-4.1055728 6.42229122,3.21114561 a 1,1 90 0 1 0,1.78885438 L -0.21114562,4.1055728 A 1.236068,1.236068 31.717474 0 1 -2,3 v -6 a 1.236068,1.236068 148.28253 0 1 1.78885438,-1.1055728 z"
style="fill:context-stroke;fill-rule:evenodd;stroke:none"
id="path8" />
</marker>
<marker
style="overflow:visible"
id="ArrowWide"
refX="0"
refY="0"
orient="auto-start-reverse"
inkscape:stockid="Wide arrow"
markerWidth="1"
markerHeight="1"
viewBox="0 0 1 1"
inkscape:isstock="true"
inkscape:collect="always"
preserveAspectRatio="xMidYMid">
<path
style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:butt"
d="M 3,-3 0,0 3,3"
transform="rotate(180,0.125,0)"
sodipodi:nodetypes="ccc"
id="path1" />
</marker>
</defs>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<rect
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="rect77"
width="220.61754"
height="312.25241"
x="-4.5802779"
y="-3.4230094"
ry="4.6700392" />
<rect
style="fill:none;stroke:#000000;stroke-width:0.309086;stroke-dasharray:none"
id="rect1"
width="201.30733"
height="72.947769"
x="4.9794211"
y="3.6587322"
ry="2.5855451" />
<text
xml:space="preserve"
style="font-size:3.175px;fill:none;stroke:#000000;stroke-width:0.264583"
x="102.90446"
y="11.789397"
id="text1"><tspan
sodipodi:role="line"
id="tspan1"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:'Ubuntu Bold';fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="102.90446"
y="11.789397">Original</tspan></text>
<rect
style="fill:none;stroke:#000000;stroke-width:0.294902;stroke-dasharray:none"
id="rect6"
width="82.234131"
height="56.321247"
x="115.08189"
y="16.034706"
ry="5.4638991" />
<g
id="g16"
transform="translate(-90.659631,6.5443841)">
<rect
style="fill:none;stroke:#000000;stroke-width:0.307482;stroke-dasharray:none"
id="rect5"
width="89.355553"
height="56.348793"
x="104.23762"
y="9.3243046"
ry="5.4665713" />
<g
id="g15"
transform="translate(-8.7898623,-68.268248)"
style="fill:#efff81;fill-opacity:1">
<rect
style="fill:#efff81;fill-opacity:1;stroke:#000000;stroke-width:0.264583;stroke-dasharray:none"
id="rect7"
width="21.240173"
height="10.947481"
x="120.16953"
y="92.749634"
ry="1.0620492" />
<text
xml:space="preserve"
style="font-size:3.175px;fill:#efff81;fill-opacity:1;stroke:#000000;stroke-width:0.264583;stroke-dasharray:none"
x="124.65015"
y="97.640045"
id="text7"><tspan
sodipodi:role="line"
id="tspan7"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="124.65015"
y="97.640045">Object A</tspan><tspan
sodipodi:role="line"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="124.65015"
y="101.76033"
id="tspan72">(unique)</tspan></text>
</g>
<text
xml:space="preserve"
style="font-size:3.175px;fill:none;stroke:#000000;stroke-width:0.264583"
x="126.33414"
y="14.70156"
id="text8"><tspan
sodipodi:role="line"
id="tspan8"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:'Ubuntu Bold';fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="126.33414"
y="14.70156">Main Scene (world/level)</tspan></text>
<g
id="g14"
transform="translate(-15.875,-65.181456)"
style="fill:#81ffc7;fill-opacity:1">
<rect
style="fill:#81ffc7;fill-opacity:1;stroke:#000000;stroke-width:0.355848;stroke-dasharray:none"
id="rect8"
width="38.74361"
height="10.856215"
x="157.82838"
y="84.181671"
ry="1.0531952" />
<text
xml:space="preserve"
style="font-size:3.175px;fill:#81ffc7;fill-opacity:1;stroke:#000000;stroke-width:0.264583;stroke-dasharray:none"
x="162.26337"
y="90.613953"
id="text9"><tspan
sodipodi:role="line"
id="tspan9"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="162.26337"
y="90.613953">Object B1 (instance)</tspan></text>
</g>
<g
id="g13"
transform="translate(-11.892453,-70.473123)">
<rect
style="fill:#81ffc7;fill-opacity:1;stroke:#000000;stroke-width:0.358688;stroke-dasharray:none"
id="rect9"
width="39.374626"
height="10.853376"
x="153.73479"
y="107.09112"
ry="1.0529197" />
<text
xml:space="preserve"
style="font-size:3.175px;fill:none;stroke:#000000;stroke-width:0.264583;stroke-dasharray:none"
x="158.16835"
y="113.52198"
id="text10"><tspan
sodipodi:role="line"
id="tspan10"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="158.16835"
y="113.52198">Object C0 (instance)</tspan></text>
</g>
<path
style="fill:none;stroke:#000000;stroke-width:0.264583;stroke-dasharray:2.11666, 0.264583;stroke-dashoffset:0;marker-end:url(#ArrowWide)"
d="M 226.2693,25.28543 H 183.09695"
id="path16" />
<path
style="fill:none;stroke:#000000;stroke-width:0.264583;stroke-dasharray:2.11666, 0.264583;stroke-dashoffset:0;marker-end:url(#ArrowWide)"
d="M 226.2693,42.010871 H 183.09695"
id="path17" />
<g
id="g17"
transform="translate(-11.892453,-56.675475)">
<rect
style="fill:#81ffc7;fill-opacity:1;stroke:#000000;stroke-width:0.358688;stroke-dasharray:none"
id="rect17"
width="39.374626"
height="10.853376"
x="153.73479"
y="107.09112"
ry="1.0529197" />
<text
xml:space="preserve"
style="font-size:3.175px;fill:#000000;stroke:none;stroke-width:0.264583;stroke-dasharray:none;-inkscape-font-specification:Ubuntu;font-family:Ubuntu;font-weight:normal;font-style:normal;font-stretch:normal;font-variant:normal;fill-opacity:1"
x="158.16835"
y="113.52198"
id="text17"><tspan
sodipodi:role="line"
id="tspan17"
style="stroke-width:0.264583;-inkscape-font-specification:Ubuntu;font-family:Ubuntu;font-weight:normal;font-style:normal;font-stretch:normal;font-variant:normal;stroke:none;fill:#000000;fill-opacity:1"
x="158.16835"
y="113.52198">Object C1 (instance)</tspan></text>
</g>
<path
style="fill:none;stroke:#000000;stroke-width:0.264583;stroke-dasharray:2.11666, 0.264583;stroke-dashoffset:0;marker-end:url(#ArrowWide)"
d="m 226.2693,42.010871 -42.64565,15.18405"
id="path18"
sodipodi:nodetypes="cc" />
</g>
<text
xml:space="preserve"
style="font-size:3.175px;fill:none;stroke:#000000;stroke-width:0.264583"
x="145.92572"
y="22.124336"
id="text11"><tspan
sodipodi:role="line"
id="tspan11"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:'Ubuntu Bold';fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="145.92572"
y="22.124336">Library Scene</tspan></text>
<rect
style="fill:#ff9342;fill-opacity:1;stroke:#000000;stroke-width:0.264583;stroke-dasharray:none"
id="rect11"
width="56.246941"
height="10.784719"
x="138.19809"
y="26.06472"
ry="1.046259" />
<text
xml:space="preserve"
style="font-size:3.175px;fill:none;stroke:#000000;stroke-width:0.264583;stroke-dasharray:none"
x="142.59734"
y="32.46125"
id="text12"><tspan
sodipodi:role="line"
id="tspan12"
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="142.59734"
y="32.46125">Object B: collection/ blueprint</tspan></text>
<g
id="g69"
transform="translate(0,-5.0567386)">
<rect
style="fill:#ff9342;fill-opacity:1;stroke:#000000;stroke-width:0.264583;stroke-dasharray:none"
id="rect12"
width="56.246941"
height="10.784719"
x="138.19809"
y="46.759216"
ry="1.046259" />
<text
xml:space="preserve"
style="font-size:3.175px;fill:none;stroke:#000000;stroke-width:0.264583;stroke-dasharray:none"
x="142.59734"
y="53.155746"
id="text13"><tspan
sodipodi:role="line"
id="tspan13"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="142.59734"
y="53.155746">Object C: collection/ blueprint</tspan></text>
</g>
<path
style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:1;stroke-dasharray:none;stroke-dashoffset:0;marker-end:url(#RoundedArrow)"
d="m 108.69996,76.300286 v 9.304509"
id="path28"
sodipodi:nodetypes="cc" />
<rect
style="fill:none;stroke:#000000;stroke-width:0.336158;stroke-dasharray:none"
id="rect28"
width="201.28026"
height="86.298004"
x="4.9929576"
y="91.43959"
ry="3.0587275" />
<text
xml:space="preserve"
style="font-size:3.175px;fill:#000000;stroke:none;stroke-width:0.264583;fill-opacity:1"
x="97.815292"
y="99.943169"
id="text28"><tspan
sodipodi:role="line"
id="tspan28"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:'Ubuntu Bold';fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="97.815292"
y="99.943169">Transform Step</tspan></text>
<rect
style="fill:none;stroke:#000000;stroke-width:0.320846;stroke-dasharray:none"
id="rect29"
width="82.208183"
height="66.687843"
x="115.09486"
y="106.46085"
ry="6.4695954" />
<g
id="g36"
transform="translate(-90.659631,96.957538)">
<rect
style="fill:none;stroke:#000000;stroke-width:0.336001;stroke-dasharray:none"
id="rect30"
width="89.327034"
height="67.307678"
x="104.25188"
y="9.3385649"
ry="6.5297275" />
<g
id="g31"
transform="translate(-8.7898623,-68.268248)"
style="fill:#efff81;fill-opacity:1">
<rect
style="fill:#efff81;fill-opacity:1;stroke:#000000;stroke-width:0.264583;stroke-dasharray:none"
id="rect31"
width="21.240173"
height="10.947481"
x="120.16953"
y="92.749634"
ry="1.0620492" />
<text
xml:space="preserve"
style="font-size:3.175px;fill:#efff81;fill-opacity:1;stroke:#000000;stroke-width:0.264583;stroke-dasharray:none"
x="124.65015"
y="97.640045"
id="text31"><tspan
sodipodi:role="line"
id="tspan31"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583;stroke-opacity:1"
x="124.65015"
y="97.640045">Object A</tspan><tspan
sodipodi:role="line"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583;stroke-opacity:1"
x="124.65015"
y="101.76033"
id="tspan73">(unique)</tspan></text>
</g>
<text
xml:space="preserve"
style="font-size:3.175px;fill:none;stroke:#000000;stroke-width:0.264583"
x="126.33414"
y="14.70156"
id="text32"><tspan
sodipodi:role="line"
id="tspan32"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:'Ubuntu Bold';fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="126.33414"
y="14.70156">Temporary Scene (world/level)</tspan></text>
<g
id="g33"
transform="translate(-19.939658,-65.832448)"
style="fill:#81ffc7;fill-opacity:1">
<rect
style="fill:#81ffc7;fill-opacity:1;stroke:#000000;stroke-width:0.505434;stroke-dasharray:none"
id="rect32"
width="52.860485"
height="16.052649"
x="157.90318"
y="84.256462"
ry="1.5573175" />
<text
xml:space="preserve"
style="font-size:3.175px;fill:#81ffc7;fill-opacity:1;stroke:#000000;stroke-width:0.264583;stroke-dasharray:none"
x="162.26337"
y="90.613953"
id="text33"><tspan
sodipodi:role="line"
id="tspan33"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="162.26337"
y="90.613953">Object B1 (Empty)</tspan><tspan
sodipodi:role="line"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="162.26337"
y="94.734238"
id="tspan44">+ blueprintName(&quot;ObjectB&quot;)</tspan><tspan
sodipodi:role="line"
style="fill:#81ffc7;fill-opacity:1;stroke-width:0.264583"
x="162.26337"
y="98.854515"
id="tspan47"><tspan
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;fill:#000000;fill-opacity:1;stroke:none"
id="tspan78">+ spawnHere</tspan> </tspan></text>
</g>
<g
id="g34"
transform="translate(-15.528392,-70.796885)">
<rect
style="fill:#81ffc7;fill-opacity:1;stroke:#000000;stroke-width:0.528181;stroke-dasharray:none"
id="rect33"
width="51.925056"
height="17.845793"
x="153.81953"
y="107.17587"
ry="1.7312757" />
<text
xml:space="preserve"
style="font-size:3.175px;fill:none;stroke:#000000;stroke-width:0.264583;stroke-dasharray:none"
x="158.16835"
y="113.52198"
id="text34"><tspan
sodipodi:role="line"
id="tspan34"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="158.16835"
y="113.52198">Object C0 (Empty)</tspan><tspan
sodipodi:role="line"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="158.16835"
y="117.64227"
id="tspan45">+ blueprintName(&quot;ObjectC&quot;)</tspan><tspan
sodipodi:role="line"
style="stroke-width:0.264583"
x="158.16835"
y="121.76254"
id="tspan48"><tspan
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;fill:#000000;fill-opacity:1;stroke:none"
id="tspan79">+ spawnHere</tspan> </tspan></text>
</g>
<path
style="fill:none;stroke:#000000;stroke-width:0.264583;stroke-dasharray:2.11666, 0.264583;stroke-dashoffset:0;marker-end:url(#ArrowWide)"
d="M 226.2693,25.28543 H 183.09695"
id="path34" />
<path
style="fill:none;stroke:#000000;stroke-width:0.264583;stroke-dasharray:2.11666, 0.264583;stroke-dashoffset:0;marker-end:url(#ArrowWide)"
d="M 226.2693,42.010871 H 183.09695"
id="path35" />
<g
id="g35"
transform="translate(-15.952034,-50.670754)">
<rect
style="fill:#81ffc7;fill-opacity:1;stroke:#000000;stroke-width:0.526025;stroke-dasharray:none"
id="rect35"
width="52.478481"
height="17.513748"
x="153.81845"
y="107.17479"
ry="1.6990631" />
<text
xml:space="preserve"
style="font-size:3.175px;fill:none;stroke:#000000;stroke-width:0.264583;stroke-dasharray:none"
x="158.16835"
y="113.52198"
id="text35"><tspan
sodipodi:role="line"
id="tspan35"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="158.16835"
y="113.52198">Object C1 (Empty)</tspan><tspan
sodipodi:role="line"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="158.16835"
y="117.64227"
id="tspan46">+ blueprintName(&quot;ObjectC&quot;)</tspan><tspan
sodipodi:role="line"
style="stroke-width:0.264583"
x="158.16835"
y="121.76254"
id="tspan49"><tspan
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;fill:#000000;fill-opacity:1;stroke:none"
id="tspan80">+ spawnHere</tspan> </tspan></text>
</g>
<path
style="fill:none;stroke:#000000;stroke-width:0.264583;stroke-dasharray:2.11666, 0.264583;stroke-dashoffset:0;marker-end:url(#ArrowWide)"
d="m 226.2693,42.010871 -42.64565,15.18405"
id="path36"
sodipodi:nodetypes="cc" />
</g>
<text
xml:space="preserve"
style="font-size:3.175px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="124.89855"
y="112.52866"
id="text36"><tspan
sodipodi:role="line"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:'Ubuntu Bold';fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="124.89855"
y="112.52866"
id="tspan39">Determine used Collections to export</tspan></text>
<rect
style="fill:#ff9342;fill-opacity:1;stroke:#000000;stroke-width:0.32101;stroke-dasharray:none"
id="rect36"
width="56.190514"
height="15.891233"
x="138.2263"
y="116.5061"
ry="1.5416578" />
<text
xml:space="preserve"
style="font-size:3.175px;fill:none;stroke:#000000;stroke-width:0.264583;stroke-dasharray:none"
x="142.59734"
y="122.87442"
id="text37"><tspan
sodipodi:role="line"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="142.59734"
y="122.87442"
id="tspan40">Object B</tspan><tspan
sodipodi:role="line"
style="stroke-width:0.264583"
x="142.59734"
y="126.84317"
id="tspan41" /></text>
<rect
style="fill:#ff9342;fill-opacity:1;stroke:#000000;stroke-width:0.321404;stroke-dasharray:none"
id="rect37"
width="56.190121"
height="15.930397"
x="138.2265"
y="134.55496"
ry="1.5454572" />
<text
xml:space="preserve"
style="font-size:3.175px;fill:none;stroke:#000000;stroke-width:0.264583;stroke-dasharray:none"
x="142.59734"
y="140.92308"
id="text38"><tspan
sodipodi:role="line"
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="142.59734"
y="140.92308"
id="tspan42">Object C</tspan><tspan
sodipodi:role="line"
style="stroke-width:0.264583"
x="142.59734"
y="144.89183"
id="tspan43" /></text>
<path
style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:1;stroke-dasharray:none;stroke-dashoffset:0;marker-end:url(#RoundedArrow)"
d="m 108.69996,177.9456 v 9.30451"
id="path49"
sodipodi:nodetypes="cc" />
<rect
style="fill:none;stroke:#000000;stroke-width:0.336158;stroke-dasharray:none"
id="rect49"
width="201.28026"
height="86.298004"
x="4.9929576"
y="193.0849"
ry="3.0587275" />
<text
xml:space="preserve"
style="font-size:3.175px;fill:#000000;stroke:none;stroke-width:0.264583;fill-opacity:1"
x="106.60861"
y="201.20203"
id="text50"><tspan
sodipodi:role="line"
id="tspan50"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:'Ubuntu Bold';fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="106.60861"
y="201.20203">Result</tspan></text>
<rect
style="fill:none;stroke:#000000;stroke-width:0.272675;stroke-dasharray:none"
id="rect50"
width="82.256355"
height="48.138226"
x="115.07077"
y="207.5529"
ry="4.6700392" />
<g
id="g63"
transform="translate(-90.659631,198.60285)">
<g
id="g52"
transform="translate(-8.7898623,-68.268248)"
style="fill:#efff81;fill-opacity:1">
<rect
style="fill:#efff81;fill-opacity:1;stroke:#000000;stroke-width:0.264583;stroke-dasharray:none"
id="rect52"
width="21.240173"
height="10.947481"
x="120.16953"
y="92.749634"
ry="1.0620492" />
<text
xml:space="preserve"
style="font-size:3.175px;fill:#efff81;fill-opacity:1;stroke:#000000;stroke-width:0.264583;stroke-dasharray:none"
x="124.65015"
y="97.110878"
id="text52"><tspan
sodipodi:role="line"
id="tspan52"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="124.65015"
y="97.110878">Entity A</tspan><tspan
sodipodi:role="line"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="124.65015"
y="101.23116"
id="tspan74">(unique)</tspan></text>
</g>
<rect
style="fill:none;stroke:#000000;stroke-width:0.336001;stroke-dasharray:none"
id="rect51"
width="89.327034"
height="67.307678"
x="104.25188"
y="9.3385649"
ry="6.5297275" />
<text
xml:space="preserve"
style="font-size:3.175px;fill:#0090ff;fill-opacity:1;stroke:#0090ff;stroke-width:0.264583;stroke-opacity:1"
x="135.33"
y="14.70156"
id="text53"><tspan
sodipodi:role="line"
id="tspan53"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:'Ubuntu Bold';fill:#0090ff;fill-opacity:1;stroke:none;stroke-width:0.264583;stroke-opacity:1"
x="135.33"
y="14.70156">world/level.gltf</tspan></text>
<g
id="g56"
transform="translate(-19.939658,-65.832448)"
style="fill:#81ffc7;fill-opacity:1">
<rect
style="fill:#81ffc7;fill-opacity:1;stroke:#000000;stroke-width:0.505434;stroke-dasharray:none"
id="rect53"
width="52.860485"
height="16.052649"
x="157.90318"
y="84.256462"
ry="1.5573175" />
<text
xml:space="preserve"
style="font-size:3.175px;fill:#81ffc7;fill-opacity:1;stroke:#000000;stroke-width:0.264583;stroke-dasharray:none"
x="162.26337"
y="90.613953"
id="text56"><tspan
sodipodi:role="line"
id="tspan54"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="162.26337"
y="90.613953">Entity B1</tspan><tspan
sodipodi:role="line"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="162.26337"
y="94.734238"
id="tspan55">+ blueprintName(&quot;ObjectB&quot;)</tspan><tspan
sodipodi:role="line"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="162.26337"
y="98.854515"
id="tspan56">+ spawnHere </tspan></text>
</g>
<g
id="g59"
transform="translate(-15.528392,-70.796885)">
<rect
style="fill:#81ffc7;fill-opacity:1;stroke:#000000;stroke-width:0.528181;stroke-dasharray:none"
id="rect56"
width="51.925056"
height="17.845793"
x="153.81953"
y="107.17587"
ry="1.7312757" />
<text
xml:space="preserve"
style="font-size:3.175px;fill:none;stroke:#000000;stroke-width:0.264583;stroke-dasharray:none"
x="158.16835"
y="113.52198"
id="text59"><tspan
sodipodi:role="line"
id="tspan57"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="158.16835"
y="113.52198">Entity C0</tspan><tspan
sodipodi:role="line"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="158.16835"
y="117.64227"
id="tspan58">+ blueprintName(&quot;ObjectC&quot;)</tspan><tspan
sodipodi:role="line"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="158.16835"
y="121.76254"
id="tspan59">+ spawnHere </tspan></text>
</g>
<path
style="fill:none;stroke:#000000;stroke-width:0.264583;stroke-dasharray:2.11666, 0.264583;stroke-dashoffset:0;marker-end:url(#ArrowWide)"
d="M 226.2693,25.28543 H 183.09695"
id="path59" />
<path
style="fill:none;stroke:#000000;stroke-width:0.264583;stroke-dasharray:2.11666, 0.264583;stroke-dashoffset:0;marker-end:url(#ArrowWide)"
d="M 226.2693,42.010871 H 183.09695"
id="path60" />
<g
id="g62"
transform="translate(-15.952034,-50.670754)">
<rect
style="fill:#81ffc7;fill-opacity:1;stroke:#000000;stroke-width:0.526025;stroke-dasharray:none"
id="rect60"
width="52.478481"
height="17.513748"
x="153.81845"
y="107.17479"
ry="1.6990631" />
<text
xml:space="preserve"
style="font-size:3.175px;fill:none;stroke:#000000;stroke-width:0.264583;stroke-dasharray:none"
x="158.16835"
y="113.52198"
id="text62"><tspan
sodipodi:role="line"
id="tspan60"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="158.16835"
y="113.52198">Entity C1</tspan><tspan
sodipodi:role="line"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="158.16835"
y="117.64227"
id="tspan61">+ blueprintName(&quot;ObjectC&quot;)</tspan><tspan
sodipodi:role="line"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="158.16835"
y="121.76254"
id="tspan62">+ spawnHere </tspan></text>
</g>
<path
style="fill:none;stroke:#000000;stroke-width:0.264583;stroke-dasharray:2.11666, 0.264583;stroke-dashoffset:0;marker-end:url(#ArrowWide)"
d="m 226.2693,42.010871 -42.64565,15.18405"
id="path62"
sodipodi:nodetypes="cc" />
</g>
<text
xml:space="preserve"
style="font-size:3.175px;fill:#0090ff;fill-opacity:1;stroke:#0090ff;stroke-width:0.264583;stroke-opacity:1"
x="117.87988"
y="214.18282"
id="text63"><tspan
sodipodi:role="line"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:'Ubuntu Bold';fill:#0090ff;fill-opacity:1;stroke:none;stroke-width:0.264583;stroke-opacity:1"
x="117.87988"
y="214.18282"
id="tspan63">Library of gltf files (one per Collection/Blueprint)</tspan></text>
<rect
style="fill:#ff9342;fill-opacity:1;stroke:#000000;stroke-width:0.32101;stroke-dasharray:none"
id="rect63"
width="56.190514"
height="15.891233"
x="138.2263"
y="218.15141"
ry="1.5416578" />
<text
xml:space="preserve"
style="font-size:3.175px;fill:none;stroke:#000000;stroke-width:0.264583;stroke-dasharray:none"
x="142.59734"
y="224.51973"
id="text66"><tspan
sodipodi:role="line"
id="tspan64"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="142.59734"
y="224.51973">Object B.gltf </tspan><tspan
sodipodi:role="line"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="142.59734"
y="228.64001"
id="tspan65"> + blueprintName(&quot;ObjectB&quot;)</tspan><tspan
sodipodi:role="line"
style="stroke-width:0.264583"
x="142.59734"
y="232.60876"
id="tspan66" /></text>
<rect
style="fill:#ff9342;fill-opacity:1;stroke:#000000;stroke-width:0.321404;stroke-dasharray:none"
id="rect66"
width="56.190121"
height="15.930397"
x="138.2265"
y="236.20027"
ry="1.5454572" />
<text
xml:space="preserve"
style="font-size:3.175px;fill:none;stroke:#000000;stroke-width:0.264583;stroke-dasharray:none"
x="142.59734"
y="242.56839"
id="text69"><tspan
sodipodi:role="line"
id="tspan67"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="142.59734"
y="242.56839">Object C:.gltf</tspan><tspan
sodipodi:role="line"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="142.59734"
y="246.68867"
id="tspan68"> + blueprintName(&quot;ObjectC&quot;)</tspan><tspan
sodipodi:role="line"
style="stroke-width:0.264583"
x="142.59734"
y="250.65742"
id="tspan69" /></text>
<g
id="g70"
transform="translate(0,11.775225)">
<rect
style="fill:#ff9342;fill-opacity:1;stroke:#000000;stroke-width:0.27923;stroke-dasharray:none"
id="rect69"
width="56.232292"
height="12.014979"
x="138.20541"
y="46.766541"
ry="1.1656103" />
<text
xml:space="preserve"
style="font-size:3.175px;fill:none;stroke:#000000;stroke-width:0.264583;stroke-dasharray:none"
x="142.59734"
y="52.097412"
id="text70"><tspan
sodipodi:role="line"
id="tspan70"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="142.59734"
y="52.097412">Object D: unused collection/ </tspan><tspan
sodipodi:role="line"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="142.59734"
y="56.217693"
id="tspan71">blueprint</tspan></text>
</g>
<path
style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:1;stroke-dasharray:none;stroke-dashoffset:0;marker-end:url(#RoundedArrow)"
d="M 56.761407,72.285855 V 101.91653"
id="path74"
sodipodi:nodetypes="cc" />
<path
style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:1;stroke-dasharray:none;stroke-dashoffset:0;marker-end:url(#RoundedArrow)"
d="m 56.761407,173.65448 v 29.63068"
id="path75"
sodipodi:nodetypes="cc" />
<path
style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:1;stroke-dasharray:none;stroke-dashoffset:0;marker-end:url(#RoundedArrow)"
d="M 157.79469,72.285855 V 101.91653"
id="path76"
sodipodi:nodetypes="cc" />
<path
style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:1;stroke-dasharray:none;stroke-dashoffset:0;marker-end:url(#RoundedArrow)"
d="m 157.79469,173.40943 v 29.63068"
id="path77"
sodipodi:nodetypes="cc" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 38 KiB

BIN
docs/workflow_empties.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

BIN
docs/workflow_original.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

View File

@ -0,0 +1,20 @@
# Workflow with blender / demo information
This example, is actually closer to a boilerplate + tooling showcases how to use a minimalistic [Blender](https://www.blender.org/) (gltf) centric workflow for [Bevy](https://bevyengine.org/), ie defining entites & their components
inside Blender using Blender's objects **custom properties**.
Aka "Blender as editor for Bevy"
It also allows you to setup 'blueprints' in Blender by using collections (the recomended way to go most of the time), or directly on single use objects .
## Running this example
```
cargo run --example advanced --features bevy/dynamic_linking
```
### Additional notes
* You usually define either the Components directly or use ```Proxy components``` that get replaced in Bevy systems with the actual Components that you want (usually when for some reason, ie external crates with unregistered components etc) you cannot use the components directly.
* this example contains code for future features, not finished yet ! please disregard anything related to saving & loading

53
examples/advanced/TODO.md Normal file
View File

@ -0,0 +1,53 @@
- [x] seperate "spawn velocity" into a seperate (optional) component
- [x] try dynamically spawned entities with save & load
- [ ] fix issues with multiple entites having the same name ??
- [ ] fix issues with system ordering
- [x] add sets for proxy-replacements
- [x] annoying camera movement : camera should be saveable as well
=> we need to be able to only save the camera position (and name)
- [ ] only spawn "new level" stuff when transitioning from menu to game ?
- [ ] put more thoughts into this
- [ ] rework how save-loading is triggered
- [ ] no hardcoded keypresses
- [ ] ability to choose what save to load ?
- [ ] take a look at bevy_moonshine_save
- [ ] move to system pipelines
- [x] split Blueprints into a seperate crate: perhaps bevy_gltf_blueprints
- [x] how to deal with states that are not defined as part of the plugin/crate ?
- [x] same issue for the assets
- [ ] support multiple main scenes in the blender plugin ?
- [ ] study possibilities of expanding the bevy & blender tooling side to define UIS
- likely using the blender data only as a placeholder/ directly replace in Python
- system ordering ?
load level => inject components => spawn blueprint entities/rehydrate => (loading) => replace proxies
OR
load level => inject components => (loading) => spawn blueprint entities/rehydrate => replace proxies
- perhaps it does make more sense to save ALL entities and not just the dynamic ones? mostly as we have the blueprints anyway, which should cut down on needed data ?
- different approaches for dealing with saving/loading
in particular the problem of entites that are defined as part of the level but can be despawned (items that get picked up etc)
Bevy side
* var 1 : spawn all entities completely, despawn those saveables that are not actually present in the save data but that have been spawned
* problems: needs correct ordering of systems otherwise the diffing above will not work
* pros: minimal save files, only bevy boilerplate
* cons: requires spawning potentially huge gltf files just to "peek" inside of them to know if they are saveable
* var 2 : save both saveables & unsaveables but only save the absolute minimal data for unsaveables
* problems: how to combine different filters into a single save file ?
* pros: easier diffing, more homogeneous handling
* cons: a lot bigger save file with mostly useless data
Blender side
* var 3 => CHOSEN OPTION : mark INSTANCES in advance as static/dynamic (ie saveable or not), thus this data would be present in the world/level and not inside the gltf data
* problems: would require adding custom properties to each instance in Blender (COULD be automated by 'peeking' inside the collection)
* pros: simpler, and this might be more "editor-like" where you would mark each item as static or not
* cons: potentially a lot of manual work / error prone

View File

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

View File

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

View File

@ -0,0 +1,34 @@
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,
"advanced/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,
"advanced/assets_game.assets.ron",
)
.add_collection_to_loading_state::<_, GameAssets>(AppState::AppLoading)
;
}
}

View File

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

View File

@ -0,0 +1,94 @@
pub mod camera;
use bevy_rapier3d::prelude::Velocity;
pub use camera::*;
pub mod lighting;
pub use lighting::*;
pub mod relationships;
pub use relationships::*;
pub mod physics;
pub use physics::*;
// pub mod blueprints;
// pub use blueprints::*;
pub mod save_load;
pub use save_load::*;
use bevy::prelude::*;
use bevy_gltf_blueprints::*;
use rand::Rng;
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);
}
}
pub struct CorePlugin;
impl Plugin for CorePlugin {
fn build(&self, app: &mut App) {
app
.add_plugins((
LightingPlugin,
CameraPlugin,
PhysicsPlugin,
SaveLoadPlugin,
BlueprintsPlugin{
library_folder: "advanced/models/library".into()
}
))
// just for testing
.add_systems(
Update,
spawn_test
)
;
}
}

View File

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

View File

@ -0,0 +1,39 @@
pub mod physics_replace_proxies;
pub use physics_replace_proxies::*;
pub mod utils;
pub mod controls;
pub use controls::*;
use bevy::prelude::*;
use crate::state::GameState;
// 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
.register_type::<AutoAABBCollider>()
.register_type::<physics_replace_proxies::Collider>()
.register_type::<physics_replace_proxies::RigidBodyProxy>()
// find a way to make serde's stuff serializable
// .register_type::<bevy_rapier3d::dynamics::CoefficientCombineRule>()
//bevy_rapier3d::dynamics::CoefficientCombineRule
.add_systems(Update, physics_replace_proxies.after(GltfBlueprintsSet::AfterSpawn))
.add_systems(
OnEnter(GameState::InGame),
resume_physics
)
.add_systems(
OnExit(GameState::InGame),
pause_physics
)
;
}
}

View File

@ -0,0 +1,121 @@
use bevy::prelude::*;
// use bevy::render::primitives::Aabb;
use bevy_rapier3d::geometry::Collider as RapierCollider;
use bevy_rapier3d::dynamics::RigidBody as RapierRigidBody;
use bevy_rapier3d::prelude::{ComputedColliderShape, ActiveEvents, ActiveCollisionTypes};
use super::utils::*;
#[derive(Component, Reflect, Default, Debug, )]
#[reflect(Component)]
pub enum RigidBodyProxy{
#[default]
Dynamic,
Fixed,
Position,
Velocity
}
#[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>>,
// rigidbodies
proxy_rigidbodies: Query<(Entity, &RigidBodyProxy,), (Without<RapierRigidBody>, Added<RigidBodyProxy>)>,
mut proxy_colliders: Query<(Entity, &Collider, &Name, &mut Visibility), (Without<RapierCollider>, Added<Collider>)>,
// needed for tri meshes
children: Query<&Children>,
mut commands: Commands,
) {
for proxy_colider in proxy_colliders.iter_mut() {
let (entity, collider_proxy, name, mut visibility) = proxy_colider;
// we hide the collider meshes: perhaps they should be removed altogether once processed ?
if name.ends_with( "_collider" ) || name.ends_with( "_sensor" ) {
*visibility = Visibility::Hidden;
}
let mut rapier_collider:RapierCollider;
match collider_proxy{
Collider::Ball(radius) => {
info!("generating collider from proxy: ball");
rapier_collider = RapierCollider::ball(*radius);
commands.entity(entity)
.insert(rapier_collider)
.insert(ActiveEvents::COLLISION_EVENTS) // FIXME: this is just for demo purposes !!!
;
}
Collider::Cuboid(size) => {
info!("generating collider from proxy: cuboid");
rapier_collider = RapierCollider::cuboid(size.x, size.y, size.z);
commands.entity(entity)
.insert(rapier_collider)
.insert(ActiveEvents::COLLISION_EVENTS) // FIXME: this is just for demo purposes !!!
;
}
Collider::Capsule(a, b, radius) => {
info!("generating collider from proxy: capsule");
rapier_collider = RapierCollider::capsule(*a, *b, *radius);
commands.entity(entity)
.insert(rapier_collider)
.insert(ActiveEvents::COLLISION_EVENTS) // FIXME: this is just for demo purposes !!!
;
}
Collider::Mesh => {
info!("generating collider from proxy: mesh");
for (_, collider_mesh) in Mesh::search_in_children(entity, &children, &meshes, &mesh_handles)
{
rapier_collider = RapierCollider::from_bevy_mesh(collider_mesh, &ComputedColliderShape::TriMesh).unwrap();
commands.entity(entity)
.insert(rapier_collider)
// FIXME: this is just for demo purposes !!!
.insert(ActiveCollisionTypes::default() | ActiveCollisionTypes::KINEMATIC_STATIC | ActiveCollisionTypes::STATIC_STATIC | ActiveCollisionTypes::DYNAMIC_STATIC)
.insert(ActiveEvents::COLLISION_EVENTS)
;
// .insert(ActiveEvents::COLLISION_EVENTS)
// break;
// RapierCollider::convex_hull(points)
}
}
}
}
// rigidbodies
for (entity, proxy_rigidbody) in proxy_rigidbodies.iter() {
info!("Generation rigid body from Proxy rigid body !! {:?}", proxy_rigidbody );
let rigid_body:RapierRigidBody = match proxy_rigidbody {
RigidBodyProxy::Dynamic => RapierRigidBody::Dynamic,
RigidBodyProxy::Fixed=> RapierRigidBody::Fixed,
RigidBodyProxy::Position => RapierRigidBody::KinematicPositionBased,
RigidBodyProxy::Velocity => RapierRigidBody::KinematicVelocityBased,
};
commands.entity(entity)
.insert(rigid_body)
// IMPORTANT ! this allows collisions between dynamic & static(fixed) entities
// see https://rapier.rs/docs/user_guides/bevy_plugin/colliders#active-collision-types
.insert(ActiveCollisionTypes::default() | ActiveCollisionTypes::KINEMATIC_STATIC | ActiveCollisionTypes::STATIC_STATIC | ActiveCollisionTypes::DYNAMIC_STATIC)
.insert(ActiveEvents::COLLISION_EVENTS)
;
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,95 @@
use bevy::pbr::{Clusters, VisiblePointLights};
use bevy::render::camera::CameraRenderGraph;
use bevy::render::view::VisibleEntities;
use bevy::{prelude::*, gltf::GltfExtras};
use bevy::tasks::IoTaskPool;
use bevy_rapier3d::prelude::RigidBody;
use std::io::Write;
use std::fs::File;
use crate::core::physics::{Collider, RigidBodyProxy};
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::<RigidBodyProxy>()
.deny::<Saveable>()
// camera stuff
.deny::<Camera>()
.deny::<CameraRenderGraph>()
.deny::<Camera3d>()
.deny::<Clusters>()
.deny::<VisibleEntities>()
.deny::<VisiblePointLights>()
//.deny::<HasGizmoMarker>()
.extract_entities(saveable_entities.into_iter());
let dyn_scene = scene_builder.build();
let serialized_scene = dyn_scene.serialize_ron(world.resource::<AppTypeRegistry>()).unwrap();
#[cfg(not(target_arch = "wasm32"))]
IoTaskPool::get()
.spawn(async move {
// Write the scene RON data to file
File::create(format!("assets/scenes/{NEW_SCENE_FILE_PATH}"))
.and_then(|mut file| file.write(serialized_scene.as_bytes()))
.expect("Error while writing scene to file");
})
.detach();
}

View File

@ -0,0 +1,30 @@
use bevy::prelude::*;
use crate::{assets::GameAssets, state::{InAppRunning, GameState}};
use bevy_gltf_blueprints::GameWorldTag;
pub fn setup_game(
mut commands: Commands,
game_assets: Res<GameAssets>,
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 {
scene: game_assets.world.clone(),
..default()
},
bevy::prelude::Name::from("world"),
GameWorldTag,
InAppRunning
));
next_game_state.set(GameState::InGame)
}

View File

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

View File

@ -0,0 +1,136 @@
pub mod in_game;
pub use in_game::*;
pub mod in_main_menu;
pub use in_main_menu::*;
pub mod picking;
pub use picking::*;
use bevy::prelude::*;
use bevy_rapier3d::prelude::*;
use crate::{insert_dependant_component, state::{AppState, GameState}};
// 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.iter() {
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.iter() {
println!("Received contact force event: {:?}", contact_force_event);
}
}
pub struct GamePlugin;
impl Plugin for GamePlugin {
fn build(&self, app: &mut App) {
app
.add_plugins(PickingPlugin)
.register_type::<Interactible>()
.register_type::<SoundMaterial>()
.register_type::<Player>()
// little helper utility, to automatically inject components that are dependant on an other component
// ie, here an Entity with a Player component should also always have a ShouldBeWithPlayer component
// you get a warning if you use this, as I consider this to be stop-gap solution (usually you should have either a bundle, or directly define all needed components)
.add_systems(Update, (
// insert_dependant_component::<Player, ShouldBeWithPlayer>,
player_move_demo, //.run_if(in_state(AppState::Running)),
// test_collision_events
)
.run_if(in_state(GameState::InGame)))
.add_systems(
OnEnter(AppState::MenuRunning),
setup_main_menu
)
.add_systems(
OnExit(AppState::MenuRunning),
teardown_main_menu
)
.add_systems(Update, (main_menu))
.add_systems(
OnEnter(AppState::AppRunning),
setup_game
)
;
}
}

View File

@ -0,0 +1,40 @@
use bevy::prelude::*;
use super::Player;
#[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, //.run_if(in_state(AppState::Running)),
))
;
}
}

51
examples/advanced/main.rs Normal file
View File

@ -0,0 +1,51 @@
use std::time::Duration;
use bevy::{prelude::*, asset::ChangeWatcher, gltf::Gltf};
use bevy_editor_pls::prelude::*;
use bevy_rapier3d::prelude::*;
use bevy_gltf_components::ComponentsFromGltfPlugin;
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 {
// This tells the AssetServer to watch for changes to assets.
// It enables our scenes to automatically reload in game when we modify their files.
// practical in our case to be able to edit the shaders without needing to recompile
// watch_for_changes: ChangeWatcher::with_delay(Duration::from_millis(50)), : FIXME: breaks scene save/loading
..default()
}
),
// editor
EditorPlugin::default(),
// physics
RapierPhysicsPlugin::<NoUserData>::default(),
RapierDebugRenderPlugin::default(),
// our custom plugins
ComponentsFromGltfPlugin,
StatePlugin,
AssetsPlugin,
CorePlugin, // reusable plugins
GamePlugin, // specific to our game
ComponentsTestPlugin // Showcases different type of components /structs
))
.run();
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -73,7 +73,7 @@ fn setup(
asset_server: Res<AssetServer>, asset_server: Res<AssetServer>,
) { ) {
let tmp: Handle<Scene> = asset_server.load("models/level1.glb#Scene0"); let tmp: Handle<Scene> = asset_server.load("basic/models/level1.glb#Scene0");
commands.insert_resource(AssetLoadHelper(tmp)); commands.insert_resource(AssetLoadHelper(tmp));
} }

View File

@ -0,0 +1,86 @@
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>>()
;
}
}

View File

@ -1,57 +0,0 @@
pub mod utils;
pub use utils::*;
pub mod gltf_to_components;
pub use gltf_to_components::*;
pub mod process_gltfs;
pub use process_gltfs::*;
use bevy::prelude::{
App,Plugin, PreUpdate
};
/// A Bevy plugin for extracting components from gltf files and automatically adding them to the relevant entities
/// It will automatically run every time you load a gltf file
/// Add this plugin to your Bevy app to get access to this feature
/// ```
/// # use bevy::prelude::*;
/// # use bevy_gltf_components::ComponentsFromGltfPlugin::*;
///
///
/// fn main() {
/// App::new()
/// .add_plugins(DefaultPlugins)
/// .add_plugin(GltfComponentsPlugin)
/// .add_startup_system(setup)
/// .add_system(spawn_level)
/// .run();
/// }
///
/// fn setup(
/// mut state: ResMut<State>,
/// asset_server: Res<AssetServer>,
/// mut commands: bevy::prelude::Commands
/// ){
/// asset_server.load("models/level1.glb#Scene0")
/// }
///
/// fn spawn_level(){
///
///}
/// ```
#[derive(Default)]
pub struct ComponentsFromGltfPlugin;
impl Plugin for ComponentsFromGltfPlugin {
fn build(&self, app: &mut App) {
app
.insert_resource(GltfLoadingTracker::new())
.add_systems(PreUpdate, (
track_new_gltf,
process_loaded_scenes,
))
;
}
}

View File

@ -1,29 +0,0 @@
# gltf_auto_export
For convenience I also added this [Blender addon](./blender_auto_export_gltf.py) that automatically exports your level/world from Blender to gltf whenever you save your Blend file
(actually when you save inside your level/world scene or in the "library" scene, where I personally usually store all collections to instanciate).
It is **very** barebones and messy, but it does a minimal ok job.
### Installation:
* in Blender go to edit => preferences => install
![blender addon install](../../docs/blender_addon_install.png)
* choose the path where ```blender_auto_export/blender_auto_export_gltf.py``` is stored
![blender addon install](../../docs/blender_addon_install2.png)
### Usage:
* before it can automatically save to gltf, you need to configure it
* go to file => export => gltf auto export
![blender addon use](../../docs/blender_addon_use.png)
* set up your parameters: output path, name of your main scene etc
![blender addon use2](../../docs/blender_addon_use2.png)
* click on "apply settings"
* now next time you save your blend file you will get an automatically exported gltf file

View File

@ -0,0 +1,85 @@
# gltf_auto_export
For convenience I also added this [Blender addon](./gltf_auto_export.py) that
- automatically exports your level/world from Blender to gltf whenever you save your Blend file.
- it also supports automatical exports of used collections as [Gltf blueprints](../../crates/bevy_gltf_blueprints/README.md) & more !
## Installation:
* in Blender go to edit => preferences => install
![blender addon install](../../docs/blender_addon_install.png)
* choose the path where ```gltf_auto_export/gltf_auto_export.py``` is stored
![blender addon install](../../docs/blender_addon_install2.png)
## Usage:
### Basics
* before it can automatically save to gltf, you need to configure it
* go to file => export => gltf auto export
![blender addon use](../../docs/blender_addon_use.png)
* set the autoexport parameters : output path, name of your main scene etc in the **auto export** panel
![blender addon use3](../../docs/blender_addon_use3.png)
* and your standard gltf export parameters in the **gltf** panel
![blender addon use2](../../docs/blender_addon_use2.png)
* click on "apply settings"
* now next time you save your blend file you will get an automatically exported gltf file (or more than one, depending on your settings, see below)
### Blueprints
You can enable this option to automatically replace all the **collection instances** inside your main scene with blueprints
- whenever you change your main scene (or your library scene , if that option is enabled), all your collection instances
* will be replaced with empties (this will not be visible to you)
* those empties will have additional custom properties / components : ```BlueprintName``` & ```SpawnHere```
* your main scene/ level will be exported to a much more trimmed down gltf file (see next point)
* all the original collections (that you used to create the instances) will be exported as **seperate gltf files** into the "library" folder
- this means you will have
* one small main gltf file (your level/world)
* as many gltf files as you have used collections in the main scene , in the library path you specified :
for the included [advanced](../../examples/advanced/) example's [assets](../../assets/advanced/models/), it looks something like this:
![library](../../docs/exported_library_files.png)
the .blend file that they are generated from can be found [here](../../assets/advanced/advanced.blend)
#### Process
This is the internal logic of the export process with blueprints
![process](../../docs/process.svg)
ie this is an example scene...
![](../../docs/workflow_original.jpg)
and what actually gets exported for the main scene/world/level
![](../../docs/workflow_empties.jpg)
all collections instances replaced with empties, and all those collections exported to gltf files as seen above
### TODO:
- [ ] add ability to have multiple main & library scenes
- [ ] detect which objects have been changed to only re-export those
## License
This tool, all its code, contents & assets is Dual-licensed under either of
- Apache License, Version 2.0, ([LICENSE-APACHE](../LICENSE_APACHE.md) or https://www.apache.org/licenses/LICENSE-2.0)
- MIT license ([LICENSE-MIT](../LICENSE_MIT.md) or https://opensource.org/licenses/MIT)

View File

@ -1,4 +1,4 @@
#TODO: this is not actually in use yet, just use the blender_auto_export_gltf.py file #TODO: this is not actually in use yet, just use the gltf_auto_export.py file
bl_info = { bl_info = {
"name": "Test glTF/glb auto-export", "name": "Test glTF/glb auto-export",
"author": "kaosigh", "author": "kaosigh",
@ -14,11 +14,11 @@ bl_info = {
import bpy import bpy
from .blender_auto_export_gltf import TEST_AUTO_OT_gltf from .gltf_auto_export import TEST_AUTO_OT_gltf
from .blender_auto_export_gltf import deps_update_handler from .gltf_auto_export import deps_update_handler
from .blender_auto_export_gltf import save_handler from .gltf_auto_export import save_handler
from .blender_auto_export_gltf import get_changedScene from .gltf_auto_export import get_changedScene
from .blender_auto_export_gltf import set_ChangedScene from .gltf_auto_export import set_ChangedScene
# Only needed if you want to add into a dynamic menu # Only needed if you want to add into a dynamic menu

View File

@ -0,0 +1,911 @@
bl_info = {
"name": "gltf_auto_export",
"author": "kaosigh",
"version": (0, 1),
"blender": (3, 4, 0),
"location": "File > Import-Export",
"description": "glTF/glb auto-export",
"warning": "",
"wiki_url": "",
"tracker_url": "",
"category": "Import-Export"
}
import os
import bpy
from bpy.types import Operator, AddonPreferences
from bpy.app.handlers import persistent
from bpy_extras.io_utils import ExportHelper
from bpy.props import (BoolProperty,
IntProperty,
StringProperty,
EnumProperty,
CollectionProperty
)
#see here for original gltf exporter infos https://github.com/KhronosGroup/glTF-Blender-IO/blob/main/addons/io_scene_gltf2/__init__.py
@persistent
def deps_update_handler(scene, depsgraph):
#print("depsgraph_update_post", scene.name)
"""
print("-------------")
changed_objects = []
for obj in depsgraph.updates:
if isinstance(obj.id, bpy.types.Object):
print("object changed, amazing", obj.id, obj.id.name)
changed_objects.append(obj)
"""
changed = scene.name or ""
# bpy.context.scene.changedObjects = changed_objects
bpy.context.scene.changedScene = changed
@persistent
def save_handler(dummy):
print("-------------")
print("saved", bpy.data.filepath)
auto_export()
def get_changedScene(self):
return self["changedScene"]
def set_changedScene(self, value):
self["changedScene"] = value
#def get_changedObjects(self):
# return self["changedObjects"]
#def set_changedObjects(self, value):
# self["changedObjects"] = value
#https://docs.blender.org/api/current/bpy.ops.export_scene.html#bpy.ops.export_scene.gltf
def export_gltf (path, export_settings):
settings = {**export_settings, "filepath": path}
bpy.ops.export_scene.gltf(**settings)
#####################################################
#### Helpers ####
def get_collection_hierarchy(root_col, levels=1):
"""Read hierarchy of the collections in the scene"""
level_lookup = {}
def recurse(root_col, parent, depth):
if depth > levels:
return
if isinstance(parent, bpy.types.Collection):
level_lookup.setdefault(parent, []).append(root_col)
for child in root_col.children:
recurse(child, root_col, depth + 1)
recurse(root_col, root_col.children, 0)
return level_lookup
# the active collection is a View Layer concept, so you actually have to find the active LayerCollection
# which must be done recursively
def find_layer_collection_recursive(find, col):
for c in col.children:
if c.collection == find:
return c
return None
#Recursivly transverse layer_collection for a particular name
def recurLayerCollection(layerColl, collName):
found = None
if (layerColl.name == collName):
return layerColl
for layer in layerColl.children:
found = recurLayerCollection(layer, collName)
if found:
return found
# Makes an empty, at location, stores it in existing collection, from https://blender.stackexchange.com/questions/51290/how-to-add-empty-object-not-using-bpy-ops
def make_empty(name, location, coll_name): #string, vector, string of existing coll
empty_obj = bpy.data.objects.new( "empty", None, )
empty_obj.name = name
empty_obj.empty_display_size = 1
bpy.data.collections[coll_name].objects.link(empty_obj)
empty_obj.location = location
return empty_obj
def make_empty2(name, location, collection):
object_data = None #bpy.data.meshes.new("NewMesh") #None
empty_obj = bpy.data.objects.new( name, object_data )
empty_obj.name = name
empty_obj.location = location
empty_obj.empty_display_size = 2
empty_obj.empty_display_type = 'PLAIN_AXES'
collection.objects.link( empty_obj )
return empty_obj
def make_empty3(name, location, collection):
original_active_object = bpy.context.active_object
bpy.ops.object.empty_add(type='PLAIN_AXES', location=location)
empty_obj = bpy.context.active_object
empty_obj.name = name
collection.objects.link( empty_obj )
bpy.context.view_layer.objects.active = original_active_object
return empty_obj
# generate a copy of a scene that replaces collection instances with empties
# FIXME: will not preserve original names
# 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):
root_collection = scene.collection
temp_scene = bpy.data.scenes.new(name="temp_scene")
copy_root_collection = temp_scene.collection
scene_objects = [o for o in root_collection.objects]
found = find_layer_collection_recursive(copy_root_collection, bpy.context.view_layer.layer_collection)
if found:
print("FOUND COLLECTION")
# 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 = []
for object in scene_objects:
if object.instance_type == 'COLLECTION':
collection_name = object.instance_collection.name
#original_names[object.name] = object.name# + "____bak"
#print("custom properties", object, object.keys(), object.items())
#for k, e in object.items():
# print("custom properties ", k, e)
print("object location", object.location)
original_name = object.name
original_names.append(original_name)
object.name = original_name + "____bak"
empty_obj = make_empty3(original_name, object.location, copy_root_collection)
"""we inject the collection/blueprint name, as a component called 'BlueprintName', but we only do this in the empty, not the original object"""
empty_obj['BlueprintName'] = '"'+collection_name+'"'
empty_obj['SpawnHere'] = ''
for k, v in object.items():
empty_obj[k] = v
else:
copy_root_collection.objects.link(object)
# bpy.data.scenes.remove(temp_scene)
# objs = bpy.data.objects
#objs.remove(objs["Cube"], do_unlink=True)
return (temp_scene, original_names)
# clear & remove "hollow scene"
def clear_hollow_scene(temp_scene, original_scene, original_names):
# reset original names
root_collection = original_scene.collection
scene_objects = [o for o in root_collection.objects]
for object in scene_objects:
if object.instance_type == 'COLLECTION':
print("object name to reset", object.name)
if object.name.endswith("____bak"):
print("reseting")
object.name = object.name.replace("____bak", "")
# remove empties (only needed when we go via ops ????)
root_collection = temp_scene.collection
scene_objects = [o for o in root_collection.objects]
for object in scene_objects:
if object.type == 'EMPTY':
bpy.data.objects.remove(object, do_unlink=True)
bpy.data.scenes.remove(temp_scene)
# returns the list of the collections in use for a given scene
def get_used_collections(scene):
root_collection = scene.collection
scene_objects = [o for o in root_collection.objects]
collection_names = set()
used_collections = []
for object in scene_objects:
print("object ", object)
if object.instance_type == 'COLLECTION':
print("THIS OBJECT IS A COLLECTION")
# print("instance_type" ,object.instance_type)
collection_name = object.instance_collection.name
print("instance collection", object.instance_collection.name)
#object.instance_collection.users_scene
# del object['blueprint']
# object['BlueprintName'] = '"'+collection_name+'"'
if not collection_name in collection_names:
collection_names.add(collection_name)
used_collections.append(object.instance_collection)
print("scene objects", scene_objects)
return (collection_names, used_collections)
def generate_gltf_export_preferences(addon_prefs):
# default values
gltf_export_preferences = dict(
export_format= 'GLB', #'GLB', 'GLTF_SEPARATE', 'GLTF_EMBEDDED'
check_existing=False,
use_selection=False,
use_visible=True, # Export visible and hidden objects. See Object/Batch Export to skip.
use_renderable=False,
use_active_collection= False,
use_active_collection_with_nested=False,
use_active_scene = False,
export_texcoords=True,
export_normals=True,
# here add draco settings
export_draco_mesh_compression_enable = False,
export_tangents=False,
#export_materials
export_colors=True,
export_attributes=True,
#use_mesh_edges
#use_mesh_vertices
export_cameras=True,
export_extras=True, # For custom exported properties.
export_lights=True,
export_yup=True,
export_skins=True,
export_morph=False,
export_apply=False,
export_animations=False
)
for key in addon_prefs.__annotations__.keys():
if str(key) not in AutoExportGltfPreferenceNames:
print("overriding setting", key, "value", getattr(addon_prefs,key))
gltf_export_preferences[key] = getattr(addon_prefs,key)
return gltf_export_preferences
######################################################
#### Export logic #####
def export_used_collections(scene, folder_path, addon_prefs, gltf_export_preferences):
(collection_names, used_collections) = get_used_collections(scene)
library_scene = getattr(addon_prefs, "export_library_scene_name")
print("used collection names", collection_names, used_collections)
# set active scene to be the library scene (hack for now)
bpy.context.window.scene = bpy.data.scenes[library_scene]
# save current active collection
active_collection = bpy.context.view_layer.active_layer_collection
for collection_name in list(collection_names):
print("exporting collection", collection_name)
layer_collection = bpy.context.view_layer.layer_collection
layerColl = recurLayerCollection(layer_collection, collection_name)
# set active collection to the collection
bpy.context.view_layer.active_layer_collection = layerColl
gltf_output_path = os.path.join(folder_path, collection_name)
export_settings = { **gltf_export_preferences, 'use_active_scene': True, 'use_active_collection': True} #'use_visible': False,
export_gltf(gltf_output_path, export_settings)
# reset active collection to the one we save before
bpy.context.view_layer.active_layer_collection = active_collection
def export_main(scene, folder_path, addon_prefs):
output_name = getattr(addon_prefs,"export_main_output_name")
gltf_export_preferences = generate_gltf_export_preferences(addon_prefs)
print("exporting to", folder_path, output_name)
export_blueprints = getattr(addon_prefs,"export_blueprints")
export_blueprints_path = os.path.join(folder_path, getattr(addon_prefs,"export_blueprints_path")) if getattr(addon_prefs,"export_blueprints_path") != '' else folder_path
# backup current active scene
old_current_scene = bpy.context.scene
# backup current selections
old_selections = bpy.context.selected_objects
if export_blueprints :
print("-----EXPORTING BLUEPRINTS----")
print("LIBRARY EXPORT", export_blueprints_path )
try:
export_used_collections(scene, export_blueprints_path, addon_prefs, gltf_export_preferences)
except Exception as error:
print("failed to export collections to gltf: ", error)
(hollow_scene, object_names) = generate_hollow_scene(scene)
#except Exception:
# print("failed to create hollow scene")
# set active scene to be the given scene
bpy.context.window.scene = hollow_scene
gltf_output_path = os.path.join(folder_path, output_name)
export_settings = { **gltf_export_preferences,
'use_active_scene': True,
'use_active_collection':True,
'use_active_collection_with_nested':True,
'use_visible': False,
'use_renderable': False,
'export_apply':True
}
export_gltf(gltf_output_path, export_settings)
if export_blueprints :
clear_hollow_scene(hollow_scene, scene, object_names)
# reset current scene from backup
bpy.context.window.scene = old_current_scene
# reset selections
for obj in old_selections:
obj.select_set(True)
"""Main function"""
def auto_export():
file_path = bpy.data.filepath
# Get the folder
folder_path = os.path.dirname(file_path)
# get the preferences for our addon
addon_prefs = bpy.context.preferences.addons[__name__].preferences
print("last changed", bpy.context.scene.changedScene)
# optimised variation
last_changed = bpy.context.scene.changedScene
export_main_scene_name = getattr(addon_prefs,"export_main_scene_name")
export_on_library_changes = getattr(addon_prefs,"export_on_library_changes")
export_library_scene_name = getattr(addon_prefs,"export_library_scene_name")
# export the main game world
game_scene = bpy.data.scenes[export_main_scene_name]
# most recent change was in the main scene (game world/ level)
if last_changed == export_main_scene_name:
print("game world changed, exporting game gltf only")
export_main(game_scene, folder_path, addon_prefs)
# if the library has changed, so will likely the game world that uses the library assets
if last_changed == export_library_scene_name and export_library_scene_name != "" and export_on_library_changes:
print("library changed")
export_main(game_scene, folder_path, addon_prefs)
######################################################
## ui logic & co
AutoExportGltfPreferenceNames = [
'auto_export',
'export_main_scene_name',
'export_main_output_name',
'export_on_library_changes',
'export_library_scene_name',
'export_blueprints',
'export_blueprints_path'
]
class AutoExportGltfAddonPreferences(AddonPreferences):
# this must match the add-on name, use '__package__'
# when defining this in a submodule of a python package.
bl_idname = __name__
auto_export: BoolProperty(
name='Auto export',
description='Automatically export to gltf on save',
default=True
)
export_main_scene_name: StringProperty(
name='Main scene',
description='The name of the main scene/level/world to auto export',
default='Scene'
)
export_main_output_name: StringProperty(
name='Glb output name',
description='The glb output name for the main scene to auto export',
default='world'
)
export_on_library_changes: BoolProperty(
name='Export on library changes',
description='Export main scene on library changes',
default=False
)
export_library_scene_name: StringProperty(
name='Library scene',
description='The name of the library scene to auto export',
default='Library'
)
# blueprint settings
export_blueprints: BoolProperty(
name='Export Blueprints',
description='Replaces collection instances with an Empty with a BlueprintName custom property',
default=False
)
export_blueprints_path: StringProperty(
name='Blueprints path',
description='path to export the blueprints to (relative to this Blend file)',
default=''
)
#####
export_format: EnumProperty(
name='Format',
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)',
'Exports a single file, with all data packed in JSON. '
'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')),
description=(
'Output format and embedding options. Binary is most efficient, '
'but JSON (embedded or separate) may be easier to edit later'
),
default='GLB'
)
export_copyright: StringProperty(
name='Copyright',
description='Legal rights and conditions for the model',
default=''
)
export_image_format: EnumProperty(
name='Images',
items=(('AUTO', 'Automatic',
'Save PNGs as PNGs and JPEGs as JPEGs. '
'If neither one, use PNG'),
('JPEG', 'JPEG Format (.jpg)',
'Save images as JPEGs. (Images that need alpha are saved as PNGs though.) '
'Be aware of a possible loss in quality'),
('NONE', 'None',
'Don\'t export images'),
),
description=(
'Output format for images. PNG is lossless and generally preferred, but JPEG might be preferable for web '
'applications due to the smaller file size. Alternatively they can be omitted if they are not needed'
),
default='AUTO'
)
export_texture_dir: StringProperty(
name='Textures',
description='Folder to place texture files in. Relative to the .gltf file',
default='',
)
"""
export_jpeg_quality: IntProperty(
name='JPEG quality',
description='Quality of JPEG export',
default=75,
min=0,
max=100
)
"""
export_keep_originals: BoolProperty(
name='Keep original',
description=('Keep original textures files if possible. '
'WARNING: if you use more than one texture, '
'where pbr standard requires only one, only one texture will be used. '
'This can lead to unexpected results'
),
default=False,
)
export_texcoords: BoolProperty(
name='UVs',
description='Export UVs (texture coordinates) with meshes',
default=True
)
export_normals: BoolProperty(
name='Normals',
description='Export vertex normals with meshes',
default=True
)
export_draco_mesh_compression_enable: BoolProperty(
name='Draco mesh compression',
description='Compress mesh using Draco',
default=False
)
export_draco_mesh_compression_level: IntProperty(
name='Compression level',
description='Compression level (0 = most speed, 6 = most compression, higher values currently not supported)',
default=6,
min=0,
max=10
)
export_draco_position_quantization: IntProperty(
name='Position quantization bits',
description='Quantization bits for position values (0 = no quantization)',
default=14,
min=0,
max=30
)
export_draco_normal_quantization: IntProperty(
name='Normal quantization bits',
description='Quantization bits for normal values (0 = no quantization)',
default=10,
min=0,
max=30
)
export_draco_texcoord_quantization: IntProperty(
name='Texcoord quantization bits',
description='Quantization bits for texture coordinate values (0 = no quantization)',
default=12,
min=0,
max=30
)
export_draco_color_quantization: IntProperty(
name='Color quantization bits',
description='Quantization bits for color values (0 = no quantization)',
default=10,
min=0,
max=30
)
export_draco_generic_quantization: IntProperty(
name='Generic quantization bits',
description='Quantization bits for generic coordinate values like weights or joints (0 = no quantization)',
default=12,
min=0,
max=30
)
export_tangents: BoolProperty(
name='Tangents',
description='Export vertex tangents with meshes',
default=False
)
export_materials: EnumProperty(
name='Materials',
items=(('EXPORT', 'Export',
'Export all materials used by included objects'),
('PLACEHOLDER', 'Placeholder',
'Do not export materials, but write multiple primitive groups per mesh, keeping material slot information'),
('NONE', 'No export',
'Do not export materials, and combine mesh primitive groups, losing material slot information')),
description='Export materials',
default='EXPORT'
)
export_original_specular: BoolProperty(
name='Export original PBR Specular',
description=(
'Export original glTF PBR Specular, instead of Blender Principled Shader Specular'
),
default=False,
)
export_colors: BoolProperty(
name='Vertex Colors',
description='Export vertex colors with meshes',
default=True
)
export_attributes: BoolProperty(
name='Attributes',
description='Export Attributes (when starting with underscore)',
default=False
)
use_mesh_edges: BoolProperty(
name='Loose Edges',
description=(
'Export loose edges as lines, using the material from the first material slot'
),
default=False,
)
use_mesh_vertices: BoolProperty(
name='Loose Points',
description=(
'Export loose points as glTF points, using the material from the first material slot'
),
default=False,
)
export_cameras: BoolProperty(
name='Cameras',
description='Export cameras',
default=True
)
use_selection: BoolProperty(
name='Selected Objects',
description='Export selected objects only',
default=False
)
use_visible: BoolProperty(
name='Visible Objects',
description='Export visible objects only',
default=True
)
use_renderable: BoolProperty(
name='Renderable Objects',
description='Export renderable objects only',
default=False
)
export_apply: BoolProperty(
name='Export Apply Modifiers',
description='Apply modifiers (excluding Armatures) to mesh objects -'
'WARNING: prevents exporting shape keys',
default=True
)
export_yup: BoolProperty(
name='+Y Up',
description='Export using glTF convention, +Y up',
default=True
)
use_visible: BoolProperty(
name='Visible Objects',
description='Export visible objects only',
default=False
)
use_renderable: BoolProperty(
name='Renderable Objects',
description='Export renderable objects only',
default=False
)
export_extras: BoolProperty(
name='Custom Properties',
description='Export custom properties as glTF extras',
default=True
)
export_animations: BoolProperty(
name='Animations',
description='Exports active actions and NLA tracks as glTF animations',
default=False
)
class AutoExportGLTF(Operator, ExportHelper):
"""test"""
bl_idname = "export_scenes.auto_gltf"
bl_label = "Apply settings"
bl_options = {'PRESET', 'UNDO'}
# ExportHelper mixin class uses this
filename_ext = ''
filter_glob: StringProperty(
default='*.glb;*.gltf',
options={'HIDDEN'}
)
"""
auto_export: BoolProperty(
name='Auto export',
description='Automatically export to gltf on save',
default=True
)
export_main_scene_name: StringProperty(
name='Main scene',
description='The name of the main scene/level/world to auto export',
default='Scene'
)
export_main_output_name: StringProperty(
name='Glb output name',
description='The glb output name for the main scene to auto export',
default='world'
)
export_on_library_changes: BoolProperty(
name='Export on library changes',
description='Export main scene on library changes',
default=False
)
export_library_scene_name: StringProperty(
name='Library scene',
description='The name of the library scene to auto export',
default='Library'
)
# blueprint settings
export_blueprints: BoolProperty(
name='Export Blueprints',
description='Replaces collection instances with an Empty with a BlueprintName custom property',
default=False
)
export_blueprints_path: StringProperty(
name='Blueprints path',
description='path to export the blueprints to (relative to this Blend file)',
default=''
)"""
def draw(self, context):
pass
def execute(self, context):
preferences = context.preferences
return {'FINISHED'}
class GLTF_PT_auto_export_main(bpy.types.Panel):
bl_space_type = 'FILE_BROWSER'
bl_region_type = 'TOOL_PROPS'
bl_label = ""
bl_parent_id = "FILE_PT_operator"
bl_options = {'HIDE_HEADER'}
@classmethod
def poll(cls, context):
sfile = context.space_data
operator = sfile.active_operator
return operator.bl_idname == "EXPORT_SCENES_OT_auto_gltf"
def draw(self, context):
layout = self.layout
layout.use_property_split = True
layout.use_property_decorate = False # No animation.
sfile = context.space_data
class GLTF_PT_auto_export_root(bpy.types.Panel):
bl_space_type = 'FILE_BROWSER'
bl_region_type = 'TOOL_PROPS'
bl_label = "Auto export"
bl_parent_id = "GLTF_PT_auto_export_main"
#bl_options = {'DEFAULT_CLOSED'}
@classmethod
def poll(cls, context):
sfile = context.space_data
operator = sfile.active_operator
return operator.bl_idname == "EXPORT_SCENES_OT_auto_gltf"
def draw_header(self, context):
sfile = context.space_data
operator = sfile.active_operator
self.layout.prop(operator, "auto_export", text="")
def draw(self, context):
layout = self.layout
layout.use_property_split = True
layout.use_property_decorate = False # No animation.
sfile = context.space_data
#operator = sfile.active_operator
operator = bpy.context.preferences.addons[__name__].preferences
layout.active = operator.auto_export
layout.prop(operator, "export_main_scene_name")
layout.prop(operator, "export_library_scene_name")
layout.prop(operator, "export_main_output_name")
layout.prop(operator, "export_on_library_changes")
class GLTF_PT_auto_export_blueprints(bpy.types.Panel):
bl_space_type = 'FILE_BROWSER'
bl_region_type = 'TOOL_PROPS'
bl_label = "Blueprints"
bl_parent_id = "GLTF_PT_auto_export_root"
@classmethod
def poll(cls, context):
sfile = context.space_data
operator = sfile.active_operator
return operator.bl_idname == "EXPORT_SCENES_OT_auto_gltf" #"EXPORT_SCENE_OT_gltf"
def draw(self, context):
layout = self.layout
layout.use_property_split = True
layout.use_property_decorate = False # No animation.
sfile = context.space_data
operator = sfile.active_operator
addon_prefs = bpy.context.preferences.addons[__name__].preferences
layout.prop(addon_prefs, "export_blueprints")
layout.prop(addon_prefs, "export_blueprints_path")
class GLTF_PT_auto_export_gltf(bpy.types.Panel):
bl_space_type = 'FILE_BROWSER'
bl_region_type = 'TOOL_PROPS'
bl_label = "Gltf"
bl_parent_id = "GLTF_PT_auto_export_main"
bl_options = {'DEFAULT_CLOSED'}
@classmethod
def poll(cls, context):
sfile = context.space_data
operator = sfile.active_operator
return operator.bl_idname == "EXPORT_SCENES_OT_auto_gltf" #"EXPORT_SCENE_OT_gltf"
def draw(self, context):
preferences = context.preferences
addon_prefs = preferences.addons[__name__].preferences
layout = self.layout
#preferences = context.preferences
#print("ADDON PREFERENCES ", list(preferences.addons.keys()))
#print("standard blender gltf prefs", list(preferences.addons["io_scene_gltf2"].preferences.keys()))
# we get the addon preferences from the standard gltf exporter & use those :
addon_prefs_gltf = preferences.addons["io_scene_gltf2"].preferences
#addon_prefs = preferences.addons[__name__].preferences
# print("KEYS", dir(addon_prefs))
#print("BLAS", addon_prefs.__annotations__)
#print(addon_prefs.__dict__)
for key in addon_prefs.__annotations__.keys():
if key not in AutoExportGltfPreferenceNames:
layout.prop(addon_prefs, key)
#for key in addon_prefs_gltf.__annotations__.keys():
# layout.prop(addon_prefs_gltf, key)
def menu_func_import(self, context):
self.layout.operator(AutoExportGLTF.bl_idname, text="glTF auto Export (.glb/gltf)")
classes = [
AutoExportGLTF,
AutoExportGltfAddonPreferences,
GLTF_PT_auto_export_main,
GLTF_PT_auto_export_root,
GLTF_PT_auto_export_blueprints,
GLTF_PT_auto_export_gltf
]
def register():
for cls in classes:
bpy.utils.register_class(cls)
# setup handlers for updates & saving
bpy.app.handlers.depsgraph_update_post.append(deps_update_handler)
bpy.app.handlers.save_post.append(save_handler)
bpy.types.Scene.changedScene = bpy.props.StringProperty(get=get_changedScene, set=set_changedScene)
#bpy.types.Scene.changedObjects = bpy.props.CollectionProperty(get=get_changedObjects, set=set_changedObjects)
# add our addon to the toolbar
bpy.types.TOPBAR_MT_file_export.append(menu_func_import)
def unregister():
for cls in classes:
bpy.utils.unregister_class(cls)
bpy.types.TOPBAR_MT_file_export.remove(menu_func_import)
# remove handlers & co
bpy.app.handlers.depsgraph_update_post.remove(deps_update_handler)
bpy.app.handlers.save_post.remove(save_handler)
del bpy.types.Scene.changedScene
#del bpy.types.Scene.changedObjects
if __name__ == "__main__":
register()

View File

@ -1,5 +1,5 @@
bl_info = { bl_info = {
"name": "blender_auto_export_gltf", "name": "gltf_auto_export_gltf",
"author": "kaosigh", "author": "kaosigh",
"version": (0, 1), "version": (0, 1),
"blender": (3, 4, 0), "blender": (3, 4, 0),
@ -24,7 +24,6 @@ from bpy.props import (BoolProperty,
) )
#see here for original gltf exporter infos https://github.com/KhronosGroup/glTF-Blender-IO/blob/main/addons/io_scene_gltf2/__init__.py #see here for original gltf exporter infos https://github.com/KhronosGroup/glTF-Blender-IO/blob/main/addons/io_scene_gltf2/__init__.py
@persistent @persistent
def deps_update_handler(scene): def deps_update_handler(scene):
print("-------------") print("-------------")
@ -109,7 +108,6 @@ def export_library_split(scene, folder_path, gltf_export_preferences):
# reset current scene from backup # reset current scene from backup
bpy.context.window.scene = old_current_scene bpy.context.window.scene = old_current_scene
def debug_test(scene): def debug_test(scene):
root_collection = scene.collection root_collection = scene.collection
collections_lookup = get_collection_hierarchy(root_collection, 1) collections_lookup = get_collection_hierarchy(root_collection, 1)
@ -238,18 +236,215 @@ def export_library_merged(scene, folder_path, gltf_export_preferences):
# reset current scene from backup # reset current scene from backup
bpy.context.window.scene = old_current_scene bpy.context.window.scene = old_current_scene
def export_main(scene, folder_path, gltf_export_preferences, output_name):
# the active collection is a View Layer concept, so you actually have to find the active LayerCollection
# which must be done recursively
def find_layer_collection_recursive(find, col):
for c in col.children:
if c.collection == find:
return c
return None
# Makes an empty, at location, stores it in existing collection, from https://blender.stackexchange.com/questions/51290/how-to-add-empty-object-not-using-bpy-ops
def make_empty(name, location, coll_name): #string, vector, string of existing coll
empty_obj = bpy.data.objects.new( "empty", None, )
empty_obj.name = name
empty_obj.empty_display_size = 1
bpy.data.collections[coll_name].objects.link(empty_obj)
empty_obj.location = location
return empty_obj
def make_empty2(name, location, collection):
object_data = None #bpy.data.meshes.new("NewMesh") #None
empty_obj = bpy.data.objects.new( name, object_data )
empty_obj.name = name
empty_obj.location = location
empty_obj.empty_display_size = 2
empty_obj.empty_display_type = 'PLAIN_AXES'
collection.objects.link( empty_obj )
return empty_obj
def make_empty3(name, location, collection):
original_active_object = bpy.context.active_object
bpy.ops.object.empty_add(type='PLAIN_AXES', location=location)
empty_obj = bpy.context.active_object
empty_obj.name = name
collection.objects.link( empty_obj )
bpy.context.view_layer.objects.active = original_active_object
return empty_obj
# generate a copy of a scene that replaces collection instances with empties
# FIXME: will not preserve original names
# 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):
root_collection = scene.collection
temp_scene = bpy.data.scenes.new(name="temp_scene")
copy_root_collection = temp_scene.collection
scene_objects = [o for o in root_collection.objects]
found = find_layer_collection_recursive(copy_root_collection, bpy.context.view_layer.layer_collection)
if found:
print("FOUND COLLECTION")
# 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 = []
for object in scene_objects:
if object.instance_type == 'COLLECTION':
collection_name = object.instance_collection.name
#original_names[object.name] = object.name# + "____bak"
#print("custom properties", object, object.keys(), object.items())
#for k, e in object.items():
# print("custom properties ", k, e)
print("object location", object.location)
original_name = object.name
original_names.append(original_name)
object.name = original_name + "____bak"
empty_obj = make_empty3(original_name, object.location, copy_root_collection)
"""we inject the collection/blueprint name, as a component called 'BlueprintName', but we only do this in the empty, not the original object"""
empty_obj['BlueprintName'] = '"'+collection_name+'"'
empty_obj['SpawnHere'] = ''
for k, v in object.items():
empty_obj[k] = v
else:
copy_root_collection.objects.link(object)
# bpy.data.scenes.remove(temp_scene)
# objs = bpy.data.objects
#objs.remove(objs["Cube"], do_unlink=True)
return (temp_scene, original_names)
def clear_hollow_scene(temp_scene, original_scene, original_names):
# reset original names
root_collection = original_scene.collection
scene_objects = [o for o in root_collection.objects]
for object in scene_objects:
if object.instance_type == 'COLLECTION':
print("object name to reset", object.name)
if object.name.endswith("____bak"):
print("reseting")
object.name = object.name.replace("____bak", "")
# remove empties (only needed when we go via ops ????)
root_collection = temp_scene.collection
scene_objects = [o for o in root_collection.objects]
for object in scene_objects:
if object.type == 'EMPTY':
bpy.data.objects.remove(object, do_unlink=True)
bpy.data.scenes.remove(temp_scene)
#Recursivly transverse layer_collection for a particular name
def recurLayerCollection(layerColl, collName):
found = None
if (layerColl.name == collName):
return layerColl
for layer in layerColl.children:
found = recurLayerCollection(layer, collName)
if found:
return found
def get_used_collections(scene):
root_collection = scene.collection
scene_objects = [o for o in root_collection.objects]
collection_names = set()
used_collections = []
for object in scene_objects:
print("object ", object)
if object.instance_type == 'COLLECTION':
print("THIS OBJECT IS A COLLECTION")
# print("instance_type" ,object.instance_type)
collection_name = object.instance_collection.name
print("instance collection", object.instance_collection.name)
#object.instance_collection.users_scene
# del object['blueprint']
# object['BlueprintName'] = '"'+collection_name+'"'
if not collection_name in collection_names:
collection_names.add(collection_name)
used_collections.append(object.instance_collection)
print("scene objects", scene_objects)
return (collection_names, used_collections)
def export_used_collections(scene, folder_path, gltf_export_preferences):
(collection_names, used_collections) = get_used_collections(scene)
print("used collection names", collection_names, used_collections)
# set active scene to be the library scene (hack for now)
bpy.context.window.scene = bpy.data.scenes["library"]
# save current active collection
active_collection = bpy.context.view_layer.active_layer_collection
for collection_name in list(collection_names):
print("exporting collection", collection_name)
layer_collection = bpy.context.view_layer.layer_collection
layerColl = recurLayerCollection(layer_collection, collection_name)
# set active collection to the collection
bpy.context.view_layer.active_layer_collection = layerColl
print("layercoll", layerColl)
gltf_output_path = os.path.join(folder_path, collection_name)
export_settings = { **gltf_export_preferences, 'use_active_scene': True, 'use_active_collection': True} #'use_visible': False,
export_gltf(gltf_output_path, export_settings)
# reset active collection to the one we save before
bpy.context.view_layer.active_layer_collection = active_collection
def export_main(scene, folder_path, gltf_export_preferences, output_name, addon_prefs):
print("exporting to", folder_path, output_name) print("exporting to", folder_path, output_name)
export_blueprints = getattr(addon_prefs,"export_blueprints")
export_blueprints_path = os.path.join(folder_path, getattr(addon_prefs,"export_blueprints_path")) if getattr(addon_prefs,"export_blueprints_path") != '' else folder_path
# backup current active scene # backup current active scene
old_current_scene = bpy.context.scene old_current_scene = bpy.context.scene
# set active scene to be the given scene
bpy.context.window.scene = scene if export_blueprints :
print("-----EXPORTING BLUEPRINTS----")
print("LIBRARY EXPORT", export_blueprints_path )
try:
#gltf_output_path = os.path.join(folder_path, "library")
#export_gltf(gltf_output_path, export_settings)
export_used_collections(scene, export_blueprints_path, gltf_export_preferences)
except Exception:
print("failed to export collections to gltf")
(hollow_scene, object_names) = generate_hollow_scene(scene)
#except Exception:
# print("failed to create hollow scene")
# set active scene to be the given scene
bpy.context.window.scene = hollow_scene
gltf_output_path = os.path.join(folder_path, output_name) gltf_output_path = os.path.join(folder_path, output_name)
export_settings = { **gltf_export_preferences, 'use_active_scene': True} export_settings = { **gltf_export_preferences,
'use_active_scene': True,
'use_active_collection':True,
'use_active_collection_with_nested':True,
'use_visible': False,
'use_renderable': False,
'export_apply':True
}
export_gltf(gltf_output_path, export_settings) export_gltf(gltf_output_path, export_settings)
if export_blueprints :
clear_hollow_scene(hollow_scene, scene, object_names)
# reset current scene from backup # reset current scene from backup
bpy.context.window.scene = old_current_scene bpy.context.window.scene = old_current_scene
@ -320,11 +515,11 @@ def auto_export():
) )
for key in addon_prefs.__annotations__.keys(): for key in addon_prefs.__annotations__.keys():
if key is not "export_on_library_changes" and key is not "export_main_scene_name" and key is not "export_main_output_name" and key is not "export_library_scene_name": #FIXME: ugh, cleanup if key != "export_on_library_changes" and key != "export_main_scene_name" and key != "export_main_output_name" and key != "export_library_scene_name" and key != "export_blueprints" and key != "export_blueprints_path": #FIXME: ugh, cleanup
gltf_export_preferences[key] = getattr(addon_prefs,key) gltf_export_preferences[key] = getattr(addon_prefs,key)
print("overriding setting", key, "value", getattr(addon_prefs,key)) print("overriding setting", key, "value", getattr(addon_prefs,key))
# testing (we want an in-memory scene, not one that is visible in the ui) # (we want an in-memory scene, not one that is visible in the ui)
#invisible_scene = bpy.types.Scene("foo") #invisible_scene = bpy.types.Scene("foo")
@ -338,10 +533,9 @@ def auto_export():
export_main_scene_name = getattr(addon_prefs,"export_main_scene_name") export_main_scene_name = getattr(addon_prefs,"export_main_scene_name")
export_main_output_name = getattr(addon_prefs,"export_main_output_name") export_main_output_name = getattr(addon_prefs,"export_main_output_name")
export_on_library_changes = getattr(addon_prefs,"export_on_library_changes") export_on_library_changes = getattr(addon_prefs,"export_on_library_changes")
export_library_scene_name = getattr(addon_prefs,"export_library_scene_name") export_library_scene_name = getattr(addon_prefs,"export_library_scene_name")
print("exporting ??", export_on_library_changes, export_main_scene_name, export_main_output_name) # print("ADD ON PARAMS FOR EXPORT ??", export_on_library_changes, export_main_scene_name, export_main_output_name, export_blueprints)
print("last changed", bpy.context.scene.changedScene) print("last changed", bpy.context.scene.changedScene)
# optimised variation # optimised variation
last_changed = bpy.context.scene.changedScene #get_changedScene() last_changed = bpy.context.scene.changedScene #get_changedScene()
@ -350,8 +544,8 @@ def auto_export():
game_scene = bpy.data.scenes[export_main_scene_name] game_scene = bpy.data.scenes[export_main_scene_name]
print("game world changed, exporting game gltf only") print("game world changed, exporting game gltf only")
export_main(game_scene, folder_path, gltf_export_preferences, export_main_output_name) export_main(game_scene, folder_path, gltf_export_preferences, export_main_output_name, addon_prefs)
if last_changed == export_library_scene_name and export_library_scene_name is not "" : # if the library has changed, so will likely the game world that uses the library assets if last_changed == export_library_scene_name and export_library_scene_name != "" : # if the library has changed, so will likely the game world that uses the library assets
print("library changed, exporting both game & library gltf") print("library changed, exporting both game & library gltf")
library_scene = bpy.data.scenes[export_library_scene_name] library_scene = bpy.data.scenes[export_library_scene_name]
@ -360,7 +554,7 @@ def auto_export():
# export the main game world # export the main game world
if export_on_library_changes: if export_on_library_changes:
game_scene = bpy.data.scenes[export_main_scene_name] game_scene = bpy.data.scenes[export_main_scene_name]
export_main(game_scene, folder_path, gltf_export_preferences, export_main_output_name) export_main(game_scene, folder_path, gltf_export_preferences, export_main_output_name, addon_prefs)
return {'FINISHED'} return {'FINISHED'}
@ -384,7 +578,14 @@ class AutoExportGltfAddonPreferences(AddonPreferences):
# this must match the add-on name, use '__package__' # this must match the add-on name, use '__package__'
# when defining this in a submodule of a python package. # when defining this in a submodule of a python package.
bl_idname = __name__ bl_idname = __name__
ui_tab: EnumProperty(
items=(('GENERAL', "General", "General settings"),
('MESHES', "Meshes", "Mesh settings"),
('OBJECTS', "Objects", "Object settings"),
('ANIMATION', "Animation", "Animation settings")),
name="ui_tab",
description="Export setting categories",
)
export_format: EnumProperty( export_format: EnumProperty(
name='Format', name='Format',
items=(('GLB', 'glTF Binary (.glb)', items=(('GLB', 'glTF Binary (.glb)',
@ -425,6 +626,19 @@ class AutoExportGltfAddonPreferences(AddonPreferences):
default='' default=''
) )
# blueprint settings
export_blueprints: BoolProperty(
name='Export Blueprints',
description='Replaces collection instances with an Empty with a BlueprintName custom property',
default=False
)
export_blueprints_path: StringProperty(
name='Export Blueprints path',
description='path to export the blueprints to',
default=''
)
##### #####
export_copyright: StringProperty( export_copyright: StringProperty(
@ -663,7 +877,7 @@ class AutoExportGltfAddonPreferences(AddonPreferences):
class TEST_AUTO_OT_gltf(Operator, ExportHelper): class AutoExportGLTF2(Operator, ExportHelper):
"""test""" """test"""
bl_idname = "export_scenes.auto_gltf" bl_idname = "export_scenes.auto_gltf"
bl_label = "Apply settings" bl_label = "Apply settings"
@ -681,6 +895,7 @@ class TEST_AUTO_OT_gltf(Operator, ExportHelper):
# to the class instance from the operator setting before calling. # to the class instance from the operator setting before calling.
def draw(self, context): def draw(self, context):
pass
layout = self.layout layout = self.layout
preferences = context.preferences preferences = context.preferences
addon_prefs = preferences.addons[__name__].preferences addon_prefs = preferences.addons[__name__].preferences
@ -716,9 +931,184 @@ class TEST_AUTO_OT_gltf(Operator, ExportHelper):
# Only needed if you want to add into a dynamic menu # Only needed if you want to add into a dynamic menu
def menu_func_import(self, context): def menu_func_import(self, context):
self.layout.operator(TEST_AUTO_OT_gltf.bl_idname, text="glTF auto Export (.glb/gltf)") self.layout.operator(AutoExportGLTF2.bl_idname, text="glTF auto Export (.glb/gltf)")
classes = [TEST_AUTO_OT_gltf, AutoExportGltfAddonPreferences]
class GLTF_PT_export_main(bpy.types.Panel):
bl_space_type = 'FILE_BROWSER'
bl_region_type = 'TOOL_PROPS'
bl_label = ""
bl_parent_id = "FILE_PT_operator"
bl_options = {'HIDE_HEADER'}
@classmethod
def poll(cls, context):
sfile = context.space_data
operator = sfile.active_operator
return operator.bl_idname == "EXPORT_SCENE_OT_gltf"
def draw(self, context):
layout = self.layout
layout.use_property_split = True
layout.use_property_decorate = False # No animation.
sfile = context.space_data
operator = sfile.active_operator
layout.prop(operator, 'export_format')
if operator.export_format == 'GLTF_SEPARATE':
layout.prop(operator, 'export_keep_originals')
if operator.export_keep_originals is False:
layout.prop(operator, 'export_texture_dir', icon='FILE_FOLDER')
layout.prop(operator, 'export_copyright')
layout.prop(operator, 'will_save_settings')
class GLTF_PT_export_transform(bpy.types.Panel):
bl_space_type = 'FILE_BROWSER'
bl_region_type = 'TOOL_PROPS'
bl_label = "Transform"
bl_parent_id = "FILE_PT_operator"
bl_options = {'DEFAULT_CLOSED'}
@classmethod
def poll(cls, context):
sfile = context.space_data
operator = sfile.active_operator
return operator.bl_idname == "EXPORT_SCENE_OT_gltf"
def draw(self, context):
layout = self.layout
layout.use_property_split = True
layout.use_property_decorate = False # No animation.
sfile = context.space_data
operator = sfile.active_operator
layout.prop(operator, 'export_yup')
class GLTF_PT_export_include(bpy.types.Panel):
bl_space_type = 'FILE_BROWSER'
bl_region_type = 'TOOL_PROPS'
bl_label = "Include"
bl_parent_id = "FILE_PT_operator"
bl_options = {'DEFAULT_CLOSED'}
@classmethod
def poll(cls, context):
sfile = context.space_data
operator = sfile.active_operator
return operator.bl_idname == "EXPORT_SCENE_OT_gltf"
def draw(self, context):
layout = self.layout
layout.use_property_split = True
layout.use_property_decorate = False # No animation.
sfile = context.space_data
operator = sfile.active_operator
col = layout.column(heading = "Limit to", align = True)
col.prop(operator, 'use_selection')
col.prop(operator, 'use_visible')
col.prop(operator, 'use_renderable')
col.prop(operator, 'use_active_collection')
if operator.use_active_collection:
col.prop(operator, 'use_active_collection_with_nested')
col.prop(operator, 'use_active_scene')
col = layout.column(heading = "Data", align = True)
col.prop(operator, 'export_extras')
col.prop(operator, 'export_cameras')
col.prop(operator, 'export_lights')
class GLTF_PT_export_animation(bpy.types.Panel):
bl_space_type = 'FILE_BROWSER'
bl_region_type = 'TOOL_PROPS'
bl_label = "Animation"
bl_parent_id = "FILE_PT_operator"
bl_options = {'DEFAULT_CLOSED'}
@classmethod
def poll(cls, context):
sfile = context.space_data
operator = sfile.active_operator
return operator.bl_idname == "EXPORT_SCENE_OT_gltf"
def draw_header(self, context):
sfile = context.space_data
operator = sfile.active_operator
self.layout.prop(operator, "export_animations", text="")
def draw(self, context):
layout = self.layout
layout.use_property_split = True
layout.use_property_decorate = False # No animation.
sfile = context.space_data
operator = sfile.active_operator
layout.active = operator.export_animations
layout.prop(operator, 'export_animation_mode')
if operator.export_animation_mode == "ACTIVE_ACTIONS":
layout.prop(operator, 'export_nla_strips_merged_animation_name')
row = layout.row()
row.active = operator.export_force_sampling and operator.export_animation_mode in ['ACTIONS', 'ACTIVE_ACTIONS']
row.prop(operator, 'export_bake_animation')
if operator.export_animation_mode == "SCENE":
layout.prop(operator, 'export_anim_scene_split_object')
class GLTF_PT_export_animation_notes(bpy.types.Panel):
bl_space_type = 'FILE_BROWSER'
bl_region_type = 'TOOL_PROPS'
bl_label = "Notes"
bl_parent_id = "GLTF_PT_export_animation"
bl_options = {'DEFAULT_CLOSED'}
@classmethod
def poll(cls, context):
sfile = context.space_data
operator = sfile.active_operator
return operator.bl_idname == "EXPORT_SCENE_OT_gltf" and \
operator.export_animation_mode in ["NLA_TRACKS", "SCENE"]
def draw(self, context):
operator = context.space_data.active_operator
layout = self.layout
if operator.export_animation_mode == "SCENE":
layout.label(text="Scene mode uses full bake mode:")
layout.label(text="- sampling is active")
layout.label(text="- baking all objects is active")
layout.label(text="- Using scene frame range")
elif operator.export_animation_mode == "NLA_TRACKS":
layout.label(text="Track mode uses full bake mode:")
layout.label(text="- sampling is active")
layout.label(text="- baking all objects is active")
classes = [
AutoExportGLTF2,
AutoExportGltfAddonPreferences,
#
# GLTF_PT_export_main,
# GLTF_PT_export_include,
# GLTF_PT_export_transform,
# GLTF_PT_export_animation,
# GLTF_PT_export_animation_notes,
#panel1
]
def register(): def register():
for cls in classes: for cls in classes:
@ -732,7 +1122,6 @@ def register():
bpy.app.handlers.depsgraph_update_post.append(deps_update_handler) bpy.app.handlers.depsgraph_update_post.append(deps_update_handler)
bpy.app.handlers.save_post.append(save_handler) bpy.app.handlers.save_post.append(save_handler)
#bpy.types.TOPBAR_MT_file_export.append(menu_func_import)
bpy.types.Scene.changedScene = bpy.props.StringProperty(get=get_changedScene, set=set_ChangedScene) bpy.types.Scene.changedScene = bpy.props.StringProperty(get=get_changedScene, set=set_ChangedScene)