feat(Blenvy): complete overhaul and re branding to : Blenvy (#192)

- replaced the various crates with a single one
- replaced the Blender add-ons with a single one
- this is an alpha release !
- for features etc see the various docs
This commit is contained in:
Mark Moissette 2024-08-14 16:40:59 +02:00 committed by GitHub
parent 9b50d77790
commit d1b5d2627d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
636 changed files with 53835 additions and 20018 deletions

View File

@ -1,11 +1,7 @@
[workspace]
members = [
"crates/*",
"examples/common*",
"examples/bevy_gltf_components/*",
"examples/bevy_gltf_blueprints/*",
"examples/bevy_gltf_save_load/*",
"examples/bevy_registry_export/*",
"examples/*",
"testing/bevy_example/",
]
resolver = "2"

148
Migration_guide.md Normal file
View File

@ -0,0 +1,148 @@
# Blender add-ons
- gltf_auto_export and bevy_components have been replaced with a single Blenvy add-on for simplicity , I recomend reading the [documentation](./tools/blenvy/README.md)
* settings are **not** transfered from the legacy add-ons !
* first uninstall the old add-ons
* install Blenvy
* configure Blenvy (for these , see the Blenvy add-on docs)
* [upgrade your components](./tools/blenvy/README-components.md#renamingupgradingfixing-components)
## Components:
- no more need to add your components to an empty called xxx_components, you can now directly add your components to the blueprint's collection itself
- you will need to "upgrade" your components from the previous add-on, as they are stored in a completely different way
## Multiple components with the same short name
Up until now , it was not possible to have multiple components with the same name (ie foo::bar::componentA & some::other::componentA) as all the logic was based on short names,
this is not an issue anymore
## Auto export:
- the previous stripped down gltf export settings are not part of the add-on anymore, please configure them like this:
- you need to reconfigure your auto export settings , as they have changed significantly as has their storage
## All the Bevy crates have been replaced with a single one
- the new crate doesn't even really need configuring, so
- in your cargo.toml file, replace any references to the old crates (bevy_gltf_components, bevy_gltf_blueprints, bevy_registry_export, bevy_gltf_save_load etc)
with:
```toml
# Cargo.toml
[dependencies]
bevy="0.14"
blenvy = { version = "0.1.0"}
```
and set things up in your code:
```rust no_run
use bevy::prelude::*;
use blenvy::*;
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_plugins(BlenvyPlugin)
.run();
}
```
## Removed almost all setting for the crate
- the ONLY setting is **aabbs** // defaults to true
## Legacy mode has been removed
- less headaches when using the tools!
If you still want to manually specify components using Blender's custom properties you need to
## BlueprintName replaced with BlueprintInfo
- this is a very important change ! to avoid possible path clashes , the ```BlueprintInfo``` component contains
the actual path (with your **assets** folder) to the Blueprint, and a name (for convenience)
## SpawnHere renamed to SpawnBlueprint
changed the naming for more clarity & specificity
## Automatic assets loading
- no more need to preload gltf files, you can spawn a level & all its blueprint like this:
```rust no_run
commands.spawn((
BlueprintInfo::from_path("levels/World.gltf"),
HideUntilReady, // Only if you want to keep the level hidden until it is finished spawning
SpawnBlueprint, // See note above
GameWorldTag,
InAppRunning,
));
```
Blenvy will take care of loading all needed blueprints & other assets for you
## Blueprint instance events
- you can now use the ```BlueprintEvent``` to get notified of crucial blueprint instance events
* ```AssetsLoaded```
```rust no run
/// event fired when a blueprint instance has finished loading all of its assets & before it attempts spawning
AssetsLoaded {
entity: Entity,
blueprint_name: String,
blueprint_path: String,
// TODO: add assets list ?
}
```
* ```InstanceReady```
```rust no run
/// event fired when a blueprint instance has completely finished spawning, ie
/// - all its assests have been loaded
/// - all of its child blueprint instances are ready
/// - all the post processing is finished (aabb calculation, material replacements etc)
InstanceReady {
entity: Entity,
blueprint_name: String,
blueprint_path: String,
},
```
## BlueprintInstanceDisabled
you can now query for this component
## Track loading blueprint instances with the BlueprintSpawning component
- other than with events, you can also query for the ```BlueprintSpawning``` component to be sure an entity you are manipulating is finished with its blueprint instance spawning process
## Keep your currently spawning blueprint instances hidden until they are ready with the HideUntilReady component
If you want your blueprint instance to be hidden until it is ready, just add this component to the entity.
This can be particularly usefull in at least two use cases:
- when spawning levels
- when spawning bluprint instances that contain **lights** at runtime: in previous versions I have noticed some very unpleasant "flashing" effect when spawning blueprints with lights,
this component avoids that issue
## Hot reload
if you have configured your Bevy project to use hot reload you will automatically get hot reloading of levels & blueprints
## Improved animation handling
- sceneAnimations
- animationTriggers
## Completely restructured blueprint spawning process
Additionally
- you do not really need to worry about SystemSets anymore

View File

@ -4,11 +4,11 @@ The workflow goes as follows (once you got your Bevy code setup)
## Bevy side
- create & register all your components you want to be able to set from the Blender side (this is basic Bevy, no specific work needed)
- follow the instructions in the [bevy_registry_export](./crates/bevy_registry_export/) to generate a registry export
- follow the instructions in the [blenvy](./crates/blenvy/) to generate a registry export
## Component creation
Setup the Blender [bevy_components](./tools/bevy_components/README.md) add-on
Setup the Blender [Blenvy](./tools/blenvy/README.md) blender add-on
to add & edit your components visually & reliably
![bevy_components](./docs/bevy_components.png)
@ -22,7 +22,7 @@ to add & edit your components visually & reliably
- 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/)
- or much better, using [blenvy](./tools/blenvy/)
## Now use your gltf files in Bevy

148
README.md
View File

@ -1,123 +1,123 @@
[![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_workflow/blob/main/LICENSE.md)
[![License](https://img.shields.io/crates/l/blenvy)](https://github.com/kaosat-dev/Blenvy/blob/main/LICENSE.md)
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/F1F5TO32O)
# Blender_bevy_components_workflow
# BLENVY: a friendly Blender <=> Bevy workflow
![demo](./docs/blender_bevy.png)
![demo](https://github.com/kaosat-dev/Blenvy/blob/main/docs/blender_bevy.png)
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
inside Blender using Blender's objects **custom properties**.
Aka "Blender as editor for Bevy"
It enables a [Blender](https://www.blender.org/) (gltf) centric workflow for Bevy, ie defining entites & their components
inside Blender. 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 .
> [!CAUTION]
> Blenvy is currently in **Alpha 1** state so there are still quite a few bugs, missing functionality, missing docs, broken examples etc
> Please make sure you back up your Blender files before using it !
> [!CAUTION]
> Please make sure to use matching versions numbers for the Blender add-on & the rust crate !
> This is the only way to make sure everything works as intended
## Quickstart
Want to jump right in? See the [quickstart guide](https://github.com/kaosat-dev/Blenvy/blob/main/docs/quickstart/README.md) for how to setup a basic project as fast as possible.
## 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 (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 add-on](./tools/gltf_auto_export/README.md) to auto-export to gltf on save (and more !) if you want !)
* now also with a nice UI tool to add & edit Bevy components in [Blender](./tools/bevy_components/README.md)
* 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 dependencies: Bevy, Serde & Ron only !
* Useful if you want to use Blender as your Editor
* define Bevy components as custom properties in Blender with an UI tool to add & edit Bevy components, automatically export gltf blueprints & more in [Blender](./tools/blenvy/README.md)
* blueprints & levels system : turn your Blender collections into [gltf Blueprints](./crates/blenvy/README.md) for reuse inside levels that are just Blender scenes
* setup & tweak components in Blender Collections & override any of them in your collection instances if you want
* setup & tweak components for objects, meshes and materials as well !
* automatically load all assets for each blueprint (gltf files, manually added assets), with no setup required
* hot reload of your levels & blueprints
* minimal setup & code, you can have something basic running fast
* minimal dependencies: Bevy, Serde & RON only!
* opensource
> If you were previously using the individual bevy_gltf_xxx crates & Blender add-ons please see the [migration guide](./Migration_guide.md)
## 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.
One crate to rule them all !
* [blenvy](./crates/blenvy/) This crate allows you to
* define components direclty inside gltf files and instanciate/inject the components on the Bevy side.
* export your project's Bevy registry to json, in order to be able to generate custom component UIs on the Blender side in the Blender [blenvy](./tools/blenvy/README.md) add-on
* define Blueprints/Prefabs for Bevy inside gltf files and spawn them in Bevy. With the ability to override and add components when spawning, efficient "level" loading etc
* the ability to save & load your game state in a relatively simple way, by leveraging the blueprint functionality to only save a minimal subset of dynamic data, seperating dynamic & static parts of levels etc.
OLD videos:
There is a [video tutorial/explanation](https://youtu.be/-lcScjQCA3c) if you want, or you can read the crate docs.
The examples for the crate are [here](./examples/bevy_gltf_components/)
- [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, efficient "level" loading etc
There is a [video tutorial/explanation](https://youtu.be/CgyNtwgYwdM) for this one too, or you can read the crate docs
The examples for the crate are [here](./examples/bevy_gltf_blueprints/)
> Note: this is the recomended crate to use and uses ```bevy_gltf_components``` under the hood
- [bevy_gltf_save_load](./crates/bevy_gltf_save_load/) This crate adds the ability to save & load your game state in a relatively simple way, by leveraging the blueprint functionality of
bevy_gltf_blueprints to only save a minimal subset of dynamic data, seperating dynamic & static parts of levels etc.
The examples for the crate are [here](./examples/bevy_gltf_save_load/)
> Note: this uses ```bevy_gltf_blueprints``` under the hood
- [bevy_registry_export](./crates/bevy_registry_export/) This crate adds the ability to export your project's Bevy registry to json, in order to be able to generate custom component UIs on the Blender side in the Blender [bevy_components](./tools/bevy_components/README.md) add-on
The examples for the crate are [here](./examples/blenvy/)
## 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
### Blender: bevy_components
- an add-on for Blender to allow easilly adding & editing Bevy components , using automatically generated UIs for each component
Please read the [README]((./tools/bevy_components/README.md)) of the add-on for installation & use instructions
### Blender: blenvy
* an all in one [Blender addon](./tools/blenvy/README.md) for the Blender side of the workflow:
* allow easilly adding & editing Bevy components , using automatically generated UIs for each component
* automatically exports your level/world from Blender to gltf whenever you save your Blend file
* automatically export your [Gltf blueprints](./crates/blenvy/README.md) & assets
## Examples
you can find all examples, by crate as seperate crates for clearer dependencies in [here](./examples/)
- [bevy_gltf_components](./examples/bevy_gltf_components/)
* [basic](./examples/bevy_gltf_components/basic/) use of ```bevy_gltf_components``` only, to spawn entities with components defined inside gltf files
- [bevy_gltf_blueprints](./examples/bevy_gltf_blueprints/)
* [basic](./examples/bevy_gltf_blueprints/basic/) 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
* [animation](./examples/bevy_gltf_blueprints/animation/) how to use and trigger animations from gltf files (a feature of ```bevy_gltf_blueprints```)
* & lots more
- [bevy_gltf_save_load](./examples/bevy_gltf_save_load/)
- [bevy_registry_export](./examples/bevy_registry_export/)
you can find all examples, [here](./examples/blenvy)
* [components](./examples/blenvy/components/) use of ```components``` only, to spawn entities with components defined inside gltf files
* [blueprints](./examples/blenvy/blueprints/) use of ```blueprints``` and ```levels``` to spawn a level and then populate it with entities coming from different gltf files, live (at runtime) spawning of entities etc
* [animation](./examples/blenvy/animation/) how to use and trigger animations from gltf files
* [save_load](./examples/blenvy/save_load/) how to save & load levels
* [demo](./examples/demo/) a full demo showcasing all features , including physics, animation
## Workflow
The workflow goes as follows (once you got your Bevy code setup)
- 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](https://github.com/kaosat-dev/Blenvy/blob/main/docs/component_registration.png)
- then you have two options
- add your components to objects in Blender **manually** as **custom properties** : see [here](./README-workflow-classic.md) for more details
- add your components to objects in Blender **with a nice UI** see [here](./README-workflow-ui.md) for more details
* setup & then use the Blenvy [Bevy crate](./crates/blenvy/README.md)
* setup & then use the Blenvy [Blender add-on](./tools/blenvy/README.md)
* iterate
* have fun !
* then add your components to objects in Blender **with a nice UI** see [here](./README-workflow-ui.md) for more details
See the [quickstart](https://github.com/kaosat-dev/Blenvy/blob/main/docs/quickstart/README.md) for a full step-by-step guide.
## Third Party Integration
Read about the [Avian Physics Integration](https://github.com/kaosat-dev/Blenvy/blob/main/docs/avian/README.md) to learn how to setup colliders in Blender that will be used by the Avian physics engine in Bevy.
## Limitations / issues
- Some of `bevy_rapier`/physics code / ways to define colliders could perhaps be done better/visually within Blender (currently it also goes via RON)
## 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
## Credits
- somebody I cannot recall helped me originally with the gltf loading tracker in the Bevy Discord, so thanks ! And if it was you, please let me know so I can give credit where credit is due :)
* Some of `avian` or `bevy_rapier` /physics code / ways to define colliders could perhaps be done better/visually within Blender
## Contributors
Thanks to all the contributors helping out with this project ! Big kudos to you, contributions are always appreciated ! :)
- [GitGhillie](https://github.com/GitGhillie)
- [Azorlogh](https://github.com/Azorlogh)
- [BSDGuyShawn](https://github.com/BSDGuyShawn)
- [yukkop](https://github.com/yukkop)
- [killercup](https://github.com/killercup)
- [janhohenheim ](https://github.com/janhohenheim)
* [GitGhillie](https://github.com/GitGhillie)
* [Azorlogh](https://github.com/Azorlogh)
* [BSDGuyShawn](https://github.com/BSDGuyShawn)
* [yukkop](https://github.com/yukkop)
* [killercup](https://github.com/killercup)
* [janhohenheim](https://github.com/janhohenheim)
* [BUGO07](https://github.com/BUGO07)
* [ChristopherBiscardi](https://github.com/ChristopherBiscardi)
* [slyedoc](https://github.com/slyedoc)
## License
This repo, 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)
* 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>)

36
RELEASE_NOTES.md Normal file
View File

@ -0,0 +1,36 @@
This new release has a lot of breaking changes in the Blender tooling as well as in the Bevy crates
Here is a rundown + rationale behing the changes:
- Blender add-ons
- Auto_export:
- spliting out of gltf exporter settings
up until now , the auto export add-on provided a *subset* of gltf settings, causing issues due to lack of features (in particular for animation settings),
and it required me to play catch up every time there where added / changed settings
the Ui of Blender's gltf exporter is NOT reusable in any form or way inside another add-on . I tried various solutions such as turning the auto exporter
into a 'extension' of the standard exporter, but none of them worked in a reliable or 'nice to use' way
So I decided to split the gltf settings from the auto_export setting, this has multiple advantages:
* access to ALL gltf settings
* the gltf UI & settings will always keep up with the official releases
* less maintenance work
The only disadvantage is that the standard gltf exporter is a normal exporter, so it generates a 'fake' gltf file that immediatly gets deleted after export,
so you will need to use the new side panel to set your gltf settings:
* this is also done to ensure that the gltf settings settings used for auto export are NOT interfering with the ones you might use when exporting gltf files normally
- change detection :
after spending MANY hours analysing the issues with change detection (using some of Blender's built in logic) I came to the conclusion that it was not going to be reliable enough, so I opted for a 'hand made' brute force approach to change detection:
* every time you save (& thus export), all your scenes are serialized / hashed and that hashed version is compared to the one from the last save to determine what changed and what did not
- handling of external/ embeded / split etc collections
while adding tests I also realised that the detection of which main scenes & blueprints that needed to be exported was faulty, so I rewrote all the code in charge of that : this means that in general , based on your settings, the add-on will more accuratly export only those levels/blueprints that really NEED to be exported
- improved handling of multi-blend file projects
Up until now, all export paths where relative ** to the blend file itself** which could lead to issues when working with multiple blend files
Also for future improvements regarding assets managment, I changed the export paths to be relative to a new "project root" folder which is your *Bevy project's root folder*
- the levels/worlds now also got a seperate setting so you can easilly set where to export them too (they are not dumped out into the main export folder anymore), giving you more control over your non blueprint exports
- bevy_components
Up until now , it was not possible to have multiple components with the same name (ie ) as all the logic was based on short names
This required completely changing HOW/WHERE components are stored in objects, and they are now stored inside a 'bevy_components' custom property

397
TODO.md Normal file
View File

@ -0,0 +1,397 @@
Auto export
- [x] the original blueprints & levels path are now left as is, and there is an auto injection of xxxpath_full for absolute paths
- [x] replace all uses of the paths with the correct ones above
- [x] levels
- [x] blueprints
- [x] materials
- [x] move out the UI for "assets" folder out of "blueprints condition"
- [x] fix asset path calculations
- root path => relative to blend file path
- asset path => relative to root path
- blueprints/levels/blueprints path => relative to assets path
- [ ] add error handling for de/serialization of project, so that in case of error, the previous saved serialized project is thrown away
- move out some parameters from auto export to a higher level (as they are now used in multiple places)
- [x] main/ library scene names
- [x] paths
- [x] Data storage for custom properties:
- for scenes (main scenes)
- at scene level
- for blueprints
- at collection level
- Note: these should be COPIED to the scene level when exporting, into the temp_scene's properties
> NOTE: UP until we manage to create a PR for Bevy to directly support the scene level gltf_extras, the auto exporter should automatically create (& remove)
any additional object with scene_<scene_name>_components to copy that data to
Assets:
- blueprint assets should be auto_generated & inserted into the list of assets : these assets are NOT removable by the user
- should not change the list of manually added assets
- [x] store assets
- [x] per main scene for level/world assets
- [x] per blueprint for blueprint in lib scene
- [x] UI:
- [x] we need to display all direct assets (stored in the scene)
- [ ] indirect assets:
- QUESTION : do we want to include them in the list of assets per level ?
- this would enable pre-loading ALL the assets, but is not ideal in most other cases
- so add an option ?
- [x] the assets of local blueprints
Blueprints:
- [x] on save: write IN THE COLLECTION PROPERTIES
- list of assets
- export path
- [x] blueprint selection for nested blueprints is broken
- [ ] scan & inject on load
- [ ] scan & inject on save
- [ ] decide where & when to do & store blueprints data
Components:
- [x] add support for adding components to collections
- [x] upgrade all operators:
- [x] add
- [x] remove
- [x] copy & paste
- [x] BLENVY_OT_component_rename_component
- [x] BLENVY_OT_component_fix
- [x] add handling for core::ops::Range<f32> & other ranges
- [x] fix is_component_valid that is used in blenvy
- [x] Hashmap Support
- [x] fix parsing of keys's type either on Bevy side (prefered) or on the Blender side
- [x] fix weird issue with missing "0" property when adding new entry in empty hashmap => happens only if the values for the "setter" have never been set
- [x] handle missing types in registry for keys & values
- [x] adding a hashmap nukes every existing component ??
- [x] Add correct upgrade handling from individual component to bevy_components
- [x] Settings handling:
- [x] move saveable settings out to a settings file
- [x] update save & load
- [x] add handling of polling frequency & enabling
- [x] move advanced tools to components tab
- [x] remove most of the (bulk) advanced tools, too complex, too unclear (even for me !) and of limited use
- component renaming should be kept, but perhaps simplified:
- if a renaming fails because the parameters are incompatible, nuke the old parameters
- perhaps just add a display list of all NON component custom properties, so the user can find them easilly ?
- [x] status "unregistered" is often false and misleading
-> see in registry ui "for custom_property in object.keys():"
- [x] overhaul / improve the component selector (with built in searching, etc)
- [x] remove select_component_name_to_replace
- [x] display of invalid components is not working ?
- [x] weird items are present in the components list that do not seem to be components
- [x] remove :
- BLENVY_OT_component_list_add_item
- BLENVY_OT_component_list_remove_item
- BLENVY_OT_component_list_select_item: merge it into the rest of the actions
- [x] clearing invalid flag after a registry change does not work correctly (ie the ui still says the component is invalid)
- [x] should reset ALL "invalid" flags IF they have the matching data
- [x] registry auto reload not working ?
- [x] changing the registry breaks all the values of existing components !!!!!!
-> VERY likely due to the int-offset computation for hashes of components
- now switched to tiger_hash
- [x] add warning about hash colision (not much we can/ could do if it is the case ?)
- [x] double check weird collisions AND/OR reuse existing if applicable
- [x] annoying default path for registry, should be relative to the assets path
General things to solve:
- [x] save settings
- [x] load settings
- [x] add blueprints data
- [x] rename all path stuff using the old naming convention : "blueprints_path_full"
- [x] generate the full paths directly when setting them in the UI
- [x] problem : how to deal with defaults: do it on start/load ?
- [x] filter out scenes that have already been used in scenes list
General issues:
- there is no safeguard for naming collisions for naming across blender files
- this can cause an issue for assets list "parent"
- "parents" can only be blueprints
- they normally need/have unique export paths (otherwise, user error, perhaps show it ?)
- perhaps a simple hashing of the parent's path would be enought
- [x] addon-prefs => settings
- [x] generate_gltf_export_settings => should not use add-on prefs at all ? since we are not overriding gltf settings that way anymore ?
- [x] remove hard coded path for standard gltf settings
- [x] load settings on file load
- [x] auto_export
- [x] components
- [x] add handling of errors when trying to load settings
- [x] fix auto export workflow
- [x] add hashing of modifiers/ geometry nodes in serialize scene
- [x] add ability to FORCE export specific blueprints & levels
- [x] change scene selector to work on actual scenes aka to deal with renamed scenes
- [x] remove get_main_and_library_scenes as it should not be needed anymore
- [x] fix asset file selection
- [x] change "assets" tab to "levels"/worlds tab & modify UI accordingly
- [x] remove local assets, useless
- [x] remove 'export_marked_assets' it should be a default setting
- [x] disable/ hide asset editing ui for external assets
- [x] fix level asets UI
- [x] remove BlueprintsList & replace is with assets list
- [x] switch to bevy 0.14 rc2
- [x] trigger events when assets are loaded, blueprints are spawned & co
- [x] overall cleanup
- [x] object.add_bevy_component => blenvy.component_add
Blender side:
- [x] force overwrite of settings files instead of partial updates ?
- [x] prevent loop when loading/setting/saving settings
- [x] fix asset changes not being detected as a scene change
- [x] fix scene setting changes not being detected as a scene change
- [x] add back lighting_components
- [x] check if scene components are being deleted through our scene re-orgs in the spawn post process
- [x] fix unreliable project hashing between sessions: (note, it is due to the use of hash() : https://stackoverflow.com/questions/27522626/hash-function-in-python-3-3-returns-different-results-between-sessions)
- [x] figure out why there are still changes per session (it is due to object pointer being present in the generated "hash")
- materials & modifiers, both using the same underlying logic
- [x] filter out components_meta
- [x] filter out xxx_ui propgroups
- [x] fix missing main/lib scene names in blenvy_common_settings
- [x] fix incorect updating of main/lib scenes list in settings
- [ ] add handling of scene renames
- [x] store (on load) a mapping of scene objects to scene names
- [x] on save, calculate another mapping of scene objects to scene names
- if there is a mismatch between the stored version & the new version for a given scene, it has been renamed !
- [x] pass this information to scene diffing to remap old/new scene names
- [ ] move the rename detection to AFTER scene serialization, otherwise we could have a naming mistmatch
- weird behaviour, perhaps find another way , ie for example replace scene name in saved previous data
- is post save causing the issue ? review
- [x] investigate weird issue of changes detected to all after a reload
- [x] should we write the previous _xxx data only AFTER a sucessfull export only ?
- [x] finer grained control of setting changes to trigger a re-export:
- [x] common: any of them should trigger
- [x] components: none
- [x] auto_export:
- auto_export: yes
- gltf settings: yes
- change detection: no ?
- export blueprints: YES
- export split dynamic/static: YES
- export merge mode : YES
- materials: YES
- [x] blenvy tooling not appearing in library scenes ?? (edit: was actually , it was not appearing in anything but object mode)
- [x] find a solution for the new color handling
- [x] in theory, srgba, linearrgba , and hsva should be able to be represented visually
- [x] bevy_render::color::Color => bevy_color::color::Color
- [x] fix weird issue with hashmaps with enums as values
- [x] prevent attempting to add unexisting components to targets (ie when using the component search)
- [x] also for the bulk fix actions
- [x] selection of nested objects in collections IS NOT WORKING !!! AHH
- [x] fix/ overhaul upgreadable components
- [x] add listing of upgradeable components for
- [x] meshes
- [x] materials
- [x] fix display of upgradeaeble components & co
- [x] add clear visual distinction between internal (selectable) & non selectable ones
- [x] do not make selection button available for external blueprints/collections
- [x] perhaps do not show the other buttons & inputs either ? we cannot change the values of an external library file anyway
- [x] BLENVY_OT_item_select is missing handling for the other types (outside of object & collection)
- [x] fix selection logic
- [x] update testing blend files
- [x] disable 'export_hierarchy_full_collections' for all cases: not reliable and redudant
- [x] fix systematic material exports despite no changes
- [x] investigate lack of detection of changes of adding/changing components
- [x] change scene serialization to account for collections ...sigh
- [x] also add one NOT PER scene for materials, to fix the above issue with materials
- [x] move material caching into hash material
- [x] also remove ____dummy____.bin when export format is gltf
- [ ] fix/cleanup asset information injection (also needed for hot reload)
- [ ] add back per blueprint assets
- [ ] reuse the already existing asset_scan + export thing
- thoughts:
- the "list of all assets" is actually the "fake"/generated one: nobody would write a list of assets for sub assets,
you would just add the assets to your blueprint
- in Bevy at spawning we have
blueprint => assets
for hot reload we need
asset => blueprint instances so we can despawn/respawn etc blueprint instances when one of their assets has changed
problem of untyped vs typed
perhaps have a mapping of untyped => typed id
map asset id => [entity ids]
- [ ] add option to 'split out' meshes from blueprints ?
- [ ] ie considering meshletts etc , it would make sense to keep blueprints seperate from purely mesh gltfs
- [ ] materials fixes & upgrades
- [x] materials do not get exported again if the files are missing, until you change a material
- [x] materials do not get exported when a material is added ?
- [x] add_material_info_to_objects is now called AFTER the blueprint export, making it useless, needs change !
- [ ] materials_path custom property should be ignored both in the list of fixable component AND on export
- [ ] if we want to add material_infos & others as normal components they should not be editable, so we need another attribute, and adapt the UI for that
- [x] injection of materials_infos should be done outside of export_materials as it should run even if materials do not need exporting
- [x] if material library is toggled, then changes to materials should not change the blueprints that are using them => not really: as the name & co might change
- [ ] material assets seem to be added to list regardless of whether material exports are enabled or not
- [x] review & upgrade overall logic of material libraries, their names & output path
- [x] change materials logic to work with multiple materials per mesh
- [x] the index of the generated gltf files is reliable, and can be used both in Blender & Bevy
- [x] change MaterialInfo to MaterialInfos & turn it into a vec/list & updated logic both on Blender & Bevy side
- [ ] persist exported materials paths in blueprints so that it can be read from library file users
- [ ] just like "export_path" write it into each blueprint's collection
- [ ] scan for used materials per blueprint !
- [ ] for scenes, scan for used materials of all non instance objects (TODO: what about overrides ?)
- [ ] add a way of visualizing per blueprint instances ?
- [ ] display export path of blueprints (mostly external) ?
- [ ] hidden objects/collections only semi respected at export
- this is because blueprints are external ?
- [ ] verify based on gltf settings
- [ ] add "Visibility::Hidden" component otherwise
https://devtalk.blender.org/t/how-to-get-render-visibility-for-object/23717
- [ ] inject_export_path_into_internal_blueprints should be called on every asset/blueprint scan !! Not just on export
- [ ] undo after a save removes any saved "serialized scene" data ? DIG into this
- [ ] add tests for
- [ ] disabled components
- [ ] blueprint instances as children of blueprint instances
- [ ] blueprint instances as children of empties
- [x] check/ fix behaviour of blender plugin if all folders are the same (ie, all in assets for example)
- [x] rename all "main scene xx" to "level scene"
- [x] make sure the "add scene" button is not available unless you have actually selected one
- [x] make auto export be on by default, however bail out early by detecting if there are any level/blueprint scenes
- [x] remove custom components to filter out correctly from exported blueprints list
- [ ] for built in Component types that need to be injected to be used by blueprints,
namely:
- BlueprintInfos (not 100% sure for this one)
- MaterialInfos
- the various scene Components
- BlueprintAssets (although not really needed anymore with the meta files)
- [ ] Inject real components instead of just custom properties
- [ ] add boilerplate to generate real component values from type definitions instead of hacking pseudo ron strings
- [ ] fall back to bevy_components if that fails (missing registry) or just basic custom properties
- [ ] If that fails as well ?
- [ ] auto reload registry if absent/possible
- cannot be done from UI possibly polling with increasing timeout
- bpy.ops.blenvy.components_registry_reload()
- double check bpy.context.components_registry
- [x] filter out MaterialInfos from list of "fixable components"
Bevy Side:
- [x] deprecate BlueprintName & BlueprintPath & use BlueprintInfo instead
- [x] make blueprint instances invisible until spawning is done to avoid "spawn flash"?
- [x] make this controlable via an additional "HideUntilReady" component
- [x] register "HideUntilReady" so users can set this on their blueprints in Blender directly
- [x] restructure blueprint spawning
- [x] "blueprint ready" only be triggered after all its sub blueprints are ready
- [x] "blueprintInstance ready"/finished
BlueprintAssetsLoaded
BlueprintSceneSpawned
BlueprintChildrenReady
BlueprintReadyForPostProcess
- [x] fix issues with deeply nested blueprints
- perhaps reverse logic by using iter_ascendants
- [x] fix materials handling
- [ ] fix animations handling
- [x] fix/upgrade blueprint level animations
- [x] fix/upgrade scene level animations
- [x] rename SceneAnimations to InstanceAnimations (more coherent with the rest)
- [ ] add back & cleanup animation frame triggers
- [x] move sub blueprint handling to blueprints_finalize_instances
- [x] look into component overriding , it seems broken:
- [x] blueprint level/ collection level components are now visible in instances in Blender
- [x] they do not seem to be transfered to the (instance) entity above:
could they be on the "empty node" ?
- [ ] simplify testing example:
- [x] remove use of rapier physics (or even the whole common boilerplate ?)
- [ ] remove/replace bevy editor pls with some native ui to display hierarchies
- [ ] a full fledged demo (including physics & co)
- [ ] other examples without interactions or physics
- [ ] add hot reloading
- [x] basics
- [x] make it enabled/disabled based on general flag
- [x] account for changes impact both parent & children (ie "world" and "blueprint3") for example, which leads to a crash as there is double despawn /respawn so we need to filter things out
- [x] if there are many assets/blueprints that have changed at the same time, it causes issues similar to the above, so apply a similar fix
- [x] also ignore any entities currently spawning (better to loose some information, than cause a crash)
- [x] for sub blueprint tracking: do not propagate/ deal with parent blueprints if they are not themselves Spawning (ie filter out by "BlueprintSpawning")
- [x] cleanup internals
- [ ] analyse what is off with blueprint level components
- [x] add the root blueprint itself to the assets either on the blender side or on the bevy side programatically
- [ ] invalidate despawned entity & parent entities AABB
- [ ] add unloading/cache removal of materials
- [ ] add back and upgrade save-load
- [x] review & change general component insertion & spawning ordering & logic
- GltfComponentsSet::Injection => GltfBlueprintsSet::Spawn => GltfBlueprintsSet::AfterSpawn
Injection => inject lights & co => spawn => afterSpawn
=> Injection => inject lights & co
- [ ] add a way of overriding assets for collection instances => how can we make this possible
- [ ] cleanup all the spurious debug messages
- [x] fix animation handling
- [x] how to deal with animation graphs ?
- [x] remove "Library" component & co
- [x] make "InBlueprint" non optional,
- [x] and perhaps rename it to "FromBlueprint"
- [ ] perhaps change it to FromBlueprint(BlueprintInfo)
- [ ] in order for this to work correctly, stop iterating descendants as soon as there is one with an Existing FromBlueprint component
- [x] BlueprintInstanceDisabled => BlueprintInstanceDisabled
- [x] fix "remove component" operator from the rename/fix/update components panel
- [ ] replace string in BlueprintInfo path with PathBuf ?
- [x] update main docs
- [x] rename project to Blenvy
- [x] replace all references to the old 2 add-ons with those to Blenvy
- [x] rename repo to "Blenvy"
- [x] do a deprecation release of all bevy_gltf_xxx crates to point at the new Blenvy crate
- [ ] consider finding a way of having unique ids for all objects & collections in Blender (not trivial, if not impossible)
this would allow things such as
- [ ] mapping uuids to blueprint paths
- [ ] solving problems with scene renames
- [ ] the ability to map external TEXT files to data in BLender (git-able, hand editable)
- [x] make aabbs calculation non configurable, getting rid of the last setting (for now)
- [ ] add information & screenshots about adding assets to the Blender add-on docs
- [x] finally deal cleanly with gltf export failures & make sure to always reset the state of the blend file
clear && pytest -svv --blender-template ../../testing/bevy_example/art/testing_library.blend --blender-executable /home/ckaos/tools/blender/blender-4.1.0-linux-x64/blender tests/test_bevy_integration_prepare.py && pytest -svv --blender-executable /home/ckaos/tools/blender/blender-4.1.0-linux-x64/blender tests/test_bevy_integration.py
-----------
PRE ALPHA1 RELEASE:
- [x] compress images in blender docs
- [x] add a small note about using the same version of the Blender add-on & the Bevy crate
- [x] do a quick check for the basics
- [x] blender add-on
- [x] barebones bevy test
- [x] hot reload => some of it works, the rest not anymore ? (see bug https://github.com/bevyengine/bevy/issues/14698)
- [x] add "right click to edit blueprint"
- [ ] merge blenvy branch into main
- [ ] push to crates.io
- [ ] tag
------------
POST ALPHA1 RELEASE:
BEVY:
- [ ] split up "spawn from blueprint"
- [ ] cleanup very verbose messages
- [ ] fix & cleanup trigger_instance_animation_markers_events
- [ ] fix & cleanup save & load
- [ ] experiment with Bevy side for splitting out animations
- [ ] test hot reload a bit more , improve missing parts (see above)
- [ ] make a fleshed out demo
BLENDER:
- [ ] overall cleanup
- [ ] review wonky logic for cross file components injections
- [ ] update & fix tests
------------
BEFORE FINAL RELASE:
- [ ] cleanup & regenerate all examples assets
- [ ] cleanup & improve all docs

View File

@ -1,21 +0,0 @@
[package]
name = "bevy_gltf_blueprints"
version = "0.11.0"
authors = ["Mark 'kaosat-dev' Moissette"]
description = "Adds the ability to define Blueprints/Prefabs for Bevy inside gltf files and spawn them in Bevy."
homepage = "https://github.com/kaosat-dev/Blender_bevy_components_workflow"
repository = "https://github.com/kaosat-dev/Blender_bevy_components_workflow"
keywords = ["gamedev", "bevy", "gltf", "blueprint", "prefab"]
categories = ["game-development"]
edition = "2021"
license = "MIT OR Apache-2.0"
[lints]
workspace = true
[dependencies]
bevy_gltf_components = { version = "0.6", path = "../bevy_gltf_components" }
bevy = { version = "0.14", default-features = false, features = ["bevy_asset", "bevy_scene", "bevy_gltf", "bevy_animation", "animation"] }
[dev-dependencies]
bevy = { version = "0.14", default-features = false, features = ["dynamic_linking"] }

View File

@ -1,21 +0,0 @@
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

@ -1,362 +0,0 @@
[![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_workflow/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 (deprecated in favor of Blenvy)
> bevy_gltf_blueprints has been deprecated in favor of its successor [Blenvy](https://crates.io/crates/blenvy), part of the [Blenvy project](https://github.com/kaosat-dev/Blenvy). No further development or maintenance will be done for Bevy bevy_gltf_blueprints. See [#194](https://github.com/kaosat-dev/Blenvy/issues/194) for background.
Built on [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.
* Allows you to create lightweight levels, where all assets are different gltf files and loaded after the main level is loaded
* Allows you to spawn different entities from gtlf files at runtime in a clean manner, including simplified animation support !
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 add-ons that do a lot of the work for you
- [gltf_auto_export](https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/tools/gltf_auto_export)
- [bevy_components](https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/tools/bevy_components)
## Usage
Here's a minimal usage example:
```toml
# Cargo.toml
[dependencies]
bevy="0.14"
bevy_gltf_blueprints = { version = "0.11.0"}
```
```rust no_run
use bevy::prelude::*;
use bevy_gltf_blueprints::*;
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_plugins(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
));
}
}
```
## Installation
Add the following to your `[dependencies]` section in `Cargo.toml`:
```toml
bevy_gltf_blueprints = "0.11.0"
```
Or use `cargo add`:
```toml
cargo add bevy_gltf_blueprints
```
## Setup
```rust no_run
use bevy::prelude::*;
use bevy_gltf_blueprints::*;
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_plugin(BlueprintsPlugin)
.run();
}
```
you may want to configure your "library"/"blueprints" settings:
```rust no_run
use bevy::prelude::*;
use bevy_gltf_blueprints::*;
fn main() {
App::new()
.add_plugins((
BlueprintsPlugin{
library_folder: "advanced/models/library".into() // replace this with your blueprints library path , relative to the assets folder,
format: GltfFormat::GLB,// optional, use either format: GltfFormat::GLB, or format: GltfFormat::GLTF, or ..Default::default() if you want to keep the default .glb extension, this sets what extensions/ gltf files will be looked for by the library
aabbs: true, // defaults to false, enable this to automatically calculate aabb for the scene/blueprint
material_library: true, // defaults to false, enable this to enable automatic injection of materials from material library files
material_library_folder: "materials".into() //defaults to "materials" the folder to look for for the material files
..Default::default()
}
))
.run();
}
```
## 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)), // optional
// 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 ```BluePrintBundle``` for convenience , which just has
* a ```BlueprintName``` component
* a ```SpawnHere``` component
## Additional information
- When a blueprint is spawned, all its children entities (and nested children etc) also have an ```InBlueprint``` component that gets insert
- In cases where that is undesirable, you can add a ```NoInBlueprint``` component on the entity you spawn the blueprint with, and the components above will not be add
- if you want to overwrite the **path** where this crate looks for blueprints (gltf files) , you can add a ```Library``` component , and that will be used instead of the default path
ie :
```rust no_run
commands
.spawn((
Name::from("test"),
BluePrintBundle {
blueprint: BlueprintName("TestBlueprint".to_string()),
..Default::default()
},
Library("models".into()) // now the path to the blueprint above will be /assets/models/TestBlueprint.glb
))
```
- this crate also provides a special optional ```GameWorldTag``` component: this is useful when you want to keep all your spawned entities inside a root entity
You can use it in your queries to add your entities as children of this "world"
This way all your levels, your dynamic entities etc, are kept seperated from UI nodes & other entities that are not relevant to the game world
> Note: you should only have a SINGLE entity tagged with that component !
```rust no_run
commands.spawn((
SceneBundle {
scene: models
.get(game_assets.world.id())
.expect("main level should have been loaded")
.scenes[0]
.clone(),
..default()
},
bevy::prelude::Name::from("world"),
GameWorldTag, // here it is
));
```
## 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```
Typically , the order of systems should be
***bevy_gltf_components (GltfComponentsSet::Injection)*** => ***bevy_gltf_blueprints (GltfBlueprintsSet::Spawn, GltfBlueprintsSet::AfterSpawn)*** => ***replace_proxies***
see an example [here](https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/examples/bevy_gltf_blueprints/basic) for how to set it up correctly
## Animation
```bevy_gltf_blueprints``` provides some lightweight helpers to deal with animations stored in gltf files
* an ```Animations``` component that gets inserted into spawned (root) entities that contains a hashmap of all animations contained inside that entity/gltf file .
(this is a copy of the ```named_animations``` inside Bevy's gltf structures )
* an ```AnimationPlayerLink``` component that gets inserted into spawned (root) entities, to make it easier to trigger/ control animations than it usually is inside Bevy + Gltf files
The workflow for animations is as follows:
* create a gltf file with animations (using Blender & co) as you would normally do
* inside Bevy, use the ```bevy_gltf_blueprints``` boilerplate (see sections above), no specific setup beyond that is required
* to control the animation of an entity, you need to query for entities that have both ```AnimationPlayerLink``` and ```Animations``` components (added by ```bevy_gltf_blueprints```) AND entities with the ```AnimationPlayer``` component
For example:
```rust no_run
// example of changing animation of entities based on proximity to the player, for "fox" entities (Tag component)
pub fn animation_change_on_proximity_foxes(
players: Query<&GlobalTransform, With<Player>>,
animated_foxes: Query<(&GlobalTransform, &AnimationPlayerLink, &Animations ), With<Fox>>,
mut animation_players: Query<&mut AnimationPlayer>,
){
for player_transforms in players.iter() {
for (fox_tranforms, link, animations) in animated_foxes.iter() {
let distance = player_transforms
.translation()
.distance(fox_tranforms.translation());
let mut anim_name = "Walk";
if distance < 8.5 {
anim_name = "Run";
}
else if distance >= 8.5 && distance < 10.0{
anim_name = "Walk";
}
else if distance >= 10.0 && distance < 15.0{
anim_name = "Survey";
}
// now play the animation based on the chosen animation name
let mut animation_player = animation_players.get_mut(link.0).unwrap();
animation_player.play_with_transition(
animations.named_animations.get(anim_name).expect("animation name should be in the list").clone(),
Duration::from_secs(3)
).repeat();
}
}
}
```
see [here](https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/examples/bevy_gltf_blueprints/animation) for how to set it up correctly
particularly from [here](https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/examples/bevy_gltf_blueprints/animation/src/game/in_game.rs)
## Materials
You have the option of using "material libraries" to share common textures/materials between blueprints, in order to avoid asset & memory bloat:
Ie for example without this option, 56 different blueprints using the same material with a large texture would lead to the material/texture being embeded
56 times !!
you can configure this with the settings:
```rust
material_library: true // defaults to false, enable this to enable automatic injection of materials from material library files
material_library_folder: "materials".into() //defaults to "materials" the folder to look for for the material files
```
> Important! you must take care of preloading your material librairy gltf files in advance, using for example ```bevy_asset_loader```since
```bevy_gltf_blueprints``` currently does NOT take care of loading those at runtime
see an example [here](https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/examples/bevy_gltf_blueprints/materials) for how to set it up correctly
Generating optimised blueprints and material libraries can be automated using the latests version of the [Blender plugin](https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/tools/gltf_auto_export)
## Legacy mode
Starting in version 0.7 there is a new parameter ```legacy_mode``` for backwards compatibility
To disable the legacy mode: (enabled by default)
```rust no_run
BlueprintsPlugin{legacy_mode: false}
```
You **need** to disable legacy mode if you want to use the [```bevy_components```](https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/tools_bevy_blueprints/tools/bevy_components) Blender addon + the [```bevy_registry_export crate```](https://crates.io/crates/bevy_registry_export) !
As it create custom properties that are writen in real **ron** file format instead of a simplified version (the one in the legacy mode)
> Note: the legacy mode support will be dropped in future versions, and the default behaviour will be NO legacy mode
## Examples
* [basic](https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/examples/bevy_gltf_blueprints/basic)
* [xbpd](https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/examples/bevy_gltf_blueprints/basic_xpbd_physics)
* [animation](https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/examples/bevy_gltf_blueprints/animation)
* [materials](https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/examples/bevy_gltf_blueprints/materials)
* [multiple_levels_multiple_blendfiles](https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/examples/bevy_gltf_blueprints/multiple_levels_multiple_blendfiles)
## Compatible Bevy versions
The main branch is compatible with the latest Bevy release, while the branch `bevy_main` tries to track the `main` branch of Bevy (PRs updating the tracked commit are welcome).
Compatibility of `bevy_gltf_blueprints` versions:
| `bevy_gltf_blueprints` | `bevy` |
| :-- | :-- |
| `0.11` | `0.14` |
| `0.9 - 0.10` | `0.13` |
| `0.3 - 0.8` | `0.12` |
| `0.1 - 0.2` | `0.11` |
| branch `main` | `0.13` |
| branch `bevy_main` | `main` |
## 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

@ -1,18 +0,0 @@
use bevy::prelude::*;
use bevy::utils::HashMap;
#[derive(Component, Reflect, Default, Debug)]
#[reflect(Component)]
/// storage for animations for a given entity (hierarchy), essentially a clone of gltf's `named_animations`
pub struct Animations {
pub named_animations: HashMap<String, Handle<AnimationClip>>,
pub named_indices: HashMap<String, AnimationNodeIndex>,
pub graph: Handle<AnimationGraph>,
}
#[derive(Component, Debug)]
/// Stop gap helper component : this is inserted into a "root" entity (an entity representing a whole gltf file)
/// so that the root entity knows which of its children contains an actualy `AnimationPlayer` component
/// this is for convenience, because currently , Bevy's gltf parsing inserts `AnimationPlayers` "one level down"
/// ie armature/root for animated models, which means more complex queries to trigger animations that we want to avoid
pub struct AnimationPlayerLink(pub Entity);

View File

@ -1,179 +0,0 @@
pub mod spawn_from_blueprints;
pub use spawn_from_blueprints::*;
pub mod spawn_post_process;
pub(crate) use spawn_post_process::*;
pub mod animation;
pub use animation::*;
pub mod aabb;
pub use aabb::*;
pub mod materials;
pub use materials::*;
pub mod copy_components;
pub use copy_components::*;
use core::fmt;
use std::path::PathBuf;
use bevy::{
prelude::*,
render::{primitives::Aabb, view::VisibilitySystems},
utils::HashMap,
};
use bevy_gltf_components::{ComponentsFromGltfPlugin, GltfComponentsSet};
#[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,
}
impl Default for BluePrintBundle {
fn default() -> Self {
BluePrintBundle {
blueprint: BlueprintName("default".into()),
spawn_here: SpawnHere,
}
}
}
#[derive(Clone, Resource)]
pub struct BluePrintsConfig {
pub(crate) format: GltfFormat,
pub(crate) library_folder: PathBuf,
pub(crate) aabbs: bool,
pub(crate) aabb_cache: HashMap<String, Aabb>, // cache for aabbs
pub(crate) material_library: bool,
pub(crate) material_library_folder: PathBuf,
pub(crate) material_library_cache: HashMap<String, Handle<StandardMaterial>>,
}
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Default)]
pub enum GltfFormat {
#[default]
GLB,
GLTF,
}
impl fmt::Display for GltfFormat {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
GltfFormat::GLB => {
write!(f, "glb",)
}
GltfFormat::GLTF => {
write!(f, "gltf")
}
}
}
}
#[derive(Debug, Clone)]
/// Plugin for gltf blueprints
pub struct BlueprintsPlugin {
pub legacy_mode: bool, // flag that gets passed on to bevy_gltf_components
pub format: GltfFormat,
/// The base folder where library/blueprints assets are loaded from, relative to the executable.
pub library_folder: PathBuf,
/// Automatically generate aabbs for the blueprints root objects
pub aabbs: bool,
pub material_library: bool,
pub material_library_folder: PathBuf,
}
impl Default for BlueprintsPlugin {
fn default() -> Self {
Self {
legacy_mode: true,
format: GltfFormat::GLB,
library_folder: PathBuf::from("models/library"),
aabbs: false,
material_library: false,
material_library_folder: PathBuf::from("materials"),
}
}
}
fn aabbs_enabled(blueprints_config: Res<BluePrintsConfig>) -> bool {
blueprints_config.aabbs
}
fn materials_library_enabled(blueprints_config: Res<BluePrintsConfig>) -> bool {
blueprints_config.material_library
}
impl Plugin for BlueprintsPlugin {
fn build(&self, app: &mut App) {
app.add_plugins(ComponentsFromGltfPlugin {
legacy_mode: self.legacy_mode,
})
.register_type::<BlueprintName>()
.register_type::<MaterialInfo>()
.register_type::<SpawnHere>()
.register_type::<Animations>()
.register_type::<BlueprintsList>()
.register_type::<Vec<String>>()
.register_type::<HashMap<String, Vec<String>>>()
.insert_resource(BluePrintsConfig {
format: self.format,
library_folder: self.library_folder.clone(),
aabbs: self.aabbs,
aabb_cache: HashMap::new(),
material_library: self.material_library,
material_library_folder: self.material_library_folder.clone(),
material_library_cache: HashMap::new(),
})
.configure_sets(
Update,
(GltfBlueprintsSet::Spawn, GltfBlueprintsSet::AfterSpawn)
.chain()
.after(GltfComponentsSet::Injection),
)
.add_systems(
Update,
(
(
prepare_blueprints,
check_for_loaded,
spawn_from_blueprints,
apply_deferred,
)
.chain(),
(compute_scene_aabbs, apply_deferred)
.chain()
.run_if(aabbs_enabled),
apply_deferred,
(
materials_inject,
check_for_material_loaded,
materials_inject2,
)
.chain()
.run_if(materials_library_enabled),
)
.chain()
.in_set(GltfBlueprintsSet::Spawn),
)
.add_systems(
PostUpdate,
(spawned_blueprint_post_process, apply_deferred)
.chain()
.in_set(GltfBlueprintsSet::AfterSpawn)
.before(VisibilitySystems::CheckVisibility),
);
}
}

View File

@ -1,201 +0,0 @@
use std::path::Path;
use bevy::{
asset::{AssetServer, Assets, Handle},
ecs::{
component::Component,
entity::Entity,
query::{Added, With},
reflect::ReflectComponent,
system::{Commands, Query, Res, ResMut},
},
gltf::Gltf,
hierarchy::{Children, Parent},
log::debug,
pbr::StandardMaterial,
reflect::Reflect,
render::mesh::Mesh,
};
use crate::{AssetLoadTracker, AssetsToLoad, BluePrintsConfig};
#[derive(Component, Reflect, Default, Debug)]
#[reflect(Component)]
/// struct containing the name & source of the material to apply
pub struct MaterialInfo {
pub name: String,
pub source: String,
}
/// flag component
#[derive(Component)]
pub(crate) struct BlueprintMaterialAssetsLoaded;
/// flag component
#[derive(Component)]
pub(crate) struct BlueprintMaterialAssetsNotLoaded;
/// system that injects / replaces materials from material library
pub(crate) fn materials_inject(
blueprints_config: ResMut<BluePrintsConfig>,
material_infos: Query<(Entity, &MaterialInfo), Added<MaterialInfo>>,
asset_server: Res<AssetServer>,
mut commands: Commands,
) {
for (entity, material_info) in material_infos.iter() {
let model_file_name = format!(
"{}_materials_library.{}",
&material_info.source, &blueprints_config.format
);
let materials_path = Path::new(&blueprints_config.material_library_folder)
.join(Path::new(model_file_name.as_str()));
let material_name = &material_info.name;
let material_full_path = materials_path.to_str().unwrap().to_string() + "#" + material_name; // TODO: yikes, cleanup
if blueprints_config
.material_library_cache
.contains_key(&material_full_path)
{
debug!("material is cached, retrieving");
blueprints_config
.material_library_cache
.get(&material_full_path)
.expect("we should have the material available");
commands
.entity(entity)
.insert(BlueprintMaterialAssetsLoaded);
} else {
let material_file_handle: Handle<Gltf> = asset_server.load(materials_path.clone());
let material_file_id = material_file_handle.id();
let asset_infos: Vec<AssetLoadTracker<Gltf>> = vec![AssetLoadTracker {
name: material_full_path,
id: material_file_id,
loaded: false,
handle: material_file_handle.clone(),
}];
commands
.entity(entity)
.insert(AssetsToLoad {
all_loaded: false,
asset_infos,
..Default::default()
})
.insert(BlueprintMaterialAssetsNotLoaded);
/**/
}
}
}
// TODO, merge with check_for_loaded, make generic ?
pub(crate) fn check_for_material_loaded(
mut blueprint_assets_to_load: Query<
(Entity, &mut AssetsToLoad<Gltf>),
With<BlueprintMaterialAssetsNotLoaded>,
>,
asset_server: Res<AssetServer>,
mut commands: Commands,
) {
for (entity, mut assets_to_load) in blueprint_assets_to_load.iter_mut() {
let mut all_loaded = true;
let mut loaded_amount = 0;
let total = assets_to_load.asset_infos.len();
for tracker in assets_to_load.asset_infos.iter_mut() {
let asset_id = tracker.id;
let loaded = asset_server.is_loaded_with_dependencies(asset_id);
tracker.loaded = loaded;
if loaded {
loaded_amount += 1;
} else {
all_loaded = false;
}
}
let progress: f32 = loaded_amount as f32 / total as f32;
assets_to_load.progress = progress;
if all_loaded {
assets_to_load.all_loaded = true;
commands
.entity(entity)
.insert(BlueprintMaterialAssetsLoaded)
.remove::<BlueprintMaterialAssetsNotLoaded>();
}
}
}
/// system that injects / replaces materials from material library
pub(crate) fn materials_inject2(
mut blueprints_config: ResMut<BluePrintsConfig>,
material_infos: Query<
(&MaterialInfo, &Children),
(
Added<BlueprintMaterialAssetsLoaded>,
With<BlueprintMaterialAssetsLoaded>,
),
>,
with_materials_and_meshes: Query<
(),
(
With<Parent>,
With<Handle<StandardMaterial>>,
With<Handle<Mesh>>,
),
>,
assets_gltf: Res<Assets<Gltf>>,
asset_server: Res<AssetServer>,
mut commands: Commands,
) {
for (material_info, children) in material_infos.iter() {
let model_file_name = format!(
"{}_materials_library.{}",
&material_info.source, &blueprints_config.format
);
let materials_path = Path::new(&blueprints_config.material_library_folder)
.join(Path::new(model_file_name.as_str()));
let material_name = &material_info.name;
let material_full_path = materials_path.to_str().unwrap().to_string() + "#" + material_name; // TODO: yikes, cleanup
let mut material_found: Option<&Handle<StandardMaterial>> = None;
if blueprints_config
.material_library_cache
.contains_key(&material_full_path)
{
debug!("material is cached, retrieving");
let material = blueprints_config
.material_library_cache
.get(&material_full_path)
.expect("we should have the material available");
material_found = Some(material);
} else {
let model_handle: Handle<Gltf> = asset_server.load(materials_path.clone()); // FIXME: kinda weird now
let mat_gltf = assets_gltf
.get(model_handle.id())
.expect("material should have been preloaded");
if mat_gltf.named_materials.contains_key(material_name as &str) {
let material = mat_gltf
.named_materials
.get(material_name as &str)
.expect("this material should have been loaded");
blueprints_config
.material_library_cache
.insert(material_full_path, material.clone());
material_found = Some(material);
}
}
if let Some(material) = material_found {
for child in children.iter() {
if with_materials_and_meshes.contains(*child) {
debug!(
"injecting material {}, path: {:?}",
material_name,
materials_path.clone()
);
commands.entity(*child).insert(material.clone());
}
}
}
}
}

View File

@ -1,312 +0,0 @@
use std::path::{Path, PathBuf};
use bevy::{gltf::Gltf, prelude::*, utils::HashMap};
use crate::{Animations, 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)]
/// flag component for dynamically spawned scenes
pub struct Spawned;
#[derive(Component, Reflect, Default, Debug)]
#[reflect(Component)]
/// flag component marking any spwaned child of blueprints ..unless the original entity was marked with the `NoInBlueprint` marker component
pub struct InBlueprint;
#[derive(Component, Reflect, Default, Debug)]
#[reflect(Component)]
/// flag component preventing any spawned child of blueprints to be marked with the `InBlueprint` component
pub struct NoInBlueprint;
#[derive(Component, Reflect, Default, Debug)]
#[reflect(Component)]
// this allows overriding the default library path for a given entity/blueprint
pub struct Library(pub PathBuf);
#[derive(Component, Reflect, Default, Debug)]
#[reflect(Component)]
/// flag component to force adding newly spawned entity as child of game world
pub struct AddToGameWorld;
#[derive(Component)]
/// helper component, just to transfer child data
pub(crate) struct OriginalChildren(pub Vec<Entity>);
/// helper component, is used to store the list of sub blueprints to enable automatic loading of dependend blueprints
#[derive(Component, Reflect, Default, Debug)]
#[reflect(Component)]
pub struct BlueprintsList(pub HashMap<String, Vec<String>>);
/// helper component, for tracking loaded assets's loading state, id , handle etc
#[derive(Default, Debug)]
pub(crate) struct AssetLoadTracker<T: bevy::prelude::Asset> {
#[allow(dead_code)]
pub name: String,
pub id: AssetId<T>,
pub loaded: bool,
#[allow(dead_code)]
pub handle: Handle<T>,
}
/// helper component, for tracking loaded assets
#[derive(Component, Debug)]
pub(crate) struct AssetsToLoad<T: bevy::prelude::Asset> {
pub all_loaded: bool,
pub asset_infos: Vec<AssetLoadTracker<T>>,
pub progress: f32,
}
impl<T: bevy::prelude::Asset> Default for AssetsToLoad<T> {
fn default() -> Self {
Self {
all_loaded: Default::default(),
asset_infos: Default::default(),
progress: Default::default(),
}
}
}
/// flag component, usually added when a blueprint is loaded
#[derive(Component)]
pub(crate) struct BlueprintAssetsLoaded;
/// flag component
#[derive(Component)]
pub(crate) struct BlueprintAssetsNotLoaded;
/// spawning prepare function,
/// * also takes into account the already exisiting "override" components, ie "override components" > components from blueprint
pub(crate) fn prepare_blueprints(
spawn_placeholders: Query<
(
Entity,
&BlueprintName,
Option<&Parent>,
Option<&Library>,
Option<&Name>,
Option<&BlueprintsList>,
),
(Added<BlueprintName>, Added<SpawnHere>, Without<Spawned>),
>,
mut commands: Commands,
asset_server: Res<AssetServer>,
blueprints_config: Res<BluePrintsConfig>,
) {
for (entity, blupeprint_name, original_parent, library_override, name, blueprints_list) in
spawn_placeholders.iter()
{
debug!(
"requesting to spawn {:?} for entity {:?}, id: {:?}, parent:{:?}",
blupeprint_name.0, name, entity, original_parent
);
// println!("main model path {:?}", model_path);
if blueprints_list.is_some() {
let blueprints_list = blueprints_list.unwrap();
// println!("blueprints list {:?}", blueprints_list.0.keys());
let mut asset_infos: Vec<AssetLoadTracker<Gltf>> = vec![];
let library_path =
library_override.map_or_else(|| &blueprints_config.library_folder, |l| &l.0);
for (blueprint_name, _) in blueprints_list.0.iter() {
let model_file_name = format!("{}.{}", &blueprint_name, &blueprints_config.format);
let model_path = Path::new(&library_path).join(Path::new(model_file_name.as_str()));
let model_handle: Handle<Gltf> = asset_server.load(model_path.clone());
let model_id = model_handle.id();
let loaded = asset_server.is_loaded_with_dependencies(model_id);
if !loaded {
asset_infos.push(AssetLoadTracker {
name: model_path.to_string_lossy().into(),
id: model_id,
loaded: false,
handle: model_handle.clone(),
});
}
}
// if not all assets are already loaded, inject a component to signal that we need them to be loaded
if !asset_infos.is_empty() {
commands
.entity(entity)
.insert(AssetsToLoad {
all_loaded: false,
asset_infos,
..Default::default()
})
.insert(BlueprintAssetsNotLoaded);
} else {
commands.entity(entity).insert(BlueprintAssetsLoaded);
}
} else {
// in case there are no blueprintsList, we revert back to the old behaviour
commands.entity(entity).insert(BlueprintAssetsLoaded);
}
}
}
pub(crate) fn check_for_loaded(
mut blueprint_assets_to_load: Query<
(Entity, &mut AssetsToLoad<Gltf>),
With<BlueprintAssetsNotLoaded>,
>,
asset_server: Res<AssetServer>,
mut commands: Commands,
) {
for (entity, mut assets_to_load) in blueprint_assets_to_load.iter_mut() {
let mut all_loaded = true;
let mut loaded_amount = 0;
let total = assets_to_load.asset_infos.len();
for tracker in assets_to_load.asset_infos.iter_mut() {
let asset_id = tracker.id;
let loaded = asset_server.is_loaded_with_dependencies(asset_id);
tracker.loaded = loaded;
if loaded {
loaded_amount += 1;
} else {
all_loaded = false;
}
}
let progress: f32 = loaded_amount as f32 / total as f32;
// println!("progress: {}",progress);
assets_to_load.progress = progress;
if all_loaded {
assets_to_load.all_loaded = true;
commands
.entity(entity)
.insert(BlueprintAssetsLoaded)
.remove::<BlueprintAssetsNotLoaded>();
}
}
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn spawn_from_blueprints(
spawn_placeholders: Query<
(
Entity,
&BlueprintName,
Option<&Transform>,
Option<&Parent>,
Option<&Library>,
Option<&AddToGameWorld>,
Option<&Name>,
),
(
With<BlueprintAssetsLoaded>,
Added<BlueprintAssetsLoaded>,
Without<BlueprintAssetsNotLoaded>,
),
>,
mut commands: Commands,
mut game_world: Query<Entity, With<GameWorldTag>>,
assets_gltf: Res<Assets<Gltf>>,
mut graphs: ResMut<Assets<AnimationGraph>>,
asset_server: Res<AssetServer>,
blueprints_config: Res<BluePrintsConfig>,
children: Query<&Children>,
) {
for (
entity,
blupeprint_name,
transform,
original_parent,
library_override,
add_to_world,
name,
) in spawn_placeholders.iter()
{
debug!(
"attempting to spawn {:?} for entity {:?}, id: {:?}, parent:{:?}",
blupeprint_name.0, name, entity, original_parent
);
let what = &blupeprint_name.0;
let model_file_name = format!("{}.{}", &what, &blueprints_config.format);
// library path is either defined at the plugin level or overriden by optional Library components
let library_path =
library_override.map_or_else(|| &blueprints_config.library_folder, |l| &l.0);
let model_path = Path::new(&library_path).join(Path::new(model_file_name.as_str()));
// info!("attempting to spawn {:?}", model_path);
let model_handle: Handle<Gltf> = asset_server.load(model_path.clone()); // FIXME: kinda weird now
let gltf = assets_gltf.get(&model_handle).unwrap_or_else(|| {
panic!(
"gltf file {:?} should have been loaded",
model_path.to_str()
)
});
// 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()
.next()
.expect("there should be at least one named scene in the gltf file to spawn");
let scene = &gltf.named_scenes[main_scene_name];
// transforms are optional, but still deal with them correctly
let mut transforms: Transform = Transform::default();
if transform.is_some() {
transforms = *transform.unwrap();
}
let mut original_children: Vec<Entity> = vec![];
if let Ok(c) = children.get(entity) {
for child in c.iter() {
original_children.push(*child);
}
}
let mut graph = AnimationGraph::new();
let mut named_animations: HashMap<String, Handle<AnimationClip>> = HashMap::new();
let mut named_indices: HashMap<String, AnimationNodeIndex> = HashMap::new();
for (key, clip) in gltf.named_animations.iter() {
named_animations.insert(key.to_string(), clip.clone());
let animation_index = graph.add_clip(clip.clone(), 1.0, graph.root);
named_indices.insert(key.to_string(), animation_index);
}
let graph = graphs.add(graph);
commands.entity(entity).insert((
SceneBundle {
scene: scene.clone(),
transform: transforms,
..Default::default()
},
Animations {
named_animations,
named_indices,
graph,
},
Spawned,
OriginalChildren(original_children),
));
if add_to_world.is_some() {
let world = game_world
.get_single_mut()
.expect("there should be a game world present");
commands.entity(world).add_child(entity);
}
}
}

View File

@ -1,100 +0,0 @@
use std::any::TypeId;
use bevy::gltf::Gltf;
use bevy::prelude::*;
use bevy::scene::SceneInstance;
// use bevy::utils::hashbrown::HashSet;
use super::{AnimationPlayerLink, Animations};
use super::{SpawnHere, Spawned};
use crate::{
AssetsToLoad, BlueprintAssetsLoaded, CopyComponents, InBlueprint, NoInBlueprint,
OriginalChildren,
};
/// this system is in charge of doing any necessary post processing after a blueprint scene has been spawned
/// - it removes one level of useless nesting
/// - it copies the blueprint's root components to the entity it was spawned on (original entity)
/// - it copies the children of the blueprint scene into the original entity
/// - it add `AnimationLink` components so that animations can be controlled from the original entity
/// - it cleans up/ removes a few , by then uneeded components
pub(crate) fn spawned_blueprint_post_process(
unprocessed_entities: Query<
(
Entity,
&Children,
&OriginalChildren,
&Animations,
Option<&NoInBlueprint>,
Option<&Name>,
),
(With<SpawnHere>, With<SceneInstance>, With<Spawned>),
>,
added_animation_players: Query<(Entity, &Parent), Added<AnimationPlayer>>,
all_children: Query<&Children>,
mut commands: Commands,
) {
for (original, children, original_children, animations, no_inblueprint, name) in
unprocessed_entities.iter()
{
debug!("post processing blueprint for entity {:?}", name);
if children.len() == 0 {
warn!("timing issue ! no children found, please restart your bevy app (bug being investigated)");
continue;
}
// the root node is the first & normally only child inside a scene, it is the one that has all relevant components
let mut root_entity = Entity::PLACEHOLDER; //FIXME: and what about childless ones ?? => should not be possible normally
// let diff = HashSet::from_iter(original_children.0).difference(HashSet::from_iter(children));
// we find the first child that was not in the entity before (aka added during the scene spawning)
for c in children.iter() {
if !original_children.0.contains(c) {
root_entity = *c;
break;
}
}
// we flag all children of the blueprint instance with 'InBlueprint'
// can be usefull to filter out anything that came from blueprints vs normal children
if no_inblueprint.is_none() {
for child in all_children.iter_descendants(root_entity) {
commands.entity(child).insert(InBlueprint);
}
}
// copy components into from blueprint instance's root_entity to original entity
commands.add(CopyComponents {
source: root_entity,
destination: original,
exclude: vec![TypeId::of::<Parent>(), TypeId::of::<Children>()],
stringent: false,
});
// we move all of children of the blueprint instance one level to the original entity
if let Ok(root_entity_children) = all_children.get(root_entity) {
for child in root_entity_children.iter() {
// info!("copying child {:?} upward from {:?} to {:?}", names.get(*child), root_entity, original);
commands.entity(original).add_child(*child);
}
}
if animations.named_animations.keys().len() > 0 {
for (added, parent) in added_animation_players.iter() {
if parent.get() == root_entity {
// FIXME: stopgap solution: since we cannot use an AnimationPlayer at the root entity level
// and we cannot update animation clips so that the EntityPaths point to one level deeper,
// BUT we still want to have some marker/control at the root entity level, we add this
commands.entity(original).insert(AnimationPlayerLink(added));
}
}
}
commands.entity(original).remove::<SpawnHere>();
commands.entity(original).remove::<Spawned>();
commands.entity(original).remove::<Handle<Scene>>();
commands.entity(original).remove::<AssetsToLoad<Gltf>>(); // also clear the sub assets tracker to free up handles, perhaps just freeing up the handles and leave the rest would be better ?
commands.entity(original).remove::<BlueprintAssetsLoaded>();
commands.entity(root_entity).despawn_recursive();
}
}

View File

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

View File

@ -1,201 +0,0 @@
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

@ -1,21 +0,0 @@
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

@ -1,145 +0,0 @@
[![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_workflow/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 (deprecated in favor of Blenvy)
> bevy_gltf_components has been deprecated in favor of its successor [Blenvy](https://crates.io/crates/blenvy), part of the [Blenvy project](https://github.com/kaosat-dev/Blenvy). No further development or maintenance will be done for Bevy bevy_gltf_components. See [#194](https://github.com/kaosat-dev/Blenvy/issues/194) for background.
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_workflow/tree/main/examples/bevy_gltf_components/basic)
* or use [```bevy_asset_loader```](https://github.com/NiklasEi/bevy_asset_loader) for reliable preloading of files, as this crate does not deal with loading your assets.
* alternatively, use the [```bevy_gltf_blueprints```](https://crates.io/crates/bevy_gltf_blueprints) crate, built 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.14"
bevy_gltf_components = { version = "0.6"}
```
```rust no_run
//too barebones of an example to be meaningfull, please see https://github.com/kaosat-dev/Blender_bevy_components_workflow/bevy_gltf_components/examples/basic for a real example
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_plugin(ComponentsFromGltfPlugin::default())
.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()
});
}
}
```
## Installation
Add the following to your `[dependencies]` section in `Cargo.toml`:
```toml
bevy_gltf_components = "0.6"
```
Or use `cargo add`:
```toml
cargo add bevy_gltf_components
```
## Configuration
starting with version 0.3, this plugin is configurable
Use the default configuration:
```rust no_run
ComponentsFromGltfPlugin::default()
```
Or disable the legacy mode: (enabled by default)
```rust no_run
ComponentsFromGltfPlugin{legacy_mode: false}
```
You **need** to disable legacy mode if you want to use the [```bevy_components```](https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/tools/bevy_components) Blender addon + the [```bevy_registry_export crate```](https://crates.io/crates/bevy_registry_export) !
As it create custom properties that are writen in real **ron** file format
instead of a simplified version (the one in the legacy mode)
> Note: the legacy mode support will be dropped in future versions, and the default behaviour will be NO legacy mode
## 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```
Typically , the order of systems should be
***bevy_gltf_components (GltfComponentsSet::Injection)*** => ***replace_proxies***
## Additional features
- as of version 0.5 , this crate also includes automatic handling of lights in gltf files, to attempt to match Blender's eevee rendering as close as possible:
* **BlenderLightShadows** (automatically generated by the gltf_auto_export Blender add-on) allows you to toggle light's shadows on/off in Blender and have matching
behaviour in Bevy
* **BlenderBackgroundShader** aka background color is also automatically set on the Bevy side
* **BlenderShadowSettings** sets the cascade_size on the bevy side to match the one configured in Blender
If these components are present in your gltf file, they will be handled automatically by this crate, will be ignored otherwise.
## Examples
https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/examples/bevy_gltf_components/basic
## Compatible Bevy versions
The main branch is compatible with the latest Bevy release, while the branch `bevy_main` tries to track the `main` branch of Bevy (PRs updating the tracked commit are welcome).
Compatibility of `bevy_gltf_components` versions:
| `bevy_gltf_components` | `bevy` |
| :-- | :-- |
| `0.6` | `0.14` |
| `0.5` | `0.13` |
| `0.2 - 0.4` | `0.12` |
| `0.1` | `0.11` |
| branch `main` | `0.13` |
| branch `bevy_main` | `main` |
## 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

@ -1,97 +0,0 @@
use bevy::pbr::DirectionalLightShadowMap;
use bevy::prelude::*;
use crate::GltfComponentsSet;
pub(crate) fn plugin(app: &mut App) {
app.register_type::<BlenderBackgroundShader>()
.register_type::<BlenderShadowSettings>()
.register_type::<BlenderLightShadows>()
.add_systems(
Update,
(process_lights, process_shadowmap, process_background_shader)
.after(GltfComponentsSet::Injection),
);
}
#[derive(Component, Reflect, Default, Debug, PartialEq, Clone)]
#[reflect(Component)]
#[non_exhaustive]
/// The properties of a light's shadow , to enable controlling per light shadows from Blender
pub struct BlenderLightShadows {
pub enabled: bool,
pub buffer_bias: f32,
}
/// The background color as described by Blender's [background shader](https://docs.blender.org/manual/en/latest/render/shader_nodes/shader/background.html).
#[derive(Component, Reflect, Default, Debug, PartialEq, Clone)]
#[reflect(Component)]
#[non_exhaustive]
pub struct BlenderBackgroundShader {
pub color: Color,
pub strength: f32,
}
/// The settings used by EEVEE's [shadow rendering](https://docs.blender.org/manual/en/latest/render/eevee/render_settings/shadows.html).
#[derive(Component, Reflect, Default, Debug, PartialEq, Clone)]
#[reflect(Component)]
#[non_exhaustive]
pub struct BlenderShadowSettings {
pub cascade_size: usize,
}
fn process_lights(
mut directional_lights: Query<
(&mut DirectionalLight, Option<&BlenderLightShadows>),
Added<DirectionalLight>,
>,
mut spot_lights: Query<(&mut SpotLight, Option<&BlenderLightShadows>), Added<SpotLight>>,
mut point_lights: Query<(&mut PointLight, Option<&BlenderLightShadows>), Added<PointLight>>,
) {
for (mut light, blender_light_shadows) in directional_lights.iter_mut() {
if let Some(blender_light_shadows) = blender_light_shadows {
light.shadows_enabled = blender_light_shadows.enabled;
} else {
light.shadows_enabled = true;
}
}
for (mut light, blender_light_shadows) in spot_lights.iter_mut() {
if let Some(blender_light_shadows) = blender_light_shadows {
light.shadows_enabled = blender_light_shadows.enabled;
} else {
light.shadows_enabled = true;
}
}
for (mut light, blender_light_shadows) in point_lights.iter_mut() {
if let Some(blender_light_shadows) = blender_light_shadows {
light.shadows_enabled = blender_light_shadows.enabled;
} else {
light.shadows_enabled = true;
}
}
}
fn process_shadowmap(
shadowmaps: Query<&BlenderShadowSettings, Added<BlenderShadowSettings>>,
mut commands: Commands,
) {
for shadowmap in shadowmaps.iter() {
commands.insert_resource(DirectionalLightShadowMap {
size: shadowmap.cascade_size,
});
}
}
fn process_background_shader(
background_shaders: Query<&BlenderBackgroundShader, Added<BlenderBackgroundShader>>,
mut commands: Commands,
) {
for background_shader in background_shaders.iter() {
commands.insert_resource(AmbientLight {
color: background_shader.color,
// Just a guess, see <https://github.com/bevyengine/bevy/issues/12280>
brightness: background_shader.strength * 400.0,
});
}
}

View File

@ -1,97 +0,0 @@
use bevy::{
core::Name,
ecs::{
entity::Entity,
query::{Added, Without},
reflect::{AppTypeRegistry, ReflectComponent},
world::World,
},
gltf::GltfExtras,
hierarchy::Parent,
log::debug,
reflect::{Reflect, TypeRegistration},
utils::HashMap,
};
use crate::{ronstring_to_reflect_component, GltfComponentsConfig, GltfProcessed};
/// main function: injects components into each entity in gltf files that have `gltf_extras`, using reflection
pub fn add_components_from_gltf_extras(world: &mut World) {
let mut extras =
world.query_filtered::<(Entity, &Name, &GltfExtras, &Parent), (Added<GltfExtras>, Without<GltfProcessed>)>();
let mut entity_components: HashMap<Entity, Vec<(Box<dyn Reflect>, TypeRegistration)>> =
HashMap::new();
let gltf_components_config = world.resource::<GltfComponentsConfig>();
for (entity, name, extra, parent) in extras.iter(world) {
debug!(
"Name: {}, entity {:?}, parent: {:?}, extras {:?}",
name, entity, parent, extra
);
let type_registry: &AppTypeRegistry = world.resource();
let type_registry = type_registry.read();
let reflect_components = ronstring_to_reflect_component(
&extra.value,
&type_registry,
gltf_components_config.legacy_mode,
);
// we assign the components specified /xxx_components objects to their parent node
let mut target_entity = entity;
// if the node contains "components" or ends with "_pa" (ie add to parent), the components will not be added to the entity itself but to its parent
// this is mostly used for Blender collections
if name.as_str().contains("components") || name.as_str().ends_with("_pa") {
debug!("adding components to parent");
target_entity = parent.get();
}
debug!("adding to {:?}", target_entity);
// if there where already components set to be added to this entity (for example when entity_data was refering to a parent), update the vec of entity_components accordingly
// this allows for example blender collection to provide basic ecs data & the instances to override/ define their own values
if entity_components.contains_key(&target_entity) {
let mut updated_components: Vec<(Box<dyn Reflect>, TypeRegistration)> = Vec::new();
let current_components = &entity_components[&target_entity];
// first inject the current components
for (component, type_registration) in current_components {
updated_components.push((component.clone_value(), type_registration.clone()));
}
// then inject the new components: this also enables overwrite components set in the collection
for (component, type_registration) in reflect_components {
updated_components.push((component.clone_value(), type_registration));
}
entity_components.insert(target_entity, updated_components);
} else {
entity_components.insert(target_entity, reflect_components);
}
}
for (entity, components) in entity_components {
let type_registry: &AppTypeRegistry = world.resource();
let type_registry = type_registry.clone();
let type_registry = type_registry.read();
if !components.is_empty() {
debug!("--entity {:?}, components {}", entity, components.len());
}
for (component, type_registration) in components {
debug!(
"------adding {} {:?}",
component.get_represented_type_info().unwrap().type_path(),
component
);
{
let mut entity_mut = world.entity_mut(entity);
type_registration
.data::<ReflectComponent>()
.expect("Unable to reflect component")
.insert(&mut entity_mut, &*component, &type_registry);
entity_mut.insert(GltfProcessed); // this is how can we insert any additional components
}
}
}
}

View File

@ -1,134 +0,0 @@
use bevy::log::{debug, warn};
use bevy::reflect::serde::ReflectDeserializer;
use bevy::reflect::{Reflect, TypeInfo, TypeRegistration, TypeRegistry};
use bevy::utils::HashMap;
use ron::Value;
use serde::de::DeserializeSeed;
use super::capitalize_first_letter;
pub fn ronstring_to_reflect_component(
ron_string: &str,
type_registry: &TypeRegistry,
simplified_types: bool,
) -> Vec<(Box<dyn Reflect>, TypeRegistration)> {
let lookup: HashMap<String, Value> = ron::from_str(ron_string).unwrap();
let mut components: Vec<(Box<dyn Reflect>, TypeRegistration)> = Vec::new();
for (key, value) in lookup.into_iter() {
let type_string = key.replace("component: ", "").trim().to_string();
let capitalized_type_name = capitalize_first_letter(type_string.as_str());
let mut parsed_value: String;
match value.clone() {
Value::String(str) => {
parsed_value = str;
}
_ => parsed_value = ron::to_string(&value).unwrap().to_string(),
}
if let Some(type_registration) =
type_registry.get_with_short_type_path(capitalized_type_name.as_str())
{
debug!("TYPE INFO {:?}", type_registration.type_info());
if simplified_types {
if let TypeInfo::TupleStruct(info) = type_registration.type_info() {
// we handle tupple strucs with only one field differently, as Blender's custom properties with custom ui (float, int, bool, etc) always give us a tupple struct
if info.field_len() == 1 {
let field = info
.field_at(0)
.expect("we should always have at least one field here");
let field_name = field.type_path();
let mut formated = parsed_value.clone();
match field_name {
"f32" => {
formated = parsed_value.parse::<f32>().unwrap().to_string();
}
"f64" => {
formated = parsed_value.parse::<f64>().unwrap().to_string();
}
"u8" => {
formated = parsed_value.parse::<u8>().unwrap().to_string();
}
"u16" => {
formated = parsed_value.parse::<u16>().unwrap().to_string();
}
"u32" => {
formated = parsed_value.parse::<u32>().unwrap().to_string();
}
"u64" => {
formated = parsed_value.parse::<u64>().unwrap().to_string();
}
"u128" => {
formated = parsed_value.parse::<u128>().unwrap().to_string();
}
"glam::Vec2" => {
let parsed: Vec<f32> = ron::from_str(&parsed_value).unwrap();
formated = format!("(x:{},y:{})", parsed[0], parsed[1]);
}
"glam::Vec3" => {
let parsed: Vec<f32> = ron::from_str(&parsed_value).unwrap();
formated =
format!("(x:{},y:{},z:{})", parsed[0], parsed[1], parsed[2]);
}
"bevy_render::color::Color" => {
let parsed: Vec<f32> = ron::from_str(&parsed_value).unwrap();
if parsed.len() == 3 {
formated = format!(
"Rgba(red:{},green:{},blue:{}, alpha: 1.0)",
parsed[0], parsed[1], parsed[2]
);
}
if parsed.len() == 4 {
formated = format!(
"Rgba(red:{},green:{},blue:{}, alpha:{})",
parsed[0], parsed[1], parsed[2], parsed[3]
);
}
}
_ => {}
}
parsed_value = format!("({formated})");
}
}
if parsed_value.is_empty() {
parsed_value = "()".to_string();
}
}
let ron_string = format!(
"{{ \"{}\":{} }}",
type_registration.type_info().type_path(),
parsed_value
);
// usefull to determine what an entity looks like Serialized
/*let test_struct = CameraRenderGraph::new("name");
let serializer = ReflectSerializer::new(&test_struct, &type_registry);
let serialized =
ron::ser::to_string_pretty(&serializer, ron::ser::PrettyConfig::default()).unwrap();
println!("serialized Component {}", serialized);*/
debug!("component data ron string {}", ron_string);
let mut deserializer = ron::Deserializer::from_str(ron_string.as_str())
.expect("deserialzer should have been generated from string");
let reflect_deserializer = ReflectDeserializer::new(type_registry);
let component = reflect_deserializer
.deserialize(&mut deserializer)
.unwrap_or_else(|_| {
panic!(
"failed to deserialize component {} with value: {:?}",
key, value
)
});
debug!("component {:?}", component);
debug!("real type {:?}", component.get_represented_type_info());
components.push((component, type_registration.clone()));
debug!("found type registration for {}", capitalized_type_name);
} else {
warn!("no type registration for {}", capitalized_type_name);
}
}
components
}

View File

@ -1,21 +0,0 @@
[package]
name = "bevy_gltf_save_load"
version = "0.5.0"
authors = ["Mark 'kaosat-dev' Moissette"]
description = "Save & load your bevy games"
homepage = "https://github.com/kaosat-dev/Blender_bevy_components_workflow"
repository = "https://github.com/kaosat-dev/Blender_bevy_components_workflow"
keywords = ["gamedev", "bevy", "save", "load", "serialize"]
categories = ["game-development"]
edition = "2021"
license = "MIT OR Apache-2.0"
[lints]
workspace = true
[dependencies]
bevy = { version = "0.14", default-features = false, features = ["bevy_asset", "bevy_scene", "bevy_gltf"] }
bevy_gltf_blueprints = { version = "0.11", path = "../bevy_gltf_blueprints" }
[dev-dependencies]
bevy = { version = "0.14", default-features = false, features = ["dynamic_linking"] }

View File

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

View File

@ -1,201 +0,0 @@
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

@ -1,21 +0,0 @@
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

@ -1,315 +0,0 @@
[![Crates.io](https://img.shields.io/crates/v/bevy_gltf_save_load)](https://crates.io/crates/bevy_gltf_save_load)
[![Docs](https://img.shields.io/docsrs/bevy_gltf_save_load)](https://docs.rs/bevy_gltf_save_load/latest/bevy_gltf_save_load/)
[![License](https://img.shields.io/crates/l/bevy_gltf_save_load)](https://github.com/kaosat-dev/Blender_bevy_components_workflow/blob/main/crates/bevy_gltf_save_load/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_save_load (deprecated in favor of Blenvy)
> bevy_gltf_save_load has been deprecated in favor of its successor [Blenvy](https://crates.io/crates/blenvy), part of the [Blenvy project](https://github.com/kaosat-dev/Blenvy). No further development or maintenance will be done for Bevy bevy_gltf_save_load. See [#194](https://github.com/kaosat-dev/Blenvy/issues/194) for background.
Built upon [bevy_gltf_blueprints](https://crates.io/crates/bevy_gltf_blueprints) this crate adds the ability to easilly **save** and **load** your game worlds for [Bevy](https://bevyengine.org/) .
* leverages blueprints & seperation between
* **dynamic** entities : entities that can change during the lifetime of your app/game
* **static** entities : entities that do NOT change (typically, a part of your levels/ environements)
* and allows allow for :
* a simple save/load workflow thanks to the above
* ability to specify **which entities** to save or to exclude
* ability to specify **which components** to save or to exclude
* ability to specify **which resources** to save or to exclude
* small(er) save files (only a portion of the entities is saved)
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_workflow/tree/main/tools/gltf_auto_export) that does a lot of the work for you (including spliting generating seperate gltf files for your static vs dynamic assets)
A bit of heads up:
* very opinionated !
* still in the early stages & not 100% feature complete
* fun fact: as the static level structure is stored seperatly, you can change your level layout & **still** reload an existing save file
## Usage
Here's a minimal usage example:
```toml
# Cargo.toml
[dependencies]
bevy="0.14"
bevy_gltf_save_load = "0.5"
bevy_gltf_blueprints = "0.11" // also needed
```
```rust no_run
use bevy::prelude::*;
use bevy_gltf_save_load::*;
fn main() {
App::new()
.add_plugins((
DefaultPlugins,
SaveLoadPlugin::default()
))
.run();
}
// add a system to trigger saving
pub fn request_save(
mut save_requests: EventWriter<SaveRequest>,
keycode: Res<Input<KeyCode>>,
)
{
if keycode.just_pressed(KeyCode::S) {
save_requests.send(SaveRequest {
path: "save.scn.ron".into(),
})
}
}
// add a system to trigger loading
pub fn request_load(
mut load_requests: EventWriter<LoadRequest>,
keycode: Res<Input<KeyCode>>,
)
{
if keycode.just_pressed(KeyCode::L) {
save_requests.send(LoadRequest {
path: "save.scn.ron".into(),
})
}
}
// setting up your world
// on initial setup, the static entities & the dynamic entities are kept seperate for clarity & loaded as blueprints from 2 seperate files
pub fn setup_game(
mut commands: Commands,
mut next_game_state: ResMut<NextState<GameState>>,
) {
info!("setting up game world");
// here we actually spawn our game world/level
let world_root = commands
.spawn((
Name::from("world"),
GameWorldTag,
InAppRunning,
TransformBundle::default(),
InheritedVisibility::default(),
))
.id();
// and we fill it with static entities
let static_data = commands
.spawn((
Name::from("static"),
BluePrintBundle {
blueprint: BlueprintName("World".to_string()),
..Default::default()
},
StaticEntitiesRoot,
Library("models".into())
))
.id();
// and we fill it with dynamic entities
let dynamic_data = commands
.spawn((
Name::from("dynamic"),
BluePrintBundle {
blueprint: BlueprintName("World_dynamic".to_string()),
..Default::default()
},
DynamicEntitiesRoot,
NoInBlueprint,
Library("models".into())
))
.id();
commands.entity(world_root).add_child(static_data);
commands.entity(world_root).add_child(dynamic_data);
next_game_state.set(GameState::InGame)
}
```
take a look at the [example](https://github.com/kaosat-dev/Blender_bevy_components_workflow/blob/main/examples/bevy_gltf_save_load/basic/src/game/mod.rs) for more clarity
## Installation
Add the following to your `[dependencies]` section in `Cargo.toml`:
```toml
bevy_gltf_save_load = "0.3"
bevy_gltf_blueprints = "0.10" // also needed, as bevy_gltf_save_load does not re-export it at this time
```
Or use `cargo add`:
```toml
cargo add bevy_gltf_save_load
```
## Setup
```rust no_run
use bevy::prelude::*;
use bevy_gltf_save_load::*;
fn main() {
App::new()
.add_plugins((
DefaultPlugins
SaveLoadPlugin::default()
))
.run();
}
```
you likely need to configure your settings (otherwise, not much will be saved)
```rust no_run
use bevy::prelude::*;
use bevy_gltf_save_load::*;
fn main() {
App::new()
.add_plugins((
DefaultPlugins,
SaveLoadPlugin {
save_path: "scenes".into(), // where do we save files to (under assets for now) defaults to "scenes"
component_filter: SceneFilter::Allowlist(HashSet::from([ // this is using Bevy's build in SceneFilter, you can compose what components you want to allow/deny
TypeId::of::<Name>(),
TypeId::of::<Transform>(),
TypeId::of::<Velocity>(),
// and any other commponent you want to include/exclude
])),
resource_filter: SceneFilter::deny_all(), // same logic as above, but for resources : also be careful & remember to register your resources !
..Default::default()
},
// you need to configure the blueprints plugin as well (might be pre_configured in the future, but for now you need to do it manually)
BlueprintsPlugin {
library_folder: "models/library".into(),
format: GltfFormat::GLB,
aabbs: true,
..Default::default()
},
))
.run();
}
```
### How to make sure your entites will be saved
- only entites that have a **Dynamic** component will be saved ! (the component is provided as part of the crate)
- you can either add that component at runtime or have it baked-in in the Blueprint
### Component Filter:
- by default only the following components are going to be saved
- **Parent**
- **Children**
- **BlueprintName** : part of bevy_gltf_blueprints, used under the hood
- **SpawnHere** :part of bevy_gltf_blueprints, used under the hood
- **Dynamic** : included in this crate, allows you to tag components as dynamic aka saveable ! Use this to make sure your entities are saved !
- you **CANNOT** remove these as they are part of the boilerplate
- you **CAN** add however many other components you want, allow them all etc as you see fit
- you can find more information about the SceneFilter object [here](https://bevyengine.org/news/bevy-0-11/#scene-filtering) and [here](https://docs.rs/bevy/latest/bevy/scene/enum.SceneFilter.html)
## Events
- to trigger **saving** use the ```SaveRequest``` event
```rust no_run
// add a system to trigger saving
pub fn request_save(
mut save_requests: EventWriter<SaveRequest>,
keycode: Res<Input<KeyCode>>,
)
{
if keycode.just_pressed(KeyCode::S) {
save_requests.send(SaveRequest {
path: "save.scn.ron".into(),
})
}
}
```
- to trigger **loading** use the ```LoadRequest``` event
```rust no_run
// add a system to trigger saving
pub fn request_load(
mut load_requests: EventWriter<LoadRequest>,
keycode: Res<Input<KeyCode>>,
)
{
if keycode.just_pressed(KeyCode::L) {
save_requests.send(LoadRequest {
path: "save.scn.ron".into(),
})
}
}
```
- you also notified when saving / loading is done
- ```SavingFinished``` for saving
- ```LoadingFinished``` for loading
> Note: I **highly** recomend you change states when you start/finish saving & loading, otherwise things **will** get unpredictable
Please see [the example](https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/examples/bevy_gltf_save_load/basic/src/game/mod.rs) for this.
## Additional notes
- the name + path of the **static** level blueprint/gltf file will be saved as part of the save file, and reused to dynamically
load the correct static assets, which is necessary when you have multiple levels, and thus all required information to reload a save is contained within the save
## SystemSet
For convenience ```bevy_gltf_save_load``` provides two **SystemSets**
- [```LoadingSet```](./src/lib.rs#19)
- [```SavingSet```](./src/lib.rs#24)
## Examples
Highly advised to get a better understanding of how things work !
To get started I recomend looking at
- [world setup](https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/examples/bevy_gltf_save_load/basic/src/game/in_game.rs)
- [various events & co](https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/examples/bevy_gltf_save_load/basic/src/game/mod.rs)
All examples are here:
- https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/examples/bevy_gltf_save_load/basic
## Compatible Bevy versions
The main branch is compatible with the latest Bevy release, while the branch `bevy_main` tries to track the `main` branch of Bevy (PRs updating the tracked commit are welcome).
Compatibility of `bevy_gltf_save_load` versions:
| `bevy_gltf_save_load` | `bevy` |
| :-- | :-- |
| `0.5 ` | `0.14` |
| `0.4 ` | `0.13` |
| `0.1 -0.3` | `0.12` |
| branch `main` | `0.12` |
| branch `bevy_main` | `main` |
## 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

@ -1,216 +0,0 @@
use gltf_json as json;
use json::camera::Type;
use json::validation::{Checked, Validate};
use serde_json::value::{to_raw_value, RawValue};
use serde::Serialize;
use bevy::reflect::TypeRegistryArc;
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
enum Output {
/// Output standard glTF.
Standard,
/// Output binary glTF.
Binary,
}
#[derive(Serialize)]
struct MyExtraData {
a: u32,
b: u32,
BlueprintName: String,
SpawnHere: String,
}
/*
pub fn serialize_gltf_inner<S>(serialize: S) -> Result<String, json::Error>
where
S: Serialize,
{
let pretty_config = ron::ser::PrettyConfig::default()
.indentor(" ".to_string())
.new_line("\n".to_string());
ron::ser::to_string_pretty(&serialize, pretty_config)
}*/
pub fn serialize_gltf(scene:&DynamicScene, registry: &TypeRegistryArc) {
}
pub fn save_game(
world: &mut World,
) {
let mut save_path:String = "".into();
let mut events = world
.resource_mut::<Events<SaveRequest>>();
for event in events.get_reader().read(&events) {
info!("SAVE EVENT !! {:?}", event);
save_path = event.path.clone();
}
info!("SAVING TO {}", save_path);
events.clear();
let saveable_entities: Vec<Entity> = world
.query_filtered::<Entity, With<Dynamic>>()
.iter(world)
.collect();
debug!("saveable entities {}", saveable_entities.len());
let components = HashSet::from([
TypeId::of::<Name>(),
TypeId::of::<Transform>(),
TypeId::of::<Velocity>() ,
TypeId::of::<BlueprintName>(),
TypeId::of::<SpawnHere>(),
TypeId::of::<Dynamic>(),
TypeId::of::<Camera>(),
TypeId::of::<Camera3d>(),
TypeId::of::<Tonemapping>(),
TypeId::of::<CameraTrackingOffset>(),
TypeId::of::<Projection>(),
TypeId::of::<CameraRenderGraph>(),
TypeId::of::<Frustum>(),
TypeId::of::<GlobalTransform>(),
TypeId::of::<VisibleEntities>(),
TypeId::of::<Pickable>(),
]);
let filter = SceneFilter::Allowlist(components);
let mut scene_builder = DynamicSceneBuilder::from_world(world).with_filter(filter);
let dyn_scene = scene_builder
/* .allow::<Transform>()
.allow::<Velocity>()
.allow::<BlueprintName>()*/
/* .deny::<Children>()
.deny::<Parent>()
.deny::<InheritedVisibility>()
.deny::<Visibility>()
.deny::<GltfExtras>()
.deny::<GlobalTransform>()
.deny::<Collider>()
.deny::<RigidBody>()
.deny::<Saveable>()
// camera stuff
.deny::<Camera>()
.deny::<CameraRenderGraph>()
.deny::<Camera3d>()
.deny::<Clusters>()
.deny::<VisibleEntities>()
.deny::<VisiblePointLights>()
//.deny::<HasGizmoMarker>()
*/
.extract_entities(saveable_entities.into_iter())
.build();
let serialized_scene = dyn_scene
.serialize_ron(world.resource::<AppTypeRegistry>())
.unwrap();
let mut root = gltf_json::Root::default();
// unfortunatly, not available yet
/*let node = root.push(json::Node {
//mesh: Some(mesh),
..Default::default()
});
root.push(json::Scene {
extensions: Default::default(),
extras: Default::default(),
name: None,
nodes: vec![node],
});*/
let camera = json::camera::Perspective{
aspect_ratio: Some(0.5),
yfov: 32.0,
zfar: Some(30.),
znear: 0.0,
extensions: None,
extras: None
};
/*let camera = json::Camera{
name:Some("Camera".into()),
orthographic: None,
perspective:None,
extensions: None,
extras: None,
type_: Checked<Type::Perspective>,
};*/
let gna = to_raw_value(&MyExtraData { a: 1, b: 2, BlueprintName: "Foo".into(), SpawnHere:"".into() }).unwrap() ;
let node = json::Node {
camera: None,//Some(camera),
children: None,
extensions: None,
extras: Some(gna),
matrix: None,
mesh:None,
name: Some("yeah".into()),
rotation: None,
scale: None,
translation: Some([0.5, 10.0 ,-100.]),
skin: None,
weights: None
// mesh: Some(json::Index::new(0)),
//..Default::default()
};
let root = json::Root {
accessors: vec![], //[positions, colors],
buffers: vec![],
buffer_views: vec![],
meshes: vec![],
nodes: vec![node],
scenes: vec![json::Scene {
extensions: Default::default(),
extras: Default::default(),
name: Some("Foo".to_string()),
nodes: vec![json::Index::new(0)],
}],
..Default::default()
};
let gltf_save_name = "test.gltf";
let writer = fs::File::create(format!("assets/scenes/{gltf_save_name}") ).expect("I/O error");
json::serialize::to_writer_pretty(writer, &root).expect("Serialization error");
// let bin = to_padded_byte_vector(triangle_vertices);
// let mut writer = fs::File::create("triangle/buffer0.bin").expect("I/O error");
// writer.write_all(&bin).expect("I/O error");
#[cfg(not(target_arch = "wasm32"))]
IoTaskPool::get()
.spawn(async move {
// Write the scene RON data to file
File::create(format!("assets/scenes/{save_path}"))
.and_then(|mut file| file.write(serialized_scene.as_bytes()))
.expect("Error while writing scene to file");
})
.detach();
}

View File

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

View File

@ -1,21 +0,0 @@
[package]
name = "bevy_registry_export"
version = "0.4.0"
authors = ["Mark 'kaosat-dev' Moissette", "Pascal 'Killercup' Hertleif"]
description = "Allows you to create a Json export of all your components/ registered types of your Bevy app/game"
homepage = "https://github.com/kaosat-dev/Blender_bevy_components_workflow"
repository = "https://github.com/kaosat-dev/Blender_bevy_components_workflow"
keywords = ["gamedev", "bevy", "assets", "registry", "components"]
categories = ["game-development"]
edition = "2021"
license = "MIT OR Apache-2.0"
[dependencies]
bevy = { version = "0.14", default-features = false, features = ["bevy_scene"] }
bevy_reflect = { version = "0.14", default-features = false }
bevy_app = { version = "0.14", default-features = false, features = ["bevy_reflect"] }
bevy_ecs = { version = "0.14", default-features = false, features = ["bevy_reflect"] }
serde_json = "1.0.108"
[dev-dependencies]
bevy = { version = "0.14", default-features = false, features = ["dynamic_linking"] }

View File

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

View File

@ -1,201 +0,0 @@
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 [2024] [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

@ -1,136 +0,0 @@
[![Crates.io](https://img.shields.io/crates/v/bevy_registry_export)](https://crates.io/crates/bevy_registry_export)
[![Docs](https://img.shields.io/docsrs/bevy_registry_export)](https://docs.rs/bevy_registry_export/latest/bevy_registry_export/)
[![License](https://img.shields.io/crates/l/bevy_registry_export)](https://github.com/kaosat-dev/Blender_bevy_components_workflow/blob/main/crates/bevy_registry_export/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_registry_export (deprecated in favor of Blenvy)
> bevy_registry_export has been deprecated in favor of its successor [Blenvy](https://crates.io/crates/blenvy), part of the [Blenvy project](https://github.com/kaosat-dev/Blenvy). No further development or maintenance will be done for Bevy bevy_registry_export. See [#194](https://github.com/kaosat-dev/Blenvy/issues/194) for background.
This plugin allows you to create a Json export of all your components/ registered types.
Its main use case is as a backbone for the [```bevy_components``` Blender add-on](https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/tools/bevy_components), that allows you to add & edit components directly in Blender, using the actual type definitions from Bevy
(and any of your custom types & components that you register in Bevy).
## Usage
Here's a minimal usage example:
```toml
# Cargo.toml
[dependencies]
bevy="0.14"
bevy_registry_export = "0.4"
```
```rust no_run
use bevy::prelude::*;
use bevy_registry_export::*;
fn main() {
App::new()
.add_plugins((
DefaultPlugins,
ExportRegistryPlugin::default() // will save your registry schema json file to assets/registry.json
))
.run();
}
```
take a look at the [example](https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/examples/bevy_registry_export/basic/src/core/mod.rs) for more clarity
## Installation
Add the following to your `[dependencies]` section in `Cargo.toml`:
```toml
bevy_registry_export = "0.4"
```
Or use `cargo add`:
```toml
cargo add bevy_registry_export
```
## Setup
```rust no_run
use bevy::prelude::*;
use bevy_registry_export::*;
fn main() {
App::new()
.add_plugins((
DefaultPlugins
ExportRegistryPlugin::default()
))
.run();
}
```
you can also configure the output path
```rust no_run
use bevy::prelude::*;
use bevy_registry_export::*;
fn main() {
App::new()
.add_plugins((
DefaultPlugins
ExportRegistryPlugin {
save_path: "assets/registry.json".into(),
..Default::default()
},
))
.run();
}
```
## Usage
- The output file will be generated in the ```Startup``` schedule whenever you run your app.
- Every time you compile & run your app, the output json file will be updated.
## Examples
All examples are here:
> the examples use ```bevy_gltf_blueprints``` with the **legacy_mode** set to **FALSE** as the new custom properties generated by the Blender add-on require newer/ non legacy logic.
- https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/examples/bevy_registry_export/basic
## Compatible Bevy versions
The main branch is compatible with the latest Bevy release, while the branch `bevy_main` tries to track the `main` branch of Bevy (PRs updating the tracked commit are welcome).
Compatibility of `bevy_registry_export` versions:
| `bevy_registry_export` | `bevy` | `bevy_components (Blender add-on)` |
| :-- | :-- |:-- |
| `0.4 ` | `0.14` | `0.3` |
| `0.3 ` | `0.13` | `0.3` |
| `0.2 ` | `0.12` | `0.3` |
| `0.1 ` | `0.12` | `0.1 -0.2` |
| branch `main` | `0.12` | `0.1` |
| branch `bevy_main` | `main` | `n/a` |
## Contributors
Thanks to all the contributors helping out with this project ! Big kudos to you, contributions are always appreciated ! :)
A big shout out to [killercup](https://github.com/killercup), that did the bulk of the Bevy side code !
## 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

@ -1,10 +1,10 @@
[package]
name = "bevy_gltf_components"
version = "0.6.0"
name = "blenvy"
version = "0.1.0-alpha.1"
authors = ["Mark 'kaosat-dev' Moissette"]
description = "Allows you to define Bevy components direclty inside gltf files and instanciate the components on the Bevy side."
homepage = "https://github.com/kaosat-dev/Blender_bevy_components_workflow"
repository = "https://github.com/kaosat-dev/Blender_bevy_components_workflow"
homepage = "https://github.com/kaosat-dev/Blenvy"
repository = "https://github.com/kaosat-dev/Blenvy"
keywords = ["gamedev", "bevy", "assets", "gltf", "components"]
categories = ["game-development"]
edition = "2021"
@ -14,9 +14,12 @@ license = "MIT OR Apache-2.0"
workspace = true
[dependencies]
bevy = { version = "0.14", default-features = false, features = ["bevy_asset", "bevy_scene", "bevy_gltf"] }
bevy = { version = "0.14", default-features = false, features = ["bevy_asset", "bevy_scene", "bevy_gltf", "animation"] }
serde = "1.0.188"
ron = "0.8.1"
serde_json = "1.0.108"
bevy_common_assets = {version = "0.11", features = ["ron"]}
[dev-dependencies]
bevy = { version = "0.14", default-features = false, features = ["dynamic_linking"] }

327
crates/blenvy/README.md Normal file
View File

@ -0,0 +1,327 @@
[![Crates.io](https://img.shields.io/crates/v/blenvy)](https://crates.io/crates/blenvy)
[![Docs](https://img.shields.io/docsrs/blenvy)](https://docs.rs/blenvy/latest/blenvy/)
[![License](https://img.shields.io/crates/l/blenvy)](https://github.com/kaosat-dev/Blenvy/blob/main/crates/blenvy/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)
# blenvy
This crate allows you to
- define [Bevy](https://bevyengine.org/) components direclty inside gltf files and instanciate the components on the Bevy side.
- define Blueprints/Prefabs for [Bevy](https://bevyengine.org/) inside gltf files and spawn them in Bevy.
* Allows you to create lightweight levels, where all assets are different gltf files and loaded after the main level is loaded
* Allows you to spawn different entities from gtlf files at runtime in a clean manner, including simplified animation support !
A blueprint is a set of **overrideable** components + a hierarchy: ie
* just a Gltf file with Gltf_extras specifying components
* a component called BlueprintInfo
Particularly useful when using [Blender](https://www.blender.org/) as an editor for the [Bevy](https://bevyengine.org/) game engine, combined with the Blender add-on that do a lot of the work for you
- [blenvy](https://github.com/kaosat-dev/Blenvy/tree/main/tools/blenvy)
- allows you to create a Json export of all your components/ registered types.
Its main use case is as a backbone for the [```blenvy``` Blender add-on](https://github.com/kaosat-dev/Blenvy/tree/main/tools/blenvy), that allows you to add & edit components directly in Blender, using the actual type definitions from Bevy
(and any of your custom types & components that you register in Bevy).
- adds the ability to easilly **save** and **load** your game worlds for [Bevy](https://bevyengine.org/) .
* leverages blueprints & seperation between
* **dynamic** entities : entities that can change during the lifetime of your app/game
* **static** entities : entities that do NOT change (typically, a part of your levels/ environements)
* and allows allow for :
* a simple save/load workflow thanks to the above
* ability to specify **which entities** to save or to exclude
* ability to specify **which components** to save or to exclude
* ability to specify **which resources** to save or to exclude
* small(er) save files (only a portion of the entities is saved)
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/Blenvy/tree/main/tools/blenvy) that does a lot of the work for you (including spliting generating seperate gltf files for your static vs dynamic assets)
## Usage
Here's a minimal usage example:
```toml
# Cargo.toml
[dependencies]
bevy="0.14"
blenvy = { version = "0.1.0-alpha.1"}
```
```rust no_run
use bevy::prelude::*;
use blenvy::*;
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_plugins(BlenvyPlugin::default())
.add_systems(Startup, setup_game)
.add_systems(Update, spawn_blueprint_instance)
.run();
}
// this is how you setup & spawn a level from a blueprint
fn setup_game(
mut commands: Commands,
) {
// here we spawn our game world/level, which is also a blueprint !
commands.spawn((
BlueprintInfo::from_path("levels/World.glb"), // all we need is a Blueprint info...
SpawnBlueprint, // and spawnblueprint to tell blenvy to spawn the blueprint now
HideUntilReady, // only reveal the level once it is ready
GameWorldTag,
));
}
fn spawn_blueprint_instance(
mut commands: Commands,
keycode: Res<ButtonInput<KeyCode>>,
){
if keycode.just_pressed(KeyCode::KeyS) {
let new_entity = commands.spawn((
BlueprintInfo::from_path("spawnable.glb"), // mandatory !!
SpawnBlueprint, // mandatory !!
TransformBundle::from_transform(Transform::from_xyz(0.0, 2.0, 0.2)), // VERY important !!
// any other component you want to insert
));
}
}
```
## Installation
Add the following to your `[dependencies]` section in `Cargo.toml`:
```toml
blenvy = "0.1.0-alpha.1"
```
Or use `cargo add`:
```toml
cargo add blenvy
```
## Setup
```rust no_run
use bevy::prelude::*;
use blenvy::*;
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_plugin(BlenvyPlugin::default())
.run();
}
```
you may want to configure your settings:
```rust no_run
use bevy::prelude::*;
use blenvy::*;
fn main() {
App::new()
.add_plugins((
BlenvyPlugin{
aabbs: true, // defaults to false, enable this to automatically calculate aabb for the scene/blueprint
..Default::default()
}
))
.run();
}
```
## Spawning entities from blueprints
You can spawn entities from blueprints like this:
```rust no_run
commands.spawn((
BlueprintInfo::from_path("Health_Pickup.glb"), // mandatory !!
// or the alterive: BlueprintInfo{name:"health pickup1".into(), path:"Health_Pickup.glb".into()}
SpawnBlueprint, // mandatory !!
TransformBundle::from_transform(Transform::from_xyz(x, 2.0, y)), // optional
// any other component you want to insert
))
```
Once spawning of the actual entity is done, the contents (components, children etc) of the Blueprint will have been merged with those of the entity itself.
> 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((
BlueprintInfo::from_path("Health_Pickup.glb"),
SpawnBlueprint,
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((
BlueprintInfo::from_path("Health_Pickup.glb"),
SpawnBlueprint,
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 ```BluePrintBundle``` for convenience , which just has
* a ```BlueprintInfo``` component
* a ```SpawnBlueprint``` component
## Additional information
- When a blueprint is spawned, an ```FromBlueprint``` component is inserted into all its children entities (and nested children etc)
- this crate also provides a special optional ```GameWorldTag``` component: this is useful when you want to keep all your spawned entities inside a root entity
You can use it in your queries to add your entities as children of this "world"
This way all your levels, your dynamic entities etc, are kept seperated from UI nodes & other entities that are not relevant to the game world
> Note: you should only have a SINGLE entity tagged with that component !
```rust no_run
commands.spawn((
BlueprintInfo::from_path("levels/World.glb"),
SpawnBlueprint,
HideUntilReady,
GameWorldTag, // here it is
));
```
## Registry
Blenvy automatically exports a Json file containing of all your registered components/ types, in order to create UIs that allows you to add & edit your components directly in Blender in the [Blenvy](https://github.com/kaosat-dev/Blenvy/tree/main/tools/blenvy) Blender add-on
- The output file will be generated in the ```Startup``` schedule whenever you run your app.
- Every time you compile & run your app, the output json file will be updated.
## Materials
Ff you enable it on the blender side, Blenvy will be using "material libraries" to share common textures/materials between blueprints, in order to avoid asset & memory bloat:
Ie for example without this option, 56 different blueprints using the same material with a large texture would lead to the material/texture being embeded
56 times !!
Generating optimised blueprints and material libraries can be automated using the [Blender plugin](https://github.com/kaosat-dev/Blenvy/tree/main/tools/blenvy)
## Animation
```blenvy``` provides some lightweight helpers to deal with animations stored in gltf files
It has both support for blueprint level animations (shared by all blueprint instance of the same blueprint)
* an ```BlueprintAnimations``` component that gets inserted into spawned (root) entities that contains a hashmap of all animations contained inside that entity/gltf file .
* an ```BlueprintAnimationPlayerLink``` component that gets inserted into spawned (root) entities, to make it easier to find Bevy's ```AnimationPlayer``` and ```AnimationTransitions``` components
And instance level animations (specific to one instance)
* an ```InstanceAnimations``` component that gets inserted into spawned (root) entities that contains a hashmap of all animations **specific to that instance** .
* an ```InstanceAnimationPlayerLink``` component that gets inserted into spawned (root) entities, to make it easier to find Bevy's ```AnimationPlayer``` and ```AnimationTransitions``` components for the animations above
The workflow for animations is as follows:
* create a gltf file with animations (using Blender & co) as you would normally do
* inside Bevy, use the ```blenvy``` boilerplate (see sections above), no specific setup beyond that is required
* to control the animation of an entity, you need to query for entities that have both ```BlueprintAnimationPlayerLink``` and ```BlueprintAnimations``` components (added by ```blenvy```) AND entities with the ```AnimationPlayer``` component
For example (blueprint animations):
```rust no_run
pub fn trigger_blueprint_animations(
animated_foxes: Query<(&BlueprintAnimationPlayerLink, &BlueprintAnimations), With<Fox>>,
mut animation_players: Query<(&mut AnimationPlayer, &mut AnimationTransitions)>,
keycode: Res<ButtonInput<KeyCode>>,
){
if keycode.just_pressed(KeyCode::KeyW) {
for (link, animations) in animated_foxes.iter() {
let (mut animation_player, mut animation_transitions) =
animation_players.get_mut(link.0).unwrap();
let anim_name = "Walk";
animation_transitions
.play(
&mut animation_player,
animations
.named_indices
.get(anim_name)
.expect("animation name should be in the list")
.clone(),
Duration::from_secs(5),
)
.repeat();
}
}
}
```
see https://github.com/kaosat-dev/Blenvy/tree/main/examples/blenvy/animation for how to set it up correctly
## Additional features
this crate also includes automatic handling of lights in gltf files, to attempt to match Blender's eevee rendering as close as possible:
* **BlenderLightShadows** (automatically generated by the gltf_auto_export Blender add-on) allows you to toggle light's shadows on/off in Blender and have matching
behaviour in Bevy
* **BlenderBackgroundShader** aka background color is also automatically set on the Bevy side
* **BlenderShadowSettings** sets the cascade_size on the bevy side to match the one configured in Blender
## Examples
https://github.com/kaosat-dev/Blenvy/tree/main/examples/blenvy/components
https://github.com/kaosat-dev/Blenvy/tree/main/examples/blenvy/blueprints
https://github.com/kaosat-dev/Blenvy/tree/main/examples/blenvy/animation
https://github.com/kaosat-dev/Blenvy/tree/main/examples/blenvy/save_load
https://github.com/kaosat-dev/Blenvy/tree/main/examples/blenvy/demo (a full fledged demo)
## Compatible Bevy versions
The main branch is compatible with the latest Bevy release, while the branch `bevy_main` tries to track the `main` branch of Bevy (PRs updating the tracked commit are welcome).
Compatibility of `blenvy` versions:
| `blenvy` | `bevy` |
| :-- | :-- |
| `0.1.0-alpha.1` | `0.14` |
| branch `main` | `0.14` |
| branch `bevy_main` | `main` |
## 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

@ -1,14 +1,15 @@
use bevy::{math::Vec3A, prelude::*, render::primitives::Aabb};
use crate::{BluePrintsConfig, Spawned};
use crate::{BlenvyConfig, BlueprintReadyForFinalizing, BlueprintReadyForPostProcess};
/// helper system that computes the compound aabbs of the scenes/blueprints
pub fn compute_scene_aabbs(
root_entities: Query<(Entity, &Name), (With<Spawned>, Without<Aabb>)>,
root_entities: Query<(Entity, &Name), (With<BlueprintReadyForPostProcess>, Without<Aabb>)>,
other_entities: Query<Entity, (With<BlueprintReadyForPostProcess>, With<Aabb>)>,
children: Query<&Children>,
existing_aabbs: Query<&Aabb>,
mut blueprints_config: ResMut<BluePrintsConfig>,
mut blenvy_config: ResMut<BlenvyConfig>,
mut commands: Commands,
) {
// compute compound aabb
@ -16,18 +17,28 @@ pub fn compute_scene_aabbs(
// info!("generating aabb for {:?}", name);
// only recompute aabb if it has not already been done before
if blueprints_config.aabb_cache.contains_key(&name.to_string()) {
let aabb = blueprints_config
if blenvy_config.aabb_cache.contains_key(&name.to_string()) {
let aabb = blenvy_config
.aabb_cache
.get(&name.to_string())
.expect("we should have the aabb available");
commands.entity(root_entity).insert(*aabb);
commands
.entity(root_entity)
.insert(*aabb)
.insert(BlueprintReadyForFinalizing);
} else {
let aabb = compute_descendant_aabb(root_entity, &children, &existing_aabbs);
commands.entity(root_entity).insert(aabb);
blueprints_config.aabb_cache.insert(name.to_string(), aabb);
blenvy_config.aabb_cache.insert(name.to_string(), aabb);
info!("Step 7: generating aabb for {:?}", name);
commands
.entity(root_entity)
.insert(aabb)
.insert(BlueprintReadyForFinalizing);
}
}
for entity in other_entities.iter() {
commands.entity(entity).insert(BlueprintReadyForFinalizing);
}
}
pub fn compute_descendant_aabb(

View File

@ -0,0 +1,253 @@
use bevy::prelude::*;
use bevy::utils::HashMap;
#[derive(Component, Reflect, Default, Debug)]
#[reflect(Component)]
/// storage for animations for a given entity's BLUEPRINT (ie for example a characters animations)
pub struct BlueprintAnimations {
pub named_animations: HashMap<String, Handle<AnimationClip>>,
pub named_indices: HashMap<String, AnimationNodeIndex>,
pub graph: Handle<AnimationGraph>,
}
#[derive(Component, Debug)]
/// Stop gap helper component : this is inserted into a "root" entity (an entity representing a whole gltf file)
/// so that the root entity knows which of its children contains an actualy `AnimationPlayer` component
/// this is for convenience, because currently , Bevy's gltf parsing inserts `AnimationPlayers` "one level down"
/// ie armature/root for animated models, which means more complex queries to trigger animations that we want to avoid
pub struct BlueprintAnimationPlayerLink(pub Entity);
#[derive(Component, Debug)]
/// Same as the above but for `AnimationInfos` components which get added (on the Blender side) to the entities that actually have the animations
/// which often is not the Blueprint or blueprint instance entity itself.
pub struct BlueprintAnimationInfosLink(pub Entity);
#[derive(Component, Reflect, Default, Debug)]
#[reflect(Component)]
/// storage for per instance / scene level animations for a given entity (hierarchy)
pub struct InstanceAnimations {
pub named_animations: HashMap<String, Handle<AnimationClip>>,
pub named_indices: HashMap<String, AnimationNodeIndex>,
pub graph: Handle<AnimationGraph>,
}
#[derive(Component, Debug)]
/// Stop gap helper component : this is inserted into a "root" entity (an entity representing a whole gltf file)
/// so that the root entity knows which of its children contains an actualy `AnimationPlayer` component
/// this is for convenience, because currently , Bevy's gltf parsing inserts `AnimationPlayers` "one level down"
/// ie armature/root for animated models, which means more complex queries to trigger animations that we want to avoid
pub struct InstanceAnimationPlayerLink(pub Entity);
#[derive(Component, Debug)]
/// Same as the above but for scene's `AnimationInfos` components which get added (on the Blender side) to the entities that actually have the animations
/// which often is not the Blueprint or blueprint instance entity itself.
pub struct InstanceAnimationInfosLink(pub Entity);
/// Stores Animation information: name, frame informations etc
#[derive(Reflect, Default, Debug)]
pub struct AnimationInfo {
pub name: String,
pub frame_start: f32,
pub frame_end: f32,
pub frames_length: f32,
pub frame_start_override: f32,
pub frame_end_override: f32,
}
/// Stores information about animations, to make things a bit easier api wise:
/// these components are automatically inserted by the `blenvy` Blender add-on on entities that have animations
#[derive(Component, Reflect, Default, Debug)]
#[reflect(Component)]
pub struct AnimationInfos {
pub animations: Vec<AnimationInfo>,
}
#[derive(Reflect, Default, Debug)]
pub struct AnimationMarker {
// pub frame: u32,
pub name: String,
pub handled_for_cycle: bool,
}
/// Stores information about animation markers: practical for adding things like triggering events at specific keyframes etc
/// it is essentiall a hashmap of `AnimationName` => `HashMap`<`FrameNumber`, Vec of marker names>
#[derive(Component, Reflect, Default, Debug)]
#[reflect(Component)]
pub struct AnimationMarkers(pub HashMap<String, HashMap<u32, Vec<String>>>);
/// Event that gets triggered once a specific marker inside an animation has been reached (frame based)
/// Provides some usefull information about which entity , wich animation, wich frame & which marker got triggered
#[derive(Event, Debug)]
pub struct AnimationMarkerReached {
pub entity: Entity,
pub animation_name: String,
pub frame: u32,
pub marker_name: String,
}
/////////////////////
/// triggers events when a given animation marker is reached for BLUEPRINT animations
pub fn trigger_blueprint_animation_markers_events(
animation_data: Query<(
Entity,
&BlueprintAnimationPlayerLink,
&BlueprintAnimationInfosLink,
&BlueprintAnimations,
)>,
// FIXME: annoying hiearchy issue yet again: the Markers & AnimationInfos are stored INSIDE the blueprint, so we need to access them differently
animation_infos: Query<(&AnimationInfos, &AnimationMarkers)>,
animation_players: Query<&AnimationPlayer>,
mut animation_marker_events: EventWriter<AnimationMarkerReached>,
animation_clips: Res<Assets<AnimationClip>>,
) {
for (entity, player_link, infos_link, animations) in animation_data.iter() {
for (animation_name, node_index) in animations.named_indices.iter() {
let animation_player = animation_players.get(player_link.0).unwrap();
let (animation_infos, animation_markers) = animation_infos.get(infos_link.0).unwrap();
if animation_player.animation_is_playing(*node_index) {
if let Some(animation) = animation_player.animation(*node_index) {
// animation.speed()
// animation.completions()
if let Some(animation_clip_handle) =
animations.named_animations.get(animation_name)
{
if let Some(animation_clip) = animation_clips.get(animation_clip_handle) {
let animation_length_seconds = animation_clip.duration();
let animation_length_frames =
animation_infos // FIXME: horribly inneficient
.animations
.iter()
.find(|anim| &anim.name == animation_name)
.unwrap()
.frames_length;
// TODO: we also need to take playback speed into account
let time_in_animation = animation.elapsed()
- (animation.completions() as f32) * animation_length_seconds;
let frame_seconds = (animation_length_frames
/ animation_length_seconds)
* time_in_animation;
// println!("frame seconds {}", frame_seconds);
let frame = frame_seconds.ceil() as u32; // FIXME , bad hack
let matching_animation_marker = &animation_markers.0[animation_name];
if matching_animation_marker.contains_key(&frame) {
let matching_markers_per_frame =
matching_animation_marker.get(&frame).unwrap();
println!(
"FOUND A MARKER {:?} at frame {}",
matching_markers_per_frame, frame
);
// FIXME: complete hack-ish solution , otherwise this can fire multiple times in a row, depending on animation length , speed , etc
let diff = frame as f32 - frame_seconds;
if diff < 0.1 {
for marker_name in matching_markers_per_frame {
animation_marker_events.send(AnimationMarkerReached {
entity,
animation_name: animation_name.clone(),
frame,
marker_name: marker_name.clone(),
});
}
}
}
}
}
}
}
}
}
}
/// triggers events when a given animation marker is reached for INSTANCE animations
// TODO: implement this correctly
pub fn trigger_instance_animation_markers_events(
animation_infos: Query<(
Entity,
&AnimationMarkers,
&InstanceAnimationPlayerLink,
&InstanceAnimations,
&AnimationInfos,
)>,
animation_players: Query<&AnimationPlayer>,
animation_clips: Res<Assets<AnimationClip>>,
__animation_graphs: Res<Assets<AnimationGraph>>,
mut _animation_marker_events: EventWriter<AnimationMarkerReached>,
) {
for (__entity, __markers, player_link, animations, __animation_infos) in animation_infos.iter()
{
//let (animation_player, animation_transitions) = animation_players.get(player_link.0).unwrap();
//let foo = animation_transitions.get_main_animation().unwrap();
for (animation_name, node_index) in animations.named_indices.iter() {
let animation_player = animation_players.get(player_link.0).unwrap();
if animation_player.animation_is_playing(*node_index) {
if let Some(__animation) = animation_player.animation(*node_index) {
if let Some(animation_clip_handle) =
animations.named_animations.get(animation_name)
{
if let Some(__animation_clip) = animation_clips.get(animation_clip_handle) {
println!("found the animation clip");
}
}
}
}
}
/*let animation_clip = animation_clips.get(animation_player.animation_clip());
// animation_player.play(animation)
if animation_clip.is_some() {
// println!("Entity {:?} markers {:?}", entity, markers);
// println!("Player {:?} {}", animation_player.elapsed(), animation_player.completions());
// FIMXE: yikes ! very inneficient ! perhaps add boilerplate to the "start playing animation" code so we know what is playing
let animation_name = animations.named_animations.iter().find_map(|(key, value)| {
if value == animation_player.animation_clip() {
Some(key)
} else {
None
}
});
if animation_name.is_some() {
let animation_name = animation_name.unwrap();
let animation_length_seconds = animation_clip.unwrap().duration();
let animation_length_frames = animation_infos
.animations
.iter()
.find(|anim| &anim.name == animation_name)
.unwrap()
.frames_length;
// TODO: we also need to take playback speed into account
let time_in_animation = animation_player.elapsed()
- (animation_player.completions() as f32) * animation_length_seconds;
let frame_seconds =
(animation_length_frames / animation_length_seconds) * time_in_animation;
let frame = frame_seconds as u32;
let matching_animation_marker = &markers.0[animation_name];
if matching_animation_marker.contains_key(&frame) {
let matching_markers_per_frame = matching_animation_marker.get(&frame).unwrap();
// let timediff = animation_length_seconds - time_in_animation;
// println!("timediff {}", timediff);
// println!("FOUND A MARKER {:?} at frame {}", matching_markers_per_frame, frame);
// emit an event AnimationMarkerReached(entity, animation_name, frame, marker_name)
// FIXME: problem, this can fire multiple times in a row, depending on animation length , speed , etc
for marker in matching_markers_per_frame {
animation_marker_events.send(AnimationMarkerReached {
entity,
animation_name: animation_name.clone(),
frame,
marker_name: marker.clone(),
});
}
}
}
}*/
}
}

View File

@ -0,0 +1,88 @@
use bevy::{asset::LoadedUntypedAsset, prelude::*};
use serde::Deserialize;
/// helper component, is used to store the list of sub blueprints to enable automatic loading of dependend blueprints
#[derive(Component, Reflect, Default, Debug, Deserialize)]
#[reflect(Component)]
pub struct BlueprintAsset {
pub name: String,
pub path: String,
}
/// helper component, is used to store the list of sub blueprints to enable automatic loading of dependend blueprints
/// these are only the DIRECT dependencies of a blueprint, does not contain the indirect assets (ie assets of sub blueprints, etc)
#[derive(Component, Reflect, Default, Debug, Deserialize)]
#[reflect(Component)]
pub struct BlueprintAssets {
/// only this field should get filled in from the Blender side
pub assets: Vec<BlueprintAsset>,
/// set to default when deserializing
#[serde(default)]
#[reflect(default)]
pub loaded: bool,
/// set to default when deserializing
#[serde(default)]
#[reflect(default)]
pub progress: f32,
#[reflect(ignore)]
#[serde(skip)]
pub asset_infos: Vec<AssetLoadTracker>,
}
//(pub Vec<BlueprintAsset>);
/// helper component, is used to store the list of sub blueprints to enable automatic loading of dependend blueprints
#[derive(Component, Reflect, Default, Debug, Deserialize)]
pub struct BlueprintAllAssets {
/// only this field should get filled in from the Blender side
pub assets: Vec<BlueprintAsset>,
}
////////////////////////
///
/// flag component, usually added when a blueprint is loaded
#[derive(Component)]
pub(crate) struct BlueprintAssetsLoaded;
/// flag component
#[derive(Component)]
pub(crate) struct BlueprintAssetsNotLoaded;
/// helper component, for tracking loaded assets's loading state, id , handle etc
#[derive(Debug, Reflect)]
pub struct AssetLoadTracker {
#[allow(dead_code)]
pub name: String,
pub path: String,
pub id: AssetId<LoadedUntypedAsset>,
pub loaded: bool,
#[allow(dead_code)]
pub handle: Handle<LoadedUntypedAsset>,
}
/// helper component, for tracking loaded assets
#[derive(Component, Default, Debug)]
pub(crate) struct BlueprintAssetsLoadState {
pub all_loaded: bool,
pub asset_infos: Vec<AssetLoadTracker>,
pub progress: f32,
}
// for preloading asset files
#[derive(serde::Deserialize, bevy::asset::Asset, bevy::reflect::TypePath, Debug)]
pub(crate) struct File {
pub(crate) path: String,
}
#[derive(serde::Deserialize, bevy::asset::Asset, bevy::reflect::TypePath, Debug)]
pub(crate) struct BlueprintPreloadAssets {
pub(crate) assets: Vec<(String, File)>,
}
#[derive(Component)]
pub(crate) struct BlueprintMetaHandle(pub Handle<BlueprintPreloadAssets>);
/// flag component, usually added when a blueprint meta file is loaded
#[derive(Component)]
pub(crate) struct BlueprintMetaLoaded;
#[derive(Component)]
pub(crate) struct BlueprintMetaLoading;

View File

@ -1,4 +1,5 @@
use bevy::{ecs::world::Command, prelude::*};
use bevy::ecs::world::Command;
use bevy::prelude::*;
use std::any::TypeId;
// originally based https://github.com/bevyengine/bevy/issues/1515,
@ -68,10 +69,15 @@ impl CopyComponents {
}
})
.map(|type_id| {
return (
type_id.data::<ReflectComponent>().unwrap().clone(),
type_id.type_info().type_id(), // we need the original type_id down the line
);
match type_id.data::<ReflectComponent>() {
Some(data) => (
data.clone(),
type_id.type_info().type_id(), // we need the original type_id down the line
),
None => {
panic!("type {:?}'s ReflectComponent data was not found in the type registry, this could be because the type is missing a #[reflect(Component)]", type_id.type_info().type_path());
}
}
})
.collect::<Vec<_>>()
};

View File

@ -0,0 +1,108 @@
use crate::{
BlueprintAssetsLoadState, BlueprintAssetsLoaded, BlueprintInfo, BlueprintInstanceReady,
BlueprintSpawning, FromBlueprint, SpawnBlueprint, SubBlueprintsSpawnTracker,
};
use bevy::asset::AssetEvent;
use bevy::prelude::*;
use bevy::scene::SceneInstance;
use bevy::utils::hashbrown::HashMap;
/// Resource mapping asset paths (ideally untyped ids, but more complex) to a list of blueprint instance entity ids
#[derive(Debug, Clone, Resource, Default)]
pub(crate) struct AssetToBlueprintInstancesMapper {
// pub(crate) untyped_id_to_blueprint_entity_ids: HashMap<UntypedAssetId, Vec<Entity>>
pub(crate) untyped_id_to_blueprint_entity_ids: HashMap<String, Vec<Entity>>,
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn react_to_asset_changes(
mut gltf_events: EventReader<AssetEvent<Gltf>>, // FIXME: Problem: we need to react to any asset change, not just gltf files !
// mut untyped_events: EventReader<AssetEvent<LoadedUntypedAsset>>,
blueprint_assets: Query<(Entity, Option<&Name>, &BlueprintInfo, Option<&Children>)>,
_blueprint_children_entities: Query<&FromBlueprint>, //=> can only be used if the entites are tagged
assets_to_blueprint_instances: Res<AssetToBlueprintInstancesMapper>,
all_parents: Query<&Parent>,
spawning_blueprints: Query<&BlueprintSpawning>,
asset_server: Res<AssetServer>,
mut commands: Commands,
) {
let mut respawn_candidates: Vec<&Entity> = vec![];
for event in gltf_events.read() {
// LoadedUntypedAsset
if let AssetEvent::Modified { id } = event {
// React to the gltf file being modified
// println!("Modified gltf {:?}", asset_server.get_path(*id));
if let Some(asset_path) = asset_server.get_path(*id) {
// let untyped = asset_server.get_handle_untyped(asset_path.clone());
// println!("matching untyped handle {:?}", untyped);
// let bla = untyped.unwrap().id();
// asset_server.get
// in order to avoid respawn both a parent & a child , which would crash Bevy, we do things in two steps
if let Some(entities) = assets_to_blueprint_instances
.untyped_id_to_blueprint_entity_ids
.get(&asset_path.to_string())
{
for entity in entities.iter() {
// println!("matching blueprint instance {}", entity);
// disregard entities that are already (re) spawning
if !respawn_candidates.contains(&entity)
&& blueprint_assets.get(*entity).is_ok()
&& spawning_blueprints.get(*entity).is_err()
{
respawn_candidates.push(entity);
}
}
}
}
}
}
// we process all candidates here to deal with the case where multiple assets have changed in a single frame, which could cause respawn chaos
// now find hierarchy of changes and only set the uppermost parent up for respawning
// TODO: improve this, very inneficient
let mut retained_candidates: Vec<Entity> = vec![];
'outer: for entity in respawn_candidates.iter() {
for parent in all_parents.iter_ancestors(**entity) {
for ent in respawn_candidates.iter() {
if **ent == parent {
if !retained_candidates.contains(&parent) {
retained_candidates.push(parent);
}
continue 'outer;
}
}
}
if !retained_candidates.contains(entity) {
retained_candidates.push(**entity);
}
}
// println!("respawn candidates {:?}", respawn_candidates);
for retained in retained_candidates.iter() {
// println!("retained {}", retained);
if let Ok((entity, entity_name, _blueprint_info, children)) =
blueprint_assets.get(*retained)
{
info!("Change detected !!, now respawn {:?}", entity_name);
// TODO: only remove those that are "in blueprint"
if children.is_some() {
for child in children.unwrap().iter() {
commands.entity(*child).despawn_recursive();
}
}
commands
.entity(entity)
.remove::<BlueprintInstanceReady>()
.remove::<BlueprintAssetsLoaded>()
.remove::<SceneInstance>()
.remove::<BlueprintAssetsLoadState>()
.remove::<SubBlueprintsSpawnTracker>()
.insert(SpawnBlueprint);
}
}
// println!("done with asset updates");
}

View File

@ -0,0 +1,106 @@
use bevy::prelude::*;
use crate::BlenvyConfig;
#[derive(Reflect, Default, Debug)]
/// struct containing the name & path of the material to apply
pub struct MaterialInfo {
pub name: String,
pub path: String,
}
#[derive(Component, Reflect, Default, Debug)]
#[reflect(Component)]
/// component containing the full list of `MaterialInfos` for a given entity/object
pub struct MaterialInfos(Vec<MaterialInfo>);
#[derive(Component, Default, Debug)]
pub struct MaterialProcessed;
/// system that injects / replaces materials from materials library
pub(crate) fn inject_materials(
mut blenvy_config: ResMut<BlenvyConfig>,
material_infos_query: Query<
(Entity, &MaterialInfos, &Children),
Without<MaterialProcessed>, // (With<BlueprintReadyForPostProcess>)
/*(
Added<BlueprintMaterialAssetsLoaded>,
With<BlueprintMaterialAssetsLoaded>,
),*/
>,
with_materials_and_meshes: Query<
(),
(
With<Parent>,
With<Handle<StandardMaterial>>,
With<Handle<Mesh>>,
),
>,
assets_gltf: Res<Assets<Gltf>>,
asset_server: Res<AssetServer>,
mut commands: Commands,
) {
for (entity, material_infos, children) in material_infos_query.iter() {
for (material_index, material_info) in material_infos.0.iter().enumerate() {
let material_full_path = format!("{}#{}", material_info.path, material_info.name);
let mut material_found: Option<&Handle<StandardMaterial>> = None;
if blenvy_config
.materials_cache
.contains_key(&material_full_path)
{
debug!("material is cached, retrieving");
let material = blenvy_config
.materials_cache
.get(&material_full_path)
.expect("we should have the material available");
material_found = Some(material);
} else {
let model_handle: Handle<Gltf> = asset_server.load(material_info.path.clone()); // FIXME: kinda weird now
let Some(mat_gltf) = assets_gltf.get(model_handle.id()) else {
warn!(
"materials file {} should have been preloaded skipping",
material_info.path
);
continue;
};
/*let mat_gltf = assets_gltf.get(model_handle.id()).unwrap_or_else(|| {
panic!(
"materials file {} should have been preloaded",
material_info.path
)
});*/
if mat_gltf
.named_materials
.contains_key(&material_info.name as &str)
{
let material = mat_gltf
.named_materials
.get(&material_info.name as &str)
.expect("this material should have been loaded at this stage, please make sure you are correctly preloading them");
blenvy_config
.materials_cache
.insert(material_full_path, material.clone());
material_found = Some(material);
}
}
if let Some(material) = material_found {
info!("Step 6: injecting/replacing materials");
for (child_index, child) in children.iter().enumerate() {
if child_index == material_index && with_materials_and_meshes.contains(*child) {
info!(
"injecting material {}, path: {:?}",
material_info.name,
material_info.path.clone()
);
commands.entity(*child).insert(material.clone());
}
}
}
}
commands.entity(entity).insert(MaterialProcessed);
}
}

View File

@ -0,0 +1,143 @@
pub mod spawn_from_blueprints;
use bevy_common_assets::ron::RonAssetPlugin;
pub use spawn_from_blueprints::*;
pub mod animation;
pub use animation::*;
pub mod aabb;
pub use aabb::*;
pub mod assets;
pub use assets::*;
pub mod materials;
pub use materials::*;
pub mod copy_components;
pub use copy_components::*;
pub(crate) mod hot_reload;
pub(crate) use hot_reload::*;
use bevy::{prelude::*, utils::hashbrown::HashMap};
use crate::GltfComponentsSet;
#[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: BlueprintInfo,
pub spawn_here: SpawnBlueprint,
}
impl Default for BluePrintBundle {
fn default() -> Self {
BluePrintBundle {
blueprint: BlueprintInfo {
name: "default".into(),
path: "".into(),
},
spawn_here: SpawnBlueprint,
}
}
}
#[derive(Debug, Default, Clone)]
/// Plugin for gltf blueprints
pub struct BlueprintsPlugin {}
fn hot_reload(watching_for_changes: Res<WatchingForChanges>) -> bool {
// println!("hot reload ? {}", watching_for_changes.0);
watching_for_changes.0
}
trait BlenvyApp {
fn register_watching_for_changes(&mut self) -> &mut Self;
}
impl BlenvyApp for App {
fn register_watching_for_changes(&mut self) -> &mut Self {
let asset_server = self
.world()
.get_resource::<AssetServer>()
.expect(ASSET_ERROR);
let watching_for_changes = asset_server.watching_for_changes();
self.insert_resource(WatchingForChanges(watching_for_changes))
}
}
#[derive(Debug, Clone, Resource, Default)]
pub(crate) struct WatchingForChanges(pub(crate) bool);
const ASSET_ERROR: &str = ""; // TODO
impl Plugin for BlueprintsPlugin {
fn build(&self, app: &mut App) {
app.register_watching_for_changes()
.insert_resource(AssetToBlueprintInstancesMapper {
untyped_id_to_blueprint_entity_ids: HashMap::new(),
})
.add_event::<BlueprintEvent>()
.register_type::<BlueprintInfo>()
.register_type::<MaterialInfo>()
.register_type::<MaterialInfos>()
.register_type::<SpawnBlueprint>()
.register_type::<BlueprintInstanceDisabled>()
.register_type::<HideUntilReady>()
.register_type::<BlueprintAnimations>()
.register_type::<InstanceAnimations>()
.register_type::<AnimationInfo>()
.register_type::<AnimationInfos>()
.register_type::<Vec<AnimationInfo>>()
.register_type::<AnimationMarkers>()
.register_type::<HashMap<u32, Vec<String>>>()
.register_type::<HashMap<String, HashMap<u32, Vec<String>>>>()
.add_event::<AnimationMarkerReached>()
.register_type::<BlueprintAsset>()
.register_type::<Vec<BlueprintAsset>>()
.register_type::<Vec<String>>()
.register_type::<BlueprintAssets>()
.register_type::<HashMap<String, Vec<String>>>()
.add_plugins(RonAssetPlugin::<BlueprintPreloadAssets>::new(&["meta.ron"]))
.configure_sets(
Update,
(GltfBlueprintsSet::Spawn, GltfBlueprintsSet::AfterSpawn)
.chain()
.after(GltfComponentsSet::Injection),
)
.add_systems(
Update,
(
blueprints_prepare_metadata_file_for_spawn,
blueprints_check_assets_metadata_files_loading,
blueprints_prepare_spawn,
blueprints_check_assets_loading,
blueprints_assets_loaded,
blueprints_scenes_spawned,
blueprints_cleanup_spawned_scene,
// beyond this point : post processing to finalize blueprint instances
inject_materials,
compute_scene_aabbs,
blueprints_finalize_instances,
)
.chain()
.in_set(GltfBlueprintsSet::Spawn),
)
// animation
.add_systems(
Update,
(
trigger_blueprint_animation_markers_events,
trigger_instance_animation_markers_events,
),
)
// hot reload
.add_systems(Update, react_to_asset_changes.run_if(hot_reload));
}
}

View File

@ -0,0 +1,904 @@
use std::path::Path;
use bevy::{gltf::Gltf, prelude::*, scene::SceneInstance, utils::hashbrown::HashMap};
use crate::{
AnimationInfos, AssetLoadTracker, AssetToBlueprintInstancesMapper, BlueprintAnimationInfosLink,
BlueprintAnimationPlayerLink, BlueprintAnimations, BlueprintAssetsLoadState,
BlueprintAssetsLoaded, BlueprintAssetsNotLoaded, BlueprintMetaLoaded, BlueprintMetaLoading,
BlueprintPreloadAssets, InstanceAnimationInfosLink, InstanceAnimationPlayerLink,
InstanceAnimations, WatchingForChanges,
};
/// this is a flag component for our levels/game world
#[derive(Component)]
pub struct GameWorldTag;
/// Main component for the blueprints
/// has both name & path of the blueprint to enable injecting the data from the correct blueprint
/// into the entity that contains this component
#[derive(Component, Reflect, Default, Debug)]
#[reflect(Component)]
pub struct BlueprintInfo {
pub name: String,
pub path: String,
}
impl BlueprintInfo {
pub fn from_path(path: &str) -> BlueprintInfo {
let p = Path::new(&path);
return BlueprintInfo {
name: p.file_stem().unwrap().to_os_string().into_string().unwrap(), // seriously ? , also unwraps !!
path: path.into(),
};
}
}
/// flag component needed to signify the intent to spawn a Blueprint
#[derive(Component, Reflect, Default, Debug)]
#[reflect(Component)]
pub struct SpawnBlueprint;
#[derive(Component, Reflect, Default, Debug)]
#[reflect(Component)]
/// flag component marking any spawned child of blueprints
pub struct FromBlueprint;
#[derive(Component, Reflect, Default, Debug)]
#[reflect(Component)]
/// flag component to force adding newly spawned entity as child of game world
pub struct AddToGameWorld;
#[derive(Component)]
/// helper component, just to transfer child data
pub(crate) struct OriginalChildren(pub Vec<Entity>);
#[derive(Component, Reflect, Default, Debug)]
#[reflect(Component)]
/// You can add this component to a blueprint instance, and the instance will be hidden until it is ready
/// You usually want to use this for worlds/level spawning , or dynamic spawning at runtime, but not when you are adding blueprint instances to an existing entity
/// as it would first become invisible before re-appearing again
pub struct HideUntilReady;
#[derive(Component, Reflect, Default, Debug)]
#[reflect(Component)]
/// Companion to the `HideUntilReady` component: this stores the visibility of the entity before the blueprint was inserted into it
pub(crate) struct OriginalVisibility(Visibility);
#[derive(Component, Reflect, Default, Debug)]
#[reflect(Component)]
/// marker component, gets added to all children of a currently spawning blueprint instance, can be usefull to avoid manipulating still in progress entities
pub struct BlueprintInstanceDisabled;
#[derive(Event, Debug)]
pub enum BlueprintEvent {
/// event fired when a blueprint instance has finished loading all of its assets & before it attempts spawning
AssetsLoaded {
entity: Entity,
blueprint_name: String,
blueprint_path: String,
// TODO: add assets list ?
},
/// event fired when a blueprint instance has completely finished spawning, ie
/// - all its assests have been loaded
/// - all of its child blueprint instances are ready
/// - all the post processing is finished (aabb calculation, material replacements etc)
InstanceReady {
entity: Entity,
blueprint_name: String,
blueprint_path: String,
},
}
#[derive(Component, Reflect, Debug, Default)]
#[reflect(Component)]
/// component gets added when a blueprint starts spawning, removed when spawning is completely done
pub struct BlueprintSpawning;
/*
Overview of the Blueprint Spawning process
- Blueprint Load Assets
- Blueprint Assets Ready: spawn Blueprint's scene
- Blueprint Scene Ready (SceneInstance component is present):
- get list of sub Blueprints if any, inject sub blueprints spawn tracker
- Blueprint copy components to original entity, remove useless nodes
- Blueprint post process
- generate aabb (need full hierarchy in its final form)
- inject materials from library if needed
- Blueprint Ready
- bubble information up to parent blueprint instance
- if all sub_blueprints are ready => Parent blueprint Instance is ready
=> distinguish between blueprint instances inside blueprint instances vs blueprint instances inside blueprints ??
*/
pub(super) fn blueprints_prepare_metadata_file_for_spawn(
blueprint_instances_to_spawn: Query<
(
Entity,
&BlueprintInfo,
Option<&Name>,
Option<&Parent>,
Option<&HideUntilReady>,
Option<&Visibility>,
Option<&AddToGameWorld>,
),
(
Without<BlueprintMetaLoading>,
Without<BlueprintSpawning>,
Without<BlueprintInstanceReady>,
),
>,
mut game_world: Query<Entity, With<GameWorldTag>>,
asset_server: Res<AssetServer>,
mut commands: Commands,
) {
for (
entity,
blueprint_info,
entity_name,
original_parent,
hide_until_ready,
original_visibility,
add_to_world,
) in blueprint_instances_to_spawn.iter()
{
// get path to assets / metadata file
info!(
"Step 1: spawn request detected: loading metadata file for {:?}",
blueprint_info
);
let blueprint_path = blueprint_info.path.clone();
let metadata_path = blueprint_path
.replace(".glb", ".meta.ron")
.replace(".gltf", ".meta.ron"); // FIXME: horrible
let mut asset_infos: Vec<AssetLoadTracker> = vec![];
//let foo_handle:Handle<BlueprintPreloadAssets> = asset_server.load(metadata_path);
let untyped_handle = asset_server.load_untyped(metadata_path.clone());
let asset_id = untyped_handle.id();
asset_infos.push(AssetLoadTracker {
name: metadata_path.clone(),
path: metadata_path.clone(),
id: asset_id,
loaded: false,
handle: untyped_handle.clone(),
});
// add the blueprint spawning marker & co
commands.entity(entity).insert((
BlueprintAssetsLoadState {
all_loaded: false,
asset_infos,
..Default::default()
},
BlueprintMetaLoading,
BlueprintSpawning,
));
// if the entity has no name, add one based on the blueprint's
if entity_name.is_none() {
commands
.entity(entity)
.insert(bevy::prelude::Name::from(blueprint_info.name.clone()));
}
if original_parent.is_none() {
// only allow hiding until ready when the entity does not have a parent (?)
if hide_until_ready.is_some() {
// if there is already a set visibility, save it for later
if let Some(original_visibility) = original_visibility {
commands
.entity(entity)
.insert(OriginalVisibility(*original_visibility));
}
// & now hide the instance until it is ready
commands.entity(entity).insert(Visibility::Hidden);
}
// only allow automatically adding a newly spawned blueprint instance to the "world", if the entity does not have a parent
if add_to_world.is_some() {
let world = game_world
.get_single_mut()
.expect("there should be a game world present");
commands.entity(world).add_child(entity);
}
}
}
}
// TODO: merge with other asset loading checker ?
pub(crate) fn blueprints_check_assets_metadata_files_loading(
mut blueprint_assets_to_load: Query<
(Entity, &BlueprintInfo, &mut BlueprintAssetsLoadState),
With<BlueprintMetaLoading>,
>,
asset_server: Res<AssetServer>,
mut commands: Commands,
) {
for (entity, _blueprint_info, mut assets_to_load) in blueprint_assets_to_load.iter_mut() {
let mut all_loaded = true;
let mut loaded_amount = 0;
let total = assets_to_load.asset_infos.len();
for tracker in assets_to_load.asset_infos.iter_mut() {
let asset_id = tracker.id;
let loaded = asset_server.is_loaded_with_dependencies(asset_id);
let mut failed = false;
if let bevy::asset::LoadState::Failed(_) = asset_server.load_state(asset_id) {
failed = true;
}
tracker.loaded = loaded || failed;
if loaded || failed {
loaded_amount += 1;
} else {
all_loaded = false;
}
if all_loaded {
commands
.entity(entity)
.insert(BlueprintMetaHandle(asset_server.load(tracker.path.clone())))
.remove::<BlueprintAssetsLoadState>();
break;
}
}
let progress: f32 = loaded_amount as f32 / total as f32;
assets_to_load.progress = progress;
// println!("LOADING: in progress for ALL assets of {:?} (instance of {}): {} ",entity_name, blueprint_info.path, progress * 100.0);
}
}
pub(super) fn blueprints_prepare_spawn(
blueprint_instances_to_spawn: Query<
(Entity, &BlueprintInfo, &BlueprintMetaHandle),
Added<BlueprintMetaHandle>,
>,
mut commands: Commands,
asset_server: Res<AssetServer>,
// for hot reload
watching_for_changes: Res<WatchingForChanges>,
mut assets_to_blueprint_instances: ResMut<AssetToBlueprintInstancesMapper>,
// for debug
// all_names: Query<&Name>
blueprint_metas: Res<Assets<BlueprintPreloadAssets>>,
) {
for (entity, blueprint_info, blueprint_meta_handle) in blueprint_instances_to_spawn.iter() {
info!(
"Step 2: metadata loaded: loading assets for {:?}",
blueprint_info,
);
// we add the asset of the blueprint itself
// TODO: add detection of already loaded data
let untyped_handle = asset_server.load_untyped(&blueprint_info.path);
let asset_id = untyped_handle.id();
let loaded = asset_server.is_loaded_with_dependencies(asset_id);
let mut asset_infos: Vec<AssetLoadTracker> = vec![];
if !loaded {
asset_infos.push(AssetLoadTracker {
name: blueprint_info.name.clone(),
path: blueprint_info.path.clone(),
id: asset_id,
loaded: false,
handle: untyped_handle.clone(),
});
}
// and we also add all its assets
/* prefetch attempt */
if let Some(blenvy_metadata) = blueprint_metas.get(&blueprint_meta_handle.0) {
for asset in blenvy_metadata.assets.iter() {
let asset_path = asset.1.path.clone();
let asset_name = asset.0.clone();
let untyped_handle = asset_server.load_untyped(&asset_path);
let asset_id = untyped_handle.id();
let loaded = asset_server.is_loaded_with_dependencies(asset_id);
if !loaded {
asset_infos.push(AssetLoadTracker {
name: asset_name.clone(),
path: asset_path.clone(),
id: asset_id,
loaded: false,
handle: untyped_handle.clone(),
});
}
// FIXME: dang, too early, asset server has not yet started loading yet
// let path_id = asset_server.get_path_id(&asset.path).expect("we should have alread checked for this asset");
let path_id = asset_path.clone();
// Only do this if hot reload is enabled
if watching_for_changes.0 {
if !assets_to_blueprint_instances
.untyped_id_to_blueprint_entity_ids
.contains_key(&path_id)
{
assets_to_blueprint_instances
.untyped_id_to_blueprint_entity_ids
.insert(path_id.clone(), vec![]);
}
// only insert if not already present in mapping
if !assets_to_blueprint_instances.untyped_id_to_blueprint_entity_ids[&path_id]
.contains(&entity)
{
// println!("adding mapping between {} and entity {:?}", path_id, all_names.get(entity));
assets_to_blueprint_instances
.untyped_id_to_blueprint_entity_ids
.get_mut(&path_id)
.unwrap()
.push(entity);
}
}
}
} else {
warn!("no asset metadata found for {}, please make sure to generate them using the Blender add-on, or preload your assets manually", blueprint_info.path);
}
// Only do this if hot reload is enabled
// TODO: should this be added to the list of "all assets" on the blender side instead
if watching_for_changes.0 {
// also add the root blueprint info to the list of hot reload items
if !assets_to_blueprint_instances
.untyped_id_to_blueprint_entity_ids
.contains_key(&blueprint_info.path)
{
assets_to_blueprint_instances
.untyped_id_to_blueprint_entity_ids
.insert(blueprint_info.path.clone(), vec![]);
}
// only insert if not already present in mapping
if !assets_to_blueprint_instances.untyped_id_to_blueprint_entity_ids
[&blueprint_info.path]
.contains(&entity)
{
// println!("adding mapping between {} and entity {:?}", path_id, all_names.get(entity));
assets_to_blueprint_instances
.untyped_id_to_blueprint_entity_ids
.get_mut(&blueprint_info.path)
.unwrap()
.push(entity);
}
}
// now insert load tracker
// if there are assets to load
if !asset_infos.is_empty() {
commands.entity(entity).insert((
BlueprintAssetsLoadState {
all_loaded: false,
asset_infos,
..Default::default()
},
BlueprintAssetsNotLoaded,
));
} else {
commands.entity(entity).insert(BlueprintAssetsLoaded);
}
commands
.entity(entity)
.insert(BlueprintMetaLoaded)
.remove::<BlueprintMetaLoading>()
.remove::<BlueprintMetaHandle>();
}
}
/// This system tracks & updates the loading state of all blueprints assets
pub(crate) fn blueprints_check_assets_loading(
mut blueprint_assets_to_load: Query<
(Entity, &BlueprintInfo, &mut BlueprintAssetsLoadState),
With<BlueprintAssetsNotLoaded>,
>,
asset_server: Res<AssetServer>,
mut commands: Commands,
mut blueprint_events: EventWriter<BlueprintEvent>,
) {
for (entity, blueprint_info, mut assets_to_load) in blueprint_assets_to_load.iter_mut() {
let mut all_loaded = true;
let mut loaded_amount = 0;
let total = assets_to_load.asset_infos.len();
for tracker in assets_to_load.asset_infos.iter_mut() {
let asset_id = tracker.id;
let loaded = asset_server.is_loaded_with_dependencies(asset_id);
if loaded {
debug!("LOADED {}", tracker.path.clone());
}
let mut failed = false;
if let bevy::asset::LoadState::Failed(_) = asset_server.load_state(asset_id) {
warn!("FAILED TO LOAD {}", tracker.path.clone());
failed = true;
}
tracker.loaded = loaded || failed;
if loaded || failed {
loaded_amount += 1;
} else {
all_loaded = false;
}
}
let progress: f32 = loaded_amount as f32 / total as f32;
assets_to_load.progress = progress;
//println!("LOADING: in progress for ALL assets of {:?} (instance of {}): {} ",entity_name, blueprint_info.path, progress * 100.0);
if all_loaded {
assets_to_load.all_loaded = true;
// println!("LOADING: DONE for ALL assets of {:?} (instance of {}), preparing for spawn", entity_name, blueprint_info.path);
blueprint_events.send(BlueprintEvent::AssetsLoaded {
entity,
blueprint_name: blueprint_info.name.clone(),
blueprint_path: blueprint_info.path.clone(),
});
commands
.entity(entity)
.insert(BlueprintAssetsLoaded)
.remove::<BlueprintAssetsNotLoaded>();
}
}
}
pub(crate) fn blueprints_assets_loaded(
spawn_placeholders: Query<
(Entity, &BlueprintInfo, Option<&Transform>, Option<&Name>),
(
Added<BlueprintAssetsLoaded>,
Without<BlueprintAssetsNotLoaded>,
),
>,
all_children: Query<&Children>,
assets_gltf: Res<Assets<Gltf>>,
asset_server: Res<AssetServer>,
mut graphs: ResMut<Assets<AnimationGraph>>,
mut commands: Commands,
) {
for (entity, blueprint_info, transform, name) in spawn_placeholders.iter() {
/*info!(
"BLUEPRINT: all assets loaded, attempting to spawn blueprint SCENE {:?} for entity {:?}, id: {:}, parent:{:?}",
blueprint_info.name, name, entity, original_parent
);*/
info!(
"Step 3: all assets loaded, attempting to spawn blueprint scene {:?} for entity {:?}, id: {}",
blueprint_info, name, entity
);
// info!("attempting to spawn {:?}", model_path);
let model_handle: Handle<Gltf> = asset_server.load(blueprint_info.path.clone());
let blueprint_gltf = assets_gltf.get(&model_handle).unwrap_or_else(|| {
panic!(
"gltf file {:?} should have been loaded",
&blueprint_info.path
)
});
// 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 = blueprint_gltf
.named_scenes
.keys()
.next()
.expect("there should be at least one named scene in the gltf file to spawn");
let scene = &blueprint_gltf.named_scenes[main_scene_name];
// transforms are optional, but still deal with them correctly
let mut transforms: Transform = Transform::default();
if transform.is_some() {
transforms = *transform.unwrap();
}
let mut original_children: Vec<Entity> = vec![];
if let Ok(c) = all_children.get(entity) {
for child in c.iter() {
original_children.push(*child);
}
}
// TODO: not a fan of this
// prepare data for animations
let mut graph = AnimationGraph::new();
let mut named_animations: HashMap<String, Handle<AnimationClip>> = HashMap::new();
let mut named_indices: HashMap<String, AnimationNodeIndex> = HashMap::new();
for (key, clip) in blueprint_gltf.named_animations.iter() {
named_animations.insert(key.to_string(), clip.clone());
let animation_index = graph.add_clip(clip.clone(), 1.0, graph.root);
named_indices.insert(key.to_string(), animation_index);
}
let graph = graphs.add(graph);
//println!("Named animations : {:?}", named_animations.keys());
//println!("ANIMATION INFOS: {:?}", animation_infos);
commands.entity(entity).insert((
SceneBundle {
scene: scene.clone(),
transform: transforms,
..Default::default()
},
OriginalChildren(original_children),
BlueprintAnimations {
// TODO: perhaps swap this out with InstanceAnimations depending on whether we are spawning a level or a simple blueprint
// these are animations specific to the blueprint
named_animations,
named_indices,
graph,
},
));
}
}
#[derive(Component, Reflect, Debug, Default)]
#[reflect(Component)]
pub struct SubBlueprintsSpawnTracker {
pub sub_blueprint_instances: HashMap<Entity, bool>,
}
#[derive(Component, Reflect, Debug)]
#[reflect(Component)]
pub struct SubBlueprintSpawnRoot(pub Entity);
#[derive(Component, Reflect, Debug)]
#[reflect(Component)]
pub struct BlueprintSceneSpawned;
#[derive(Component, Reflect, Debug)]
#[reflect(Component)]
pub struct BlueprintChildrenReady;
pub(crate) fn blueprints_scenes_spawned(
spawned_blueprint_scene_instances: Query<
(
Entity,
Option<&Name>,
Option<&Children>,
Option<&SubBlueprintSpawnRoot>,
),
(With<BlueprintSpawning>, Added<SceneInstance>),
>,
with_blueprint_infos: Query<(Entity, Option<&Name>), With<BlueprintInfo>>,
all_children: Query<&Children>,
all_parents: Query<&Parent>,
// mut sub_blueprint_trackers: Query<(Entity, &mut SubBlueprintsSpawnTracker, &BlueprintInfo)>,
mut commands: Commands,
all_names: Query<&Name>,
) {
for (entity, name, children, track_root) in spawned_blueprint_scene_instances.iter() {
info!(
"Step 4: Done spawning blueprint scene for entity named {:?} (track root: {:?})",
name, track_root
);
let mut sub_blueprint_instances: Vec<Entity> = vec![];
let mut sub_blueprint_instance_names: Vec<Name> = vec![];
let mut tracker_data: HashMap<Entity, bool> = HashMap::new();
if track_root.is_none() {
for parent in all_parents.iter_ancestors(entity) {
if with_blueprint_infos.get(parent).is_ok() {
println!(
"found a parent with blueprint_info {:?} for {:?}",
all_names.get(parent),
all_names.get(entity)
);
commands
.entity(entity)
.insert(SubBlueprintSpawnRoot(parent)); // Injecting to know which entity is the root
break;
}
}
}
if children.is_some() {
for child in all_children.iter_descendants(entity) {
if with_blueprint_infos.get(child).is_ok() {
// println!("Parent blueprint instance of {:?} is {:?}", all_names.get(child), all_names.get(entity));
for parent in all_parents.iter_ancestors(child) {
if with_blueprint_infos.get(parent).is_ok() {
if parent == entity {
//println!("yohoho");
/*println!(
"Parent blueprint instance of {:?} is {:?}",
all_names.get(child),
all_names.get(parent)
);*/
commands.entity(child).insert(SubBlueprintSpawnRoot(entity)); // Injecting to know which entity is the root
tracker_data.insert(child, false);
sub_blueprint_instances.push(child);
if let Ok(nname) = all_names.get(child) {
sub_blueprint_instance_names.push(nname.clone());
}
/*if track_root.is_some() {
let prev_root = track_root.unwrap().0;
// if we already had a track root, and it is different from the current entity , change the previous track root's list of children
if prev_root != entity {
let mut tracker = sub_blueprint_trackers.get_mut(prev_root).expect("should have a tracker");
tracker.1.sub_blueprint_instances.remove(&child);
}
}*/
}
break;
}
}
}
// Mark all components as "Disabled" (until Bevy gets this as first class feature)
commands.entity(child).insert(BlueprintInstanceDisabled);
}
}
if tracker_data.keys().len() > 0 {
commands.entity(entity).insert(SubBlueprintsSpawnTracker {
sub_blueprint_instances: tracker_data.clone(),
});
} else {
commands.entity(entity).insert(BlueprintChildrenReady);
}
}
}
// could be done differently, by notifying each parent of a spawning blueprint that this child is done spawning ?
// perhaps using component hooks or observers (ie , if a ComponentSpawning + Parent)
use crate::CopyComponents;
use std::any::TypeId;
use super::BlueprintMetaHandle;
#[derive(Component, Reflect, Debug)]
#[reflect(Component)]
pub struct BlueprintReadyForPostProcess;
/// this system is in charge of doing component transfers & co
/// - it removes one level of useless nesting
/// - it copies the blueprint's root components to the entity it was spawned on (original entity)
/// - it copies the children of the blueprint scene into the original entity
/// - it adds an `AnimationLink` component containing the entity that has the `AnimationPlayer` so that animations can be controlled from the original entity
#[allow(clippy::too_many_arguments)]
pub(crate) fn blueprints_cleanup_spawned_scene(
blueprint_scenes: Query<
(
Entity,
&Children,
&OriginalChildren,
Option<&Name>,
&BlueprintAnimations,
),
Added<BlueprintChildrenReady>,
>,
animation_players: Query<(Entity, &Parent), With<AnimationPlayer>>,
all_children: Query<&Children>,
all_parents: Query<&Parent>,
with_animation_infos: Query<&AnimationInfos>,
// FIXME: meh
anims: Query<&BlueprintAnimations>,
mut commands: Commands,
all_names: Query<&Name>,
) {
for (original, children, original_children, name, animations) in blueprint_scenes.iter() {
info!("Step 5: Cleaning up spawned scene {:?}", name);
if children.len() == 0 {
// TODO: investigate, Honestly not sure if this issue from Bevy 0.12 is still present at all anymore
warn!("timing issue ! no children found, please restart your bevy app (bug being investigated)");
continue;
}
// the root node is the first & normally only child inside a scene, it is the one that has all relevant components
let mut blueprint_root_entity = Entity::PLACEHOLDER; //FIXME: and what about childless ones ?? => should not be possible normally
// let diff = HashSet::from_iter(original_children.0).difference(HashSet::from_iter(children));
// we find the first child that was not in the entity before (aka added during the scene spawning)
for child in children.iter() {
if !original_children.0.contains(child) {
blueprint_root_entity = *child;
break;
}
}
// we flag all children of the blueprint instance with 'FromBlueprint'
// can be usefull to filter out anything that came from blueprints vs normal children
for child in all_children.iter_descendants(blueprint_root_entity) {
commands.entity(child).insert(FromBlueprint); // we do this here in order to avoid doing it to normal children
}
// copy components into from blueprint instance's blueprint_root_entity to original entity
commands.add(CopyComponents {
source: blueprint_root_entity,
destination: original,
exclude: vec![TypeId::of::<Parent>(), TypeId::of::<Children>()],
stringent: false,
});
// we move all of children of the blueprint instance one level to the original entity to avoid having an additional, useless nesting level
if let Ok(root_entity_children) = all_children.get(blueprint_root_entity) {
for child in root_entity_children.iter() {
// info!("copying child {:?} upward from {:?} to {:?}", names.get(*child), blueprint_root_entity, original);
commands.entity(original).add_child(*child);
}
}
if animations.named_animations.keys().len() > 0 {
for (entity_with_player, parent) in animation_players.iter() {
if parent.get() == blueprint_root_entity {
println!(
"FOUND ANIMATION PLAYER FOR {:?} {:?} ",
all_names.get(original),
all_names.get(entity_with_player)
);
// FIXME: stopgap solution: since we cannot use an AnimationPlayer at the root entity level
// and we cannot update animation clips so that the EntityPaths point to one level deeper,
// BUT we still want to have some marker/control at the root entity level, we add this
commands
.entity(original)
.insert((BlueprintAnimationPlayerLink(entity_with_player),)); // FIXME : this is only valid for per-blueprint logic, no per scene animations
// since v0.14 you need both AnimationTransitions and AnimationGraph components/handle on the same entity as the animationPlayer
let transitions = AnimationTransitions::new();
commands
.entity(entity_with_player)
.insert((transitions, animations.graph.clone()));
}
}
// FIXME VERY convoluted, but it works
for child in all_children.iter_descendants(blueprint_root_entity) {
if with_animation_infos.get(child).is_ok() {
// player is already on the same entity as the animation_infos
if animation_players.get(child).is_ok() {
println!(
"found BLUEPRINT animation player for {:?} at {:?} Root: {:?}",
all_names.get(child),
all_names.get(child),
all_names.get(original)
);
commands.entity(original).insert(
//BlueprintAnimationPlayerLink(bla),
BlueprintAnimationInfosLink(child),
);
} else {
for parent in all_parents.iter_ancestors(child) {
if animation_players.get(parent).is_ok() {
/*println!(
"found SCENE animation player for {:?} at {:?} Root: {:?}",
all_names.get(child),
all_names.get(parent),
all_names.get(original)
);
println!("INSERTING SCENE ANIMATIONS INTO");*/
let original_animations = anims.get(original).unwrap();
commands.entity(child).insert((
InstanceAnimationPlayerLink(parent),
InstanceAnimations {
named_animations: original_animations
.named_animations
.clone(),
named_indices: original_animations.named_indices.clone(),
graph: original_animations.graph.clone(),
},
));
}
if with_animation_infos.get(parent).is_ok() {
commands
.entity(child)
.insert(InstanceAnimationInfosLink(parent));
}
}
}
}
}
}
commands
.entity(original)
.remove::<BlueprintChildrenReady>() // we are done with this step, we can remove the `BlueprintChildrenReady` tag component
.insert(BlueprintReadyForPostProcess); // Tag the entity so any systems dealing with post processing can know it is now their "turn"
commands.entity(blueprint_root_entity).despawn_recursive(); // Remove the root entity that comes from the spawned-in scene
}
}
#[derive(Component, Reflect, Debug)]
#[reflect(Component)]
pub struct BlueprintReadyForFinalizing;
#[derive(Component, Debug)]
/// flag component added when a Blueprint instance ist Ready : ie :
/// - its assets have loaded
/// - it has finished spawning
pub struct BlueprintInstanceReady;
pub(crate) fn blueprints_finalize_instances(
blueprint_instances: Query<
(
Entity,
Option<&Name>,
&BlueprintInfo,
Option<&SubBlueprintSpawnRoot>,
Option<&HideUntilReady>,
Option<&OriginalVisibility>,
),
(With<BlueprintSpawning>, With<BlueprintReadyForFinalizing>),
>,
mut sub_blueprint_trackers: Query<&mut SubBlueprintsSpawnTracker, With<BlueprintInfo>>,
spawning_blueprints: Query<&BlueprintSpawning>,
all_children: Query<&Children>,
mut blueprint_events: EventWriter<BlueprintEvent>,
mut commands: Commands,
// all_names: Query<&Name>
) {
for (entity, name, blueprint_info, parent_blueprint, hide_until_ready, original_visibility) in
blueprint_instances.iter()
{
info!("Step 8: Finalizing blueprint instance {:?}", name);
commands
.entity(entity)
.remove::<BlueprintMetaLoaded>()
.remove::<BlueprintReadyForFinalizing>()
.remove::<BlueprintReadyForPostProcess>()
.remove::<BlueprintSpawning>()
.remove::<SpawnBlueprint>()
//.remove::<Handle<Scene>>(); // FIXME: if we delete the handle to the scene, things get despawned ! not what we want
.remove::<BlueprintAssetsLoadState>() // also clear the sub assets tracker to free up handles, perhaps just freeing up the handles and leave the rest would be better ?
.remove::<BlueprintAssetsLoaded>()
.remove::<OriginalChildren>() // we do not need to keep the original children information
.insert(BlueprintInstanceReady);
// Deal with sub blueprints
// now check if the current entity is a child blueprint instance of another entity
// this should always be done last, as children should be finished before the parent can be processed correctly
// TODO: perhaps use observers for these
if let Some(track_root) = parent_blueprint {
// only propagate sub_blueprint spawning if the parent blueprint instance ist actually in spawning mode
if spawning_blueprints.get(track_root.0).is_ok() {
if let Ok(mut tracker) = sub_blueprint_trackers.get_mut(track_root.0) {
tracker
.sub_blueprint_instances
.entry(entity)
.or_insert(true);
tracker.sub_blueprint_instances.insert(entity, true);
// TODO: ugh, my limited rust knowledge, this is bad code
let mut all_spawned = true;
for val in tracker.sub_blueprint_instances.values() {
if !val {
all_spawned = false;
break;
}
}
if all_spawned {
// let root_name = all_names.get(track_root.0);
// println!("ALLLLL SPAAAAWNED for {} named {:?}", track_root.0, root_name);
commands.entity(track_root.0).insert(BlueprintChildrenReady);
}
}
}
}
commands
.entity(entity)
.remove::<BlueprintInstanceDisabled>();
for child in all_children.iter_descendants(entity) {
commands.entity(child).remove::<BlueprintInstanceDisabled>();
}
if hide_until_ready.is_some() {
if let Some(original_visibility) = original_visibility {
commands.entity(entity).insert(original_visibility.0);
} else {
commands.entity(entity).insert(Visibility::Inherited);
}
}
blueprint_events.send(BlueprintEvent::InstanceReady {
entity,
blueprint_name: blueprint_info.name.clone(),
blueprint_path: blueprint_info.path.clone(),
});
}
}

View File

@ -0,0 +1,180 @@
use bevy::core_pipeline::tonemapping::Tonemapping;
use bevy::pbr::DirectionalLightShadowMap;
use bevy::prelude::*;
use bevy::render::view::{ColorGrading, ColorGradingGlobal, ColorGradingSection};
use crate::GltfComponentsSet;
pub(crate) fn plugin(app: &mut App) {
app.register_type::<BlenderBackgroundShader>()
.register_type::<BlenderShadowSettings>()
.register_type::<BlenderLightShadows>()
.register_type::<BlenderToneMapping>()
.register_type::<BlenderColorGrading>()
.add_systems(
Update,
(
process_lights,
process_shadowmap,
process_background_shader,
process_tonemapping,
process_colorgrading,
)
.after(GltfComponentsSet::Injection),
);
}
#[derive(Component, Reflect, Default, Debug, PartialEq, Clone)]
#[reflect(Component)]
#[non_exhaustive]
/// The properties of a light's shadow , to enable controlling per light shadows from Blender
pub struct BlenderLightShadows {
pub enabled: bool,
pub buffer_bias: f32,
}
/// The background color as described by Blender's [background shader](https://docs.blender.org/manual/en/latest/render/shader_nodes/shader/background.html).
#[derive(Component, Reflect, Default, Debug, PartialEq, Clone)]
#[reflect(Component)]
#[non_exhaustive]
pub struct BlenderBackgroundShader {
pub color: Color,
pub strength: f32,
}
/// The settings used by EEVEE's [shadow rendering](https://docs.blender.org/manual/en/latest/render/eevee/render_settings/shadows.html).
#[derive(Component, Reflect, Default, Debug, PartialEq, Clone)]
#[reflect(Component)]
#[non_exhaustive]
pub struct BlenderShadowSettings {
pub cascade_size: usize,
}
/// Not all possible Blender `ToneMappings` are available in Bevy & vice versa
#[derive(Component, Reflect, Default, Debug, PartialEq, Clone)]
#[reflect(Component)]
#[non_exhaustive]
pub enum BlenderToneMapping {
#[default]
None,
AgX,
Filmic,
}
#[derive(Component, Reflect, Default, Debug, PartialEq, Clone)]
#[reflect(Component)]
#[non_exhaustive]
pub struct BlenderColorGrading {
exposure: f32,
gamma: f32,
}
fn process_lights(
mut directional_lights: Query<
(&mut DirectionalLight, Option<&BlenderLightShadows>),
Added<DirectionalLight>,
>,
mut spot_lights: Query<(&mut SpotLight, Option<&BlenderLightShadows>), Added<SpotLight>>,
mut point_lights: Query<(&mut PointLight, Option<&BlenderLightShadows>), Added<PointLight>>,
) {
for (mut light, blender_light_shadows) in directional_lights.iter_mut() {
if let Some(blender_light_shadows) = blender_light_shadows {
light.shadows_enabled = blender_light_shadows.enabled;
}
}
for (mut light, blender_light_shadows) in spot_lights.iter_mut() {
if let Some(blender_light_shadows) = blender_light_shadows {
light.shadows_enabled = blender_light_shadows.enabled;
}
}
for (mut light, blender_light_shadows) in point_lights.iter_mut() {
if let Some(blender_light_shadows) = blender_light_shadows {
light.shadows_enabled = blender_light_shadows.enabled;
}
}
}
fn process_shadowmap(
shadowmaps: Query<&BlenderShadowSettings, Added<BlenderShadowSettings>>,
mut commands: Commands,
) {
for shadowmap in shadowmaps.iter() {
commands.insert_resource(DirectionalLightShadowMap {
size: shadowmap.cascade_size,
});
}
}
fn process_background_shader(
background_shaders: Query<&BlenderBackgroundShader, Added<BlenderBackgroundShader>>,
mut commands: Commands,
) {
for background_shader in background_shaders.iter() {
commands.insert_resource(AmbientLight {
color: background_shader.color,
// Just a guess, see <https://github.com/bevyengine/bevy/issues/12280>
brightness: background_shader.strength * 400.0,
});
commands.insert_resource(ClearColor(background_shader.color));
}
}
// FIXME: this logic should not depend on if toneMapping or Cameras where added first
fn process_tonemapping(
tonemappings: Query<(Entity, &BlenderToneMapping), Added<BlenderToneMapping>>,
cameras: Query<Entity, With<Camera>>,
mut commands: Commands,
) {
for entity in cameras.iter() {
for (scene_id, tone_mapping) in tonemappings.iter() {
match tone_mapping {
BlenderToneMapping::None => {
//println!("TONEMAPPING NONE");
commands.entity(entity).remove::<Tonemapping>();
}
BlenderToneMapping::AgX => {
//println!("TONEMAPPING Agx");
commands.entity(entity).insert(Tonemapping::AgX);
}
BlenderToneMapping::Filmic => {
//println!("TONEMAPPING Filmic");
commands.entity(entity).insert(Tonemapping::BlenderFilmic);
}
}
commands.entity(scene_id).remove::<BlenderToneMapping>();
}
}
}
// FIXME: this logic should not depend on if toneMapping or Cameras where added first
fn process_colorgrading(
blender_colorgradings: Query<(Entity, &BlenderColorGrading), Added<BlenderColorGrading>>,
cameras: Query<Entity, With<Camera>>,
mut commands: Commands,
) {
for entity in cameras.iter() {
for (scene_id, blender_colorgrading) in blender_colorgradings.iter() {
info!("COLOR GRADING");
commands.entity(entity).insert(ColorGrading {
global: ColorGradingGlobal {
exposure: blender_colorgrading.exposure,
..Default::default()
},
shadows: ColorGradingSection {
gamma: blender_colorgrading.gamma,
..Default::default()
},
midtones: ColorGradingSection {
gamma: blender_colorgrading.gamma,
..Default::default()
},
highlights: ColorGradingSection {
gamma: blender_colorgrading.gamma,
..Default::default()
},
});
commands.entity(scene_id).remove::<ColorGrading>();
}
}
}

View File

@ -10,13 +10,7 @@ pub use process_gltfs::*;
pub mod blender_settings;
use bevy::{
app::Startup,
ecs::{
component::Component,
reflect::ReflectComponent,
system::{Res, Resource},
},
log::warn,
ecs::{component::Component, reflect::ReflectComponent},
prelude::{App, IntoSystemConfigs, Plugin, SystemSet, Update},
reflect::Reflect,
};
@ -27,9 +21,9 @@ use bevy::{
/// ```
/// # use bevy::prelude::*;
/// # use bevy::gltf::*;
/// # use bevy_gltf_components::ComponentsFromGltfPlugin;
/// # use blenvy::ComponentsFromGltfPlugin;
///
/// //too barebones of an example to be meaningfull, please see https://github.com/kaosat-dev/Blender_bevy_components_workflow/examples/basic for a real example
/// //too barebones of an example to be meaningfull, please see https://github.com/kaosat-dev/Blenvy/examples/basic for a real example
/// fn main() {
/// App::new()
/// .add_plugins(DefaultPlugins)
@ -65,35 +59,13 @@ pub enum GltfComponentsSet {
Injection,
}
#[derive(Clone, Resource)]
pub struct GltfComponentsConfig {
pub(crate) legacy_mode: bool,
}
pub struct ComponentsFromGltfPlugin {
pub legacy_mode: bool,
}
impl Default for ComponentsFromGltfPlugin {
fn default() -> Self {
Self { legacy_mode: true }
}
}
fn check_for_legacy_mode(gltf_components_config: Res<GltfComponentsConfig>) {
if gltf_components_config.legacy_mode {
warn!("using simplified component definitions is deprecated since 0.3, prefer defining components with real ron values (use the bevy_components tool for Blender for simplicity) ");
}
}
#[derive(Default)]
pub struct ComponentsFromGltfPlugin {}
impl Plugin for ComponentsFromGltfPlugin {
fn build(&self, app: &mut App) {
app.add_plugins(blender_settings::plugin)
.register_type::<GltfProcessed>()
.insert_resource(GltfComponentsConfig {
legacy_mode: self.legacy_mode,
})
.add_systems(Startup, check_for_legacy_mode)
.add_systems(
Update,
(add_components_from_gltf_extras).in_set(GltfComponentsSet::Injection),

View File

@ -0,0 +1,159 @@
use bevy::{
core::Name,
ecs::{
entity::Entity,
query::{Added, Without},
reflect::{AppTypeRegistry, ReflectComponent},
world::World,
},
gltf::{GltfExtras, GltfMaterialExtras, GltfMeshExtras, GltfSceneExtras},
hierarchy::Parent,
log::{debug, warn},
reflect::{Reflect, TypeRegistration},
utils::HashMap,
};
use crate::{ronstring_to_reflect_component, GltfProcessed};
// , mut entity_components: HashMap<Entity, Vec<(Box<dyn Reflect>, TypeRegistration)>>
fn find_entity_components(
entity: Entity,
name: Option<&Name>,
parent: Option<&Parent>,
reflect_components: Vec<(Box<dyn Reflect>, TypeRegistration)>,
entity_components: &HashMap<Entity, Vec<(Box<dyn Reflect>, TypeRegistration)>>,
) -> (Entity, Vec<(Box<dyn Reflect>, TypeRegistration)>) {
// we assign the components specified /xxx_components objects to their parent node
let mut target_entity = entity;
// if the node contains "components" or ends with "_pa" (ie add to parent), the components will not be added to the entity itself but to its parent
// this is mostly used for Blender collections
if parent.is_some() {
if let Some(name) = name {
if name.as_str().contains("components") || name.as_str().ends_with("_pa") {
debug!("adding components to parent");
target_entity = parent.expect("the target entity had a parent ").get();
}
}
}
debug!("adding to {:?}", target_entity);
// if there where already components set to be added to this entity (for example when entity_data was refering to a parent), update the vec of entity_components accordingly
// this allows for example blender collection to provide basic ecs data & the instances to override/ define their own values
if entity_components.contains_key(&target_entity) {
let mut updated_components: Vec<(Box<dyn Reflect>, TypeRegistration)> = Vec::new();
let current_components = &entity_components[&target_entity];
// first inject the current components
for (component, type_registration) in current_components {
updated_components.push((component.clone_value(), type_registration.clone()));
}
// then inject the new components: this also enables overwrite components set in the collection
for (component, type_registration) in reflect_components {
updated_components.push((component.clone_value(), type_registration));
}
return (target_entity, updated_components);
}
(target_entity, reflect_components)
}
/// main function: injects components into each entity in gltf files that have `gltf_extras`, using reflection
pub fn add_components_from_gltf_extras(world: &mut World) {
let mut extras = world.query_filtered::<(Entity, Option<&Name>, &GltfExtras, Option<&Parent>), (Added<GltfExtras>, Without<GltfProcessed>)>();
let mut scene_extras = world.query_filtered::<(Entity, Option<&Name>, &GltfSceneExtras, Option<&Parent>), (Added<GltfSceneExtras>, Without<GltfProcessed>)>();
let mut mesh_extras = world.query_filtered::<(Entity, Option<&Name>, &GltfMeshExtras, Option<&Parent>), (Added<GltfMeshExtras>, Without<GltfProcessed>)>();
let mut material_extras = world.query_filtered::<(Entity, Option<&Name>, &GltfMaterialExtras, Option<&Parent>), (Added<GltfMaterialExtras>, Without<GltfProcessed>)>();
let mut entity_components: HashMap<Entity, Vec<(Box<dyn Reflect>, TypeRegistration)>> =
HashMap::new();
// let gltf_components_config = world.resource::<GltfComponentsConfig>();
for (entity, name, extra, parent) in extras.iter(world) {
debug!(
"Gltf Extra: Name: {:?}, entity {:?}, parent: {:?}, extras {:?}",
name, entity, parent, extra
);
let type_registry: &AppTypeRegistry = world.resource();
let type_registry = type_registry.read();
let reflect_components = ronstring_to_reflect_component(&extra.value, &type_registry);
// let name = name.unwrap_or(&Name::new(""));
let (target_entity, updated_components) =
find_entity_components(entity, name, parent, reflect_components, &entity_components);
entity_components.insert(target_entity, updated_components);
}
for (entity, name, extra, parent) in scene_extras.iter(world) {
debug!(
"Gltf Scene Extra: Name: {:?}, entity {:?}, parent: {:?}, scene_extras {:?}",
name, entity, parent, extra
);
let type_registry: &AppTypeRegistry = world.resource();
let type_registry = type_registry.read();
let reflect_components = ronstring_to_reflect_component(&extra.value, &type_registry);
let (target_entity, updated_components) =
find_entity_components(entity, name, parent, reflect_components, &entity_components);
entity_components.insert(target_entity, updated_components);
}
for (entity, name, extra, parent) in mesh_extras.iter(world) {
debug!(
"Gltf Mesh Extra: Name: {:?}, entity {:?}, parent: {:?}, mesh_extras {:?}",
name, entity, parent, extra
);
let type_registry: &AppTypeRegistry = world.resource();
let type_registry = type_registry.read();
let reflect_components = ronstring_to_reflect_component(&extra.value, &type_registry);
let (target_entity, updated_components) =
find_entity_components(entity, name, parent, reflect_components, &entity_components);
entity_components.insert(target_entity, updated_components);
}
for (entity, name, extra, parent) in material_extras.iter(world) {
debug!(
"Name: {:?}, entity {:?}, parent: {:?}, material_extras {:?}",
name, entity, parent, extra
);
let type_registry: &AppTypeRegistry = world.resource();
let type_registry = type_registry.read();
let reflect_components = ronstring_to_reflect_component(&extra.value, &type_registry);
let (target_entity, updated_components) =
find_entity_components(entity, name, parent, reflect_components, &entity_components);
entity_components.insert(target_entity, updated_components);
}
for (entity, components) in entity_components {
let type_registry: &AppTypeRegistry = world.resource();
let type_registry = type_registry.clone();
let type_registry = type_registry.read();
if !components.is_empty() {
debug!("--entity {:?}, components {}", entity, components.len());
}
for (component, type_registration) in components {
debug!(
"------adding {} {:?}",
component.get_represented_type_info().unwrap().type_path(),
component
);
{
let mut entity_mut = world.entity_mut(entity);
let Some(reflected_component) = type_registration.data::<ReflectComponent>() else {
warn!(?component, "unable to reflect component");
entity_mut.insert(GltfProcessed);
continue;
};
reflected_component.insert(&mut entity_mut, &*component, &type_registry);
entity_mut.insert(GltfProcessed); //
}
}
}
}

View File

@ -0,0 +1,139 @@
use bevy::log::{debug, warn};
use bevy::reflect::serde::ReflectDeserializer;
use bevy::reflect::{Reflect, TypeRegistration, TypeRegistry};
use bevy::utils::HashMap;
use ron::Value;
use serde::de::DeserializeSeed;
use super::capitalize_first_letter;
pub fn ronstring_to_reflect_component(
ron_string: &str,
type_registry: &TypeRegistry,
) -> Vec<(Box<dyn Reflect>, TypeRegistration)> {
let lookup: HashMap<String, Value> = ron::from_str(ron_string).unwrap();
let mut components: Vec<(Box<dyn Reflect>, TypeRegistration)> = Vec::new();
// println!("ron_string {:?}", ron_string);
for (name, value) in lookup.into_iter() {
let parsed_value: String = match value.clone() {
Value::String(str) => str,
_ => ron::to_string(&value).unwrap().to_string(),
};
if name.as_str() == "bevy_components" {
bevy_components_string_to_components(parsed_value, type_registry, &mut components);
} else {
components_string_to_components(
name,
value,
parsed_value,
type_registry,
&mut components,
);
}
}
components
}
fn components_string_to_components(
name: String,
value: Value,
parsed_value: String,
type_registry: &TypeRegistry,
components: &mut Vec<(Box<dyn Reflect>, TypeRegistration)>,
) {
let type_string = name.replace("component: ", "").trim().to_string();
let capitalized_type_name = capitalize_first_letter(type_string.as_str());
if let Some(type_registration) =
type_registry.get_with_short_type_path(capitalized_type_name.as_str())
{
debug!("TYPE INFO {:?}", type_registration.type_info());
let ron_string = format!(
"{{ \"{}\":{} }}",
type_registration.type_info().type_path(),
parsed_value
);
/*
// usefull to determine what an entity looks like Serialized
let test_struct = Color::Srgba(Srgba { red: 0.2, green: 0.2, blue: 0.2, alpha: 0.2 });
//CameraRenderGraph::new("name");
let serializer = ReflectSerializer::new(&test_struct, &type_registry);
let serialized =
ron::ser::to_string_pretty(&serializer, ron::ser::PrettyConfig::default()).unwrap();
println!("serialized Component {}", serialized);
*/
debug!("component data ron string {}", ron_string);
let mut deserializer = ron::Deserializer::from_str(ron_string.as_str())
.expect("deserialzer should have been generated from string");
let reflect_deserializer = ReflectDeserializer::new(type_registry);
/*let component = reflect_deserializer
.deserialize(&mut deserializer)
.unwrap_or_else(|_| {
panic!(
"failed to deserialize component {} with value: {:?}",
name, value
)
});*/
let Ok(component) = reflect_deserializer.deserialize(&mut deserializer) else {
warn!(
"failed to deserialize component {} with value: {:?}",
name, value
);
return;
};
debug!("component {:?}", component);
debug!("real type {:?}", component.get_represented_type_info());
components.push((component, type_registration.clone()));
debug!("found type registration for {}", capitalized_type_name);
} else {
warn!("no type registration for {}", capitalized_type_name);
}
}
fn bevy_components_string_to_components(
parsed_value: String,
type_registry: &TypeRegistry,
components: &mut Vec<(Box<dyn Reflect>, TypeRegistration)>,
) {
let lookup: HashMap<String, Value> = ron::from_str(&parsed_value).unwrap();
for (key, value) in lookup.into_iter() {
let parsed_value: String = match value.clone() {
Value::String(str) => str,
_ => ron::to_string(&value).unwrap().to_string(),
};
if let Some(type_registration) = type_registry.get_with_type_path(key.as_str()) {
debug!("TYPE INFO {:?}", type_registration.type_info());
let ron_string = format!(
"{{ \"{}\":{} }}",
type_registration.type_info().type_path(),
parsed_value
);
debug!("component data ron string {}", ron_string);
let mut deserializer = ron::Deserializer::from_str(ron_string.as_str())
.expect("deserialzer should have been generated from string");
let reflect_deserializer = ReflectDeserializer::new(type_registry);
let component = reflect_deserializer
.deserialize(&mut deserializer)
.unwrap_or_else(|_| {
panic!(
"failed to deserialize component {} with value: {:?}",
key, value
)
});
debug!("component {:?}", component);
debug!("real type {:?}", component.get_represented_type_info());
components.push((component, type_registration.clone()));
debug!("found type registration for {}", key);
} else {
warn!("no type registration for {}", key);
}
}
}

91
crates/blenvy/src/lib.rs Normal file
View File

@ -0,0 +1,91 @@
use bevy::{render::primitives::Aabb, utils::HashMap};
use std::path::PathBuf;
pub mod components;
pub use components::*;
pub mod registry;
pub use registry::*;
pub mod blueprints;
pub use blueprints::*;
pub mod save_load;
pub use save_load::*;
#[derive(Clone, Resource)]
pub struct BlenvyConfig {
// registry
pub(crate) export_registry: bool,
pub(crate) registry_save_path: PathBuf,
pub(crate) registry_component_filter: SceneFilter,
#[allow(dead_code)]
pub(crate) registry_resource_filter: SceneFilter,
// blueprints
pub(crate) aabb_cache: HashMap<String, Aabb>, // cache for aabbs
pub(crate) materials_cache: HashMap<String, Handle<StandardMaterial>>, // cache for materials
// save & load
pub(crate) save_component_filter: SceneFilter,
pub(crate) save_resource_filter: SceneFilter,
#[allow(dead_code)]
pub(crate) save_path: PathBuf,
}
#[derive(Debug, Clone)]
/// Plugin for gltf blueprints
pub struct BlenvyPlugin {
pub export_registry: bool,
pub registry_save_path: PathBuf,
pub registry_component_filter: SceneFilter,
pub registry_resource_filter: SceneFilter,
// for save & load
pub save_component_filter: SceneFilter,
pub save_resource_filter: SceneFilter,
pub save_path: PathBuf,
}
impl Default for BlenvyPlugin {
fn default() -> Self {
Self {
export_registry: true,
registry_save_path: PathBuf::from("registry.json"), // relative to assets folder
registry_component_filter: SceneFilter::default(),
registry_resource_filter: SceneFilter::default(),
save_component_filter: SceneFilter::default(),
save_resource_filter: SceneFilter::default(),
save_path: PathBuf::from("blenvy_saves"), // TODO: use https://docs.rs/dirs/latest/dirs/ to default to the correct user directory
}
}
}
impl Plugin for BlenvyPlugin {
fn build(&self, app: &mut App) {
app.add_plugins((
ComponentsFromGltfPlugin::default(),
#[cfg(debug_assertions)] // we only need the export registry plugin at dev time
ExportRegistryPlugin::default(),
BlueprintsPlugin::default(),
#[cfg(not(target_arch = "wasm32"))] // save & load is only for non wasm platforms
SaveLoadPlugin::default(),
))
.insert_resource(BlenvyConfig {
export_registry: self.export_registry,
registry_save_path: self.registry_save_path.clone(),
registry_component_filter: self.registry_component_filter.clone(),
registry_resource_filter: self.registry_resource_filter.clone(),
aabb_cache: HashMap::new(),
materials_cache: HashMap::new(),
save_component_filter: self.save_component_filter.clone(),
save_resource_filter: self.save_resource_filter.clone(),
save_path: self.save_path.clone(),
});
}
}

View File

@ -1,40 +1,47 @@
use std::{fs::File, path::Path};
use bevy::log::info;
use bevy_ecs::{
reflect::{AppTypeRegistry, ReflectComponent, ReflectResource},
world::World,
use crate::{AssetRoot, BlenvyConfig};
use bevy::{
log::info,
prelude::{AppTypeRegistry, ReflectComponent, ReflectResource, World},
reflect::{TypeInfo, TypeRegistration, VariantInfo},
};
use bevy_reflect::{TypeInfo, TypeRegistration, VariantInfo}; // TypePath // DynamicTypePath
use serde_json::{json, Map, Value};
use crate::{AssetRoot, ExportComponentsConfig};
use std::{fs::File, path::Path};
pub fn export_types(world: &mut World) {
let config = world
.get_resource::<ExportComponentsConfig>()
.get_resource::<BlenvyConfig>()
.expect("ExportComponentsConfig should exist at this stage");
let asset_root = world.resource::<AssetRoot>();
let registry_save_path = Path::join(&asset_root.0, &config.save_path);
println!("registry_save_path {}", registry_save_path.display());
let registry_save_path = Path::join(&asset_root.0, &config.registry_save_path);
let writer = File::create(registry_save_path).expect("should have created schema file");
let components_to_filter_out = &config.registry_component_filter.clone();
let resources_to_filter_out = &config.registry_resource_filter.clone();
let types = world.resource_mut::<AppTypeRegistry>();
let types = types.read();
let schemas = types.iter().map(export_type).collect::<Map<_, _>>();
let schemas = types
.iter()
.filter(|type_info| {
let type_id = type_info.type_id();
components_to_filter_out.is_allowed_by_id(type_id)
&& resources_to_filter_out.is_allowed_by_id(type_id)
})
.map(export_type)
.collect::<Map<_, _>>();
serde_json::to_writer_pretty(
writer,
&json!({
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "bevy component registry schema",
"long_name": "bevy component registry schema",
"$defs": schemas,
}),
)
.expect("valid json");
info!("Done exporting registry schema")
info!("Done exporting registry schema");
}
pub fn export_type(reg: &TypeRegistration) -> (String, Value) {
@ -57,7 +64,7 @@ pub fn export_type(reg: &TypeRegistration) -> (String, Value) {
json!({
"type": "object",
"typeInfo": "Struct",
"title": t.type_path(),
"long_name": t.type_path(),
"properties": properties,
"additionalProperties": false,
"required": info
@ -75,7 +82,7 @@ pub fn export_type(reg: &TypeRegistration) -> (String, Value) {
json!({
"type": "string",
"typeInfo": "Enum",
"title": t.type_path(),
"long_name": t.type_path(),
"oneOf": info
.iter()
.map(|variant| match variant {
@ -94,12 +101,12 @@ pub fn export_type(reg: &TypeRegistration) -> (String, Value) {
VariantInfo::Struct(v) => json!({
"type": "object",
"typeInfo": "Struct",
"title": v.name(),
"long_name": v.name(),
"short_name": v.name().split("::").last().unwrap_or(v.name()),
"properties": v
.iter()
.enumerate()
.map(|(variant_idx, field)| (field.name().to_owned(), add_min_max(json!({"type": typ(field.type_path()), "title": field.name()}), reg, field_idx, Some(variant_idx))))
.map(|(variant_idx, field)| (field.name().to_owned(), add_min_max(json!({"type": typ(field.type_path()), "long_name": field.name()}), reg, field_idx, Some(variant_idx))))
.collect::<Map<_, _>>(),
"additionalProperties": false,
"required": v
@ -111,7 +118,7 @@ pub fn export_type(reg: &TypeRegistration) -> (String, Value) {
VariantInfo::Tuple(v) => json!({
"type": "array",
"typeInfo": "Tuple",
"title": v.name(),
"long_name": v.name(),
"short_name":v.name(),
"prefixItems": v
.iter()
@ -121,7 +128,7 @@ pub fn export_type(reg: &TypeRegistration) -> (String, Value) {
"items": false,
}),
VariantInfo::Unit(v) => json!({
"title": v.name(),
"long_name": v.name(),
}),
})
.collect::<Vec<_>>();
@ -129,13 +136,13 @@ pub fn export_type(reg: &TypeRegistration) -> (String, Value) {
json!({
"type": "object",
"typeInfo": "Enum",
"title": t.type_path(),
"long_name": t.type_path(),
"oneOf": variants,
})
}
}
TypeInfo::TupleStruct(info) => json!({
"title": t.type_path(),
"long_name": t.type_path(),
"type": "array",
"typeInfo": "TupleStruct",
"prefixItems": info
@ -147,26 +154,27 @@ pub fn export_type(reg: &TypeRegistration) -> (String, Value) {
}),
TypeInfo::List(info) => {
json!({
"title": t.type_path(),
"long_name": t.type_path(),
"type": "array",
"typeInfo": "List",
"items": json!({"type": typ(info.item_type_path_table().path())}),
})
}
TypeInfo::Array(info) => json!({
"title": t.type_path(),
"long_name": t.type_path(),
"type": "array",
"typeInfo": "Array",
"items": json!({"type": typ(info.item_type_path_table().path())}),
}),
TypeInfo::Map(info) => json!({
"title": t.type_path(),
"long_name": t.type_path(),
"type": "object",
"typeInfo": "Map",
"additionalProperties": json!({"type": typ(info.value_type_path_table().path())}),
"valueType": json!({"type": typ(info.value_type_path_table().path())}),
"keyType": json!({"type": typ(info.key_type_path_table().path())}),
}),
TypeInfo::Tuple(info) => json!({
"title": t.type_path(),
"long_name": t.type_path(),
"type": "array",
"typeInfo": "Tuple",
"prefixItems": info
@ -177,7 +185,7 @@ pub fn export_type(reg: &TypeRegistration) -> (String, Value) {
"items": false,
}),
TypeInfo::Value(info) => json!({
"title": t.type_path(),
"long_name": t.type_path(),
"type": map_json_type(info.type_path()),
"typeInfo": "Value",
}),
@ -221,8 +229,7 @@ fn add_min_max(
field_index: usize,
variant_index: Option<usize>,
) -> Value {
#[cfg(feature = "support-inspector")]
fn get_min_max(
/*fn get_min_max(
reg: &TypeRegistration,
field_index: usize,
variant_index: Option<usize>,
@ -244,9 +251,8 @@ fn add_min_max(
})
.and_then(|o| o.downcast_ref::<NumberOptions<f32>>())
.map(|num| (num.min, num.max))
}
}*/
#[cfg(not(feature = "support-inspector"))]
fn get_min_max(
_reg: &TypeRegistration,
_field_index: usize,

View File

@ -1,25 +1,16 @@
pub mod export_types;
use std::path::PathBuf;
use bevy_app::Startup;
use bevy_ecs::system::Resource;
pub mod export_types;
pub use export_types::*;
use bevy::{
app::Startup,
asset::AssetPlugin,
prelude::{App, Plugin},
prelude::{App, IntoSystemConfigs, Plugin, Res, Resource},
scene::SceneFilter,
};
// Plugin configuration
#[derive(Clone, Resource)]
pub struct ExportComponentsConfig {
pub(crate) save_path: PathBuf,
#[allow(dead_code)]
pub(crate) component_filter: SceneFilter, // unused for now
#[allow(dead_code)]
pub(crate) resource_filter: SceneFilter, // unused for now
}
use crate::BlenvyConfig;
pub struct ExportRegistryPlugin {
pub component_filter: SceneFilter,
@ -30,22 +21,22 @@ pub struct ExportRegistryPlugin {
impl Default for ExportRegistryPlugin {
fn default() -> Self {
Self {
component_filter: SceneFilter::default(), // unused for now
resource_filter: SceneFilter::default(), // unused for now
component_filter: SceneFilter::default(),
resource_filter: SceneFilter::default(),
save_path: PathBuf::from("registry.json"), // relative to assets folder
}
}
}
fn export_registry(blenvy_config: Res<BlenvyConfig>) -> bool {
// TODO: add detection of Release builds, wasm, and android in order to avoid exporting registry in those cases
blenvy_config.export_registry
}
impl Plugin for ExportRegistryPlugin {
fn build(&self, app: &mut App) {
app.register_asset_root()
.insert_resource(ExportComponentsConfig {
save_path: self.save_path.clone(),
component_filter: self.component_filter.clone(),
resource_filter: self.resource_filter.clone(),
})
.add_systems(Startup, export_types);
.add_systems(Startup, export_types.run_if(export_registry));
}
}

View File

@ -0,0 +1,56 @@
pub use bevy::prelude::*;
use crate::{BlueprintInfo, GameWorldTag, HideUntilReady, SpawnBlueprint};
use super::BlueprintWorld;
pub(crate) fn spawn_from_blueprintworld(
added_blueprint_worlds: Query<(Entity, &BlueprintWorld), Added<BlueprintWorld>>,
mut commands: Commands,
) {
for (__entity, blueprint_world) in added_blueprint_worlds.iter() {
println!("added blueprintWorld {:?}", blueprint_world);
// here we spawn the static part our game world/level, which is also a blueprint !
let __static_world = commands
.spawn((
BlueprintInfo::from_path(blueprint_world.path.as_str()), // all we need is a Blueprint info...
SpawnBlueprint, // and spawnblueprint to tell blenvy to spawn the blueprint now
HideUntilReady, // only reveal the level once it is ready
GameWorldTag,
))
.id();
// here we spawn the dynamic entities part of our game world/level, which is also a blueprint !
let __dynamic_world = commands
.spawn((
BlueprintInfo::from_path(
blueprint_world
.path
.replace(".glb", "_dynamic.glb")
.replace(".gltf", "_dynamic.gltf")
.as_str(),
), // all we need is a Blueprint info...
SpawnBlueprint, // and spawnblueprint to tell blenvy to spawn the blueprint now
HideUntilReady, // only reveal the level once it is ready
GameWorldTag,
))
.id();
// commands.entity(entity).add_child(static_world);
// commands.entity(entity).add_child(dynamic_world);
}
}
/*
pub(crate) fn inject_dynamic_into_children(
added_dynamic: Query<Entity, Added<Dynamic> >,
all_children: Query<&Children>,
mut commands: Commands,
) {
for entity in added_dynamic.iter() {
for child in all_children.iter_descendants(entity) {
commands.entity(child).insert(Dynamic);
}
}
}*/

View File

@ -0,0 +1,112 @@
use bevy::prelude::*;
use crate::{BlueprintInfo, DynamicEntitiesRoot, GameWorldTag, HideUntilReady, SpawnBlueprint};
#[derive(Event)]
pub struct LoadingRequest {
pub path: String,
}
#[derive(Event)]
pub struct LoadingFinished; // TODO: merge the two above
/// resource that keeps track of the current load request
#[derive(Resource, Default)]
pub struct LoadingRequested {
pub path: String,
}
/*
- Loading
- load request recieved
- pause things ?
- unload everything
- load static data using blueprintInfo
- load dynamic data from save file
General:
* wrap loading a bevy scene as a blueprint ?
* meh, has no assets & co, different logic ?
*/
pub fn process_load_requests(
mut load_requests: EventReader<LoadingRequest>,
mut commands: Commands,
) {
let mut save_path: String = "".into();
for load_request in load_requests.read() {
if !load_request.path.is_empty() {
save_path.clone_from(&load_request.path);
}
}
if !save_path.is_empty() {
commands.insert_resource(LoadingRequested { path: save_path });
}
}
pub fn should_load(loading_requests: Option<Res<LoadingRequested>>) -> bool {
resource_exists::<LoadingRequested>(loading_requests)
}
// TODO: replace with generic despawner ?
pub(crate) fn prepare_loading(
mut commands: Commands,
gameworlds: Query<Entity, With<GameWorldTag>>,
) {
for e in gameworlds.iter() {
info!("--loading: despawn old world/level");
commands.entity(e).despawn_recursive();
}
}
pub(crate) fn load_game(
mut commands: Commands,
asset_server: Res<AssetServer>,
load_request: Res<LoadingRequested>,
) {
info!("--loading: load dynamic data");
//let save_path = Path::new(load_request.path.clone().as_str());
info!("LOADING FROM {:?}", load_request.path.clone());
/*let world_root = commands
.spawn((
bevy::prelude::Name::from("world"),
GameWorldTag,
TransformBundle::default(),
InheritedVisibility::default(),
))
.id();*/
// and we fill it with dynamic data
// let input = std::fs::read(&path)?;
let _dynamic_data = commands
.spawn((
DynamicSceneBundle {
scene: asset_server.load(load_request.path.clone()),
..default()
},
bevy::prelude::Name::from("World_dynamic"),
DynamicEntitiesRoot,
GameWorldTag,
))
.id();
let _static_data = commands
.spawn((
BlueprintInfo::from_path("levels/World.glb"), // all we need is a Blueprint info...
SpawnBlueprint,
HideUntilReady,
GameWorldTag,
))
.id();
//commands.entity(world_root).add_child(static_data);
//commands.entity(world_root).add_child(dynamic_data);
// commands.insert_resource(LoadFirstStageDone);
info!("--loading: loaded dynamic data");
commands.remove_resource::<LoadingRequested>();
}

View File

@ -0,0 +1,88 @@
pub mod common;
pub use common::*;
pub mod saving;
pub use saving::*;
pub mod loading;
pub use loading::*;
#[derive(Component, Reflect, Debug, Default)]
#[reflect(Component)]
/// component used to mark any entity as Dynamic: aka add this to make sure your entity is going to be saved
pub struct Dynamic;
#[derive(Component, Reflect, Debug, Default)]
#[reflect(Component)]
/// marker component for entities that do not have parents, or whose parents should be ignored when serializing
pub(crate) struct RootEntity;
#[derive(Component, Debug)]
/// internal helper component to store parents before resetting them
pub(crate) struct OriginalParent(pub(crate) Entity);
/// Marker component to Flag the root entity of all static entities (immutables)
#[derive(Component, Reflect, Debug, Default)]
#[reflect(Component)]
pub struct StaticEntitiesRoot;
/// Marker component to Flag the root entity of all dynamic entities (mutables)
#[derive(Component, Reflect, Debug, Default)]
#[reflect(Component)]
pub struct DynamicEntitiesRoot;
#[derive(Resource, Clone, Debug, Default, Reflect)]
#[reflect(Resource)]
pub struct StaticEntitiesBlueprintInfo {
//pub blueprint_info: BlueprintInfo,
pub path: String,
}
#[derive(Component, Debug)]
pub struct BlueprintWorld {
pub path: String,
}
impl BlueprintWorld {
pub fn from_path(path: &str) -> BlueprintWorld {
// let p = Path::new(&path);
BlueprintWorld {
// name: p.file_stem().unwrap().to_os_string().into_string().unwrap(), // seriously ? , also unwraps !!
path: path.into(),
}
}
}
#[derive(Debug, Clone, Default)]
/// Plugin for saving & loading
pub struct SaveLoadPlugin {}
impl Plugin for SaveLoadPlugin {
fn build(&self, app: &mut App) {
app.register_type::<Dynamic>()
.register_type::<StaticEntitiesRoot>()
.add_event::<SavingRequest>()
.add_event::<SaveFinished>()
// common
.add_systems(Update, (spawn_from_blueprintworld,)) // inject_dynamic_into_children
// saving
.add_systems(Update, process_save_requests)
.add_systems(
Update,
(prepare_save_game, apply_deferred, save_game, cleanup_save)
.chain()
.run_if(should_save),
)
.add_event::<LoadingRequest>()
.add_event::<LoadingFinished>()
//loading
.add_systems(Update, process_load_requests)
.add_systems(
Update,
(prepare_loading, apply_deferred, load_game)
.chain()
.run_if(should_load),
//.run_if(not(resource_exists::<LoadFirstStageDone>))
// .in_set(LoadingSet::Load),
);
}
}

View File

@ -1,11 +1,11 @@
use bevy::{prelude::*, scene::SceneInstance};
use bevy_gltf_blueprints::{BluePrintBundle, BlueprintName, GameWorldTag, Library};
use blenvy::{BluePrintBundle, BlueprintName, GameWorldTag, Library};
use std::path::Path;
use crate::{DynamicEntitiesRoot, SaveLoadConfig, StaticEntitiesRoot, StaticEntitiesStorage};
#[derive(Event)]
pub struct LoadRequest {
pub struct LoadingRequest {
pub path: String,
}
@ -26,7 +26,7 @@ pub(crate) struct CleanupScene;
/// helper system that "converts" loadRequest events to `LoadRequested` resources
pub(crate) fn mark_load_requested(
mut load_requests: EventReader<LoadRequest>,
mut load_requests: EventReader<LoadingRequest>,
mut commands: Commands,
) {
let mut save_path: String = "".into();

View File

@ -12,7 +12,7 @@ pub use loading::*;
use bevy::core_pipeline::core_3d::{Camera3dDepthTextureUsage, ScreenSpaceTransmissionQuality};
use bevy::prelude::*;
use bevy::prelude::{App, IntoSystemConfigs, Plugin};
use bevy_gltf_blueprints::GltfBlueprintsSet;
use blenvy::GltfBlueprintsSet;
#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)]
pub enum SavingSet {
@ -24,32 +24,6 @@ pub enum LoadingSet {
Load,
}
// Plugin configuration
#[derive(Clone, Resource)]
pub struct SaveLoadConfig {
pub(crate) save_path: PathBuf,
pub(crate) component_filter: SceneFilter,
pub(crate) resource_filter: SceneFilter,
}
// define the plugin
pub struct SaveLoadPlugin {
pub component_filter: SceneFilter,
pub resource_filter: SceneFilter,
pub save_path: PathBuf,
}
impl Default for SaveLoadPlugin {
fn default() -> Self {
Self {
component_filter: SceneFilter::default(),
resource_filter: SceneFilter::default(),
save_path: PathBuf::from("scenes"),
}
}
}
#[derive(Component, Reflect, Debug, Default)]
#[reflect(Component)]
@ -67,16 +41,10 @@ impl Plugin for SaveLoadPlugin {
.register_type::<Camera3dDepthTextureUsage>()
.register_type::<ScreenSpaceTransmissionQuality>()
.register_type::<StaticEntitiesStorage>()
.add_event::<SaveRequest>()
.add_event::<LoadRequest>()
.add_event::<SavingRequest>()
.add_event::<LoadingRequest>()
.add_event::<LoadingFinished>()
.add_event::<SavingFinished>()
.insert_resource(SaveLoadConfig {
save_path: self.save_path.clone(),
component_filter: self.component_filter.clone(),
resource_filter: self.resource_filter.clone(),
})
.configure_sets(
Update,
(LoadingSet::Load).chain().before(GltfBlueprintsSet::Spawn), //.before(GltfComponentsSet::Injection)

View File

@ -1,22 +1,22 @@
use bevy::prelude::*;
use bevy::tasks::IoTaskPool;
use bevy_gltf_blueprints::{BlueprintName, InBlueprint, Library, SpawnHere};
use blenvy::{BlueprintName, InBlueprint, Library, SpawnHere};
use std::fs::File;
use std::io::Write;
use std::path::Path;
use crate::{Dynamic, DynamicEntitiesRoot, SaveLoadConfig, StaticEntitiesRoot};
use crate::{DynamicEntitiesRoot, SaveLoadConfig, StaticEntitiesRoot};
#[derive(Event, Debug)]
pub struct SaveRequest {
pub struct SavingRequest {
pub path: String,
}
#[derive(Event)]
pub struct SavingFinished;
pub fn should_save(save_requests: EventReader<SaveRequest>) -> bool {
pub fn should_save(save_requests: EventReader<SavingRequest>) -> bool {
!save_requests.is_empty()
}
@ -72,11 +72,12 @@ pub(crate) fn prepare_save_game(
});
}
}
pub(crate) fn save_game(world: &mut World) {
info!("saving");
let mut save_path: String = "".into();
let mut events = world.resource_mut::<Events<SaveRequest>>();
let mut events = world.resource_mut::<Events<SavingRequest>>();
for event in events.get_reader().read(&events) {
info!("SAVE EVENT !! {:?}", event);
@ -109,7 +110,10 @@ pub(crate) fn save_game(world: &mut World) {
.allow::<Children>()
.allow::<BlueprintName>()
.allow::<SpawnHere>()
.allow::<Dynamic>();
.allow::<Dynamic>()
;
// for root entities, it is the same EXCEPT we make sure parents are not included
let filter_root = filter.clone().deny::<Parent>();
@ -117,7 +121,8 @@ pub(crate) fn save_game(world: &mut World) {
let filter_resources = save_load_config
.resource_filter
.clone()
.allow::<StaticEntitiesStorage>();
.allow::<StaticEntitiesStorage>()
;
// for default stuff
let scene_builder = DynamicSceneBuilder::from_world(world)
@ -135,6 +140,7 @@ pub(crate) fn save_game(world: &mut World) {
.with_filter(filter_root.clone())
.with_resource_filter(filter_resources.clone());
// FIXME : add back
let mut dyn_scene_root = scene_builder_root
.extract_resources()
.extract_entities(

View File

@ -0,0 +1,214 @@
use std::fs::File;
use std::io::Write;
use std::path::Path;
use bevy::prelude::World;
use bevy::{prelude::*, tasks::IoTaskPool};
use crate::{BlenvyConfig, BlueprintInfo, Dynamic, FromBlueprint, RootEntity, SpawnBlueprint};
use super::{DynamicEntitiesRoot, OriginalParent, StaticEntitiesRoot};
#[derive(Event, Debug)]
pub struct SavingRequest {
pub path: String,
}
#[derive(Event)]
pub struct SaveFinished; // TODO: merge the the events above
/// resource that keeps track of the current save request
#[derive(Resource, Default)]
pub struct SavingRequested {
pub path: String,
}
pub fn process_save_requests(
mut saving_requests: EventReader<SavingRequest>,
mut commands: Commands,
) {
let mut save_path: String = "".into();
for saving_request in saving_requests.read() {
if !saving_request.path.is_empty() {
save_path.clone_from(&saving_request.path);
}
}
if !save_path.is_empty() {
commands.insert_resource(SavingRequested { path: save_path });
}
}
pub fn should_save(saving_requests: Option<Res<SavingRequested>>) -> bool {
resource_exists::<SavingRequested>(saving_requests)
}
// any child of dynamic/ saveable entities that is not saveable itself should be removed from the list of children
pub(crate) fn prepare_save_game(
saveables: Query<Entity, (With<Dynamic>, With<BlueprintInfo>)>,
root_entities: Query<Entity, Or<(With<DynamicEntitiesRoot>, Without<Parent>)>>, // With<DynamicEntitiesRoot>
dynamic_entities: Query<(Entity, &Parent, Option<&Children>), With<Dynamic>>,
_static_entities: Query<(Entity, &BlueprintInfo), With<StaticEntitiesRoot>>,
mut commands: Commands,
) {
for entity in saveables.iter() {
// FIXME : not sure about this one
commands.entity(entity).insert(SpawnBlueprint);
}
for (entity, parent, children) in dynamic_entities.iter() {
println!("prepare save game for entity");
let parent = parent.get();
if root_entities.contains(parent) {
commands.entity(entity).insert(RootEntity);
}
if let Some(children) = children {
for sub_child in children.iter() {
if !dynamic_entities.contains(*sub_child) {
commands.entity(*sub_child).insert(OriginalParent(entity));
commands.entity(entity).remove_children(&[*sub_child]);
}
}
}
}
/*for (_, blueprint_name) in static_entities.iter() {
let library_path: String = library.map_or_else(|| "", |l| l.0.to_str().unwrap()).into();
commands.insert_resource(StaticEntitiesStorage {
name: blueprint_name.0.clone(),
library_path,
});
}*/
}
pub(crate) fn save_game(world: &mut World) {
info!("saving");
let mut save_path: String = "".into();
let mut events = world.resource_mut::<Events<SavingRequest>>();
for event in events.get_reader().read(&events) {
info!("SAVE EVENT !! {:?}", event);
save_path.clone_from(&event.path);
}
events.clear();
let saveable_entities: Vec<Entity> = world
.query_filtered::<Entity, (With<Dynamic>, Without<RootEntity>)>()
// .query_filtered::<Entity, (With<Dynamic>, Without<FromBlueprint>, Without<RootEntity>)>()
.iter(world)
.collect();
let saveable_root_entities: Vec<Entity> = world
// .query_filtered::<Entity, (With<Dynamic>, With<RootEntity>)>()
.query_filtered::<Entity, (With<Dynamic>, Without<FromBlueprint>, With<RootEntity>)>()
.iter(world)
.collect();
info!("saveable entities {}", saveable_entities.len());
info!("saveable root entities {}", saveable_root_entities.len());
let config = world
.get_resource::<BlenvyConfig>()
.expect("Blenvy configuration should exist at this stage");
// we hardcode some of the always allowed types
let filter = config
.save_component_filter
.clone()
//.allow::<Parent>() // TODO: add back
.allow::<Children>()
.allow::<BlueprintInfo>()
.allow::<SpawnBlueprint>()
.allow::<Dynamic>()
/*.deny::<CameraRenderGraph>()
.deny::<CameraMainTextureUsages>()
.deny::<Handle<Mesh>>()
.deny::<Handle<StandardMaterial>>() */
;
// for root entities, it is the same EXCEPT we make sure parents are not included
let filter_root = filter.clone().deny::<Parent>();
let filter_resources = config
.clone()
.save_resource_filter
.deny::<Time<Real>>()
.clone();
//.allow::<StaticEntitiesStorage>();
// for default stuff
let scene_builder = DynamicSceneBuilder::from_world(world)
.with_filter(filter.clone())
.with_resource_filter(filter_resources.clone());
let dyn_scene = scene_builder
.extract_resources()
.extract_entities(saveable_entities.clone().into_iter())
.remove_empty_entities()
.build();
// for root entities
let scene_builder_root = DynamicSceneBuilder::from_world(world)
.with_filter(filter_root.clone())
.with_resource_filter(filter_resources.clone());
let mut __dyn_scene_root = scene_builder_root
.extract_resources()
.extract_entities(
saveable_root_entities.clone().into_iter(), // .chain(static_world_markers.into_iter()),
)
.remove_empty_entities()
.build();
// dyn_scene.entities.append(&mut dyn_scene_root.entities);
// dyn_scene.resources.append(&mut dyn_scene_root.resources);
let serialized_scene = dyn_scene
.serialize(&world.resource::<AppTypeRegistry>().read())
.expect("filtered scene should serialize correctly");
let save_path_assets = Path::new("assets")
//.join(&config.save_path)
.join(Path::new(save_path.as_str())); // Path::new(&save_load_config.save_path).join(Path::new(save_path.as_str()));
info!("saving game to {:?}", save_path_assets);
// world.send_event(SavingFinished);
let bla = save_path_assets.clone().to_string_lossy().into_owned();
#[cfg(not(target_arch = "wasm32"))]
IoTaskPool::get()
.spawn(async move {
// Write the scene RON data to file
File::create(save_path_assets)
.and_then(|mut file| file.write(serialized_scene.as_bytes()))
.expect("Error while writing save to file");
})
.detach();
let static_world_path = "levels/world.glb";
let fake_foo = format!("(dynamic: {bla}, static: {static_world_path})");
let real_save_path = format!("{bla}.save.ron");
#[cfg(not(target_arch = "wasm32"))]
IoTaskPool::get()
.spawn(async move {
// Write the scene RON data to file
File::create(real_save_path)
.and_then(|mut file| file.write(fake_foo.as_bytes()))
.expect("Error while writing scene to file");
})
.detach();
}
pub(crate) fn cleanup_save(
needs_parent_reset: Query<(Entity, &OriginalParent)>,
mut saving_finished: EventWriter<SaveFinished>,
mut commands: Commands,
) {
for (entity, original_parent) in needs_parent_reset.iter() {
commands.entity(original_parent.0).add_child(entity);
}
// commands.remove_resource::<StaticEntitiesStorage>();
saving_finished.send(SaveFinished);
commands.remove_resource::<SavingRequested>();
}

446
docs/avian/README.md Normal file
View File

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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 459 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 842 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 284 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1004 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 404 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 796 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1020 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 317 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 806 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 413 KiB

318
docs/quickstart/README.md Normal file
View File

@ -0,0 +1,318 @@
# Quickstart
This guide assumes you use Blender 4.2 or newer and have set it to English.
> [!NOTE]
> This is not a Blender tutorial. No worries, we will make sure you find all necessary buttons to click, but we will not explain in detail what Blender concepts like "collections" or "scenes" are. If you are not familiar with Blender, you might want to look up some basic tutorials first.
## Table of Contents
- [Install the Blender Addon](#install-the-blender-addon)
- [Setup the Bevy side](#setup-the-bevy-side)
- [Setup the Blender addon for your project](#setup-the-blender-addon-for-your-project)
- [Create a blueprint](#create-a-blueprint)
- [Compose the world](#compose-the-world)
- [Run your game](#run-your-game)
- [Next Steps](#next-steps)
## Install the Blender Addon
- Download `blenvy.zip` from the [release page](https://github.com/kaosat-dev/Blenvy/releases/tag/blenvy_0.1.0-alpha.1)
- Open Blender
- Drag and drop `blenvy.zip` into Blender
- <details>
<summary>This window should show up</summary>
<img src="img/install.png" width=50%>
</summary>
</details>
- Leave all settings as is and click on `OK`
Leave the scene open, we will get back to it later.
## Setup the Bevy side
Run the following commands:
```sh
cargo new my_game
cd my_game
cargo add bevy
cargo add blenvy
```
This guide will tell you to `cargo run` at multiple points. We expect that you are still in the `my_game` directory when you do so.
Now, replace the contents of `src/main.rs` with the following:
<details>
<summary>src/main.rs</summary>
```rust
use bevy::prelude::*;
use blenvy::*;
fn main() -> AppExit {
App::new()
.add_plugins((DefaultPlugins, BlenvyPlugin::default()))
// We need to register components to make them visible to Blenvy
.register_type::<Player>()
.add_systems(Startup, setup)
.run()
}
#[derive(Component, Reflect)]
#[reflect(Component)]
struct Player {
strength: f32,
perception: f32,
endurance: f32,
charisma: f32,
intelligence: f32,
agility: f32,
luck: f32,
}
fn setup(mut commands: Commands) {
commands.spawn((
BlueprintInfo::from_path("levels/World.glb"),
SpawnBlueprint,
HideUntilReady,
GameWorldTag,
));
}
```
</details>
`Player` is the component that we will add to our entities in Blender. It can contain any fields you want, but you need to remember to call `register_type` with it.
Running this won't work yet because the `levels/World.glb` we reference doesn't exist yet. We will create it in the next step.
## Setup the Blender addon for your project
Create a directory under `my_game` called `art`. Hop back into Blender and save the default scene as `my_game.blend` in the `art` directory we just created. Your file structure should now look like this:
<details>
<summary>File structure</summary>
<img src="img/art_file_structure.png" width = 50%/>
</details>
Now, clear the default scene of any objects or collections.
The fastest way to do this is to look for the collection named simply `Collection` all the way on the right of Blender. Right-click on it and select `Delete Hierachy`. For future reference, the place where you just did this is called the *outliner*.
> [!TIP]
> Technically, we could leave the default setup as-is since we are going to recreate it later anyway.
> However, we are starting from a clean slate in order to make it explicit what we are doing
> and which data is being exported to Bevy.
<details>
<summary>The default collection to delete</summary>
<img src="img/default_collection.png" width = 50%/>
</details>
Rename the default scene from `Scene` to `World` by clicking on "Scene" in the upper right corner of Blender and typing a new name.
<details>
<summary>Where to rename the scene</summary>
<img src="img/scene_name.png" width = 50%/>
</details>
Next, create a new scene by clicking on the icon that looks a bit like two pages, next to where you just renamed the scene, and then selecting `New`. Rename this scene to `Library`.
<details>
<summary>Where to create a new scene</summary>
<img src="img/create_scene.png" width = 50%/>
</details>
If everything went alright, entering the scene selector by pressing the icon next to the scene name should look like this:
<details>
<summary>Scene selector</summary>
<img src="img/scenes.png" width = 50%/>
</details>
If you switched around scenes to see if everything worked, make sure to switch back to the `Library` scene now.
Press `N`. This will open a side window. In it, click on the tab `Blenvy`. This is the main window through which you will interact with Blenvy.
<details>
<summary>Blenvy menu</summary>
<img src="img/blenvy_menu.png" width = 50%/>
</details>
You will see that two options are highlighted red because they are not yet configured.
For `level scenes`, click on the selector, select `World` and hit `+`. For `library scenes`, select `Library` and again hit `+`.
Your menu should now look like this:
<details>
<summary>Correctly setup Blenvy</summary>
<img src="img/blenvy_after_setup.png" width = 50%/>
</details>
Save your Blender file by pressing `Ctrl + S` on Windows or Linux and `Cmd + S` on macOS. You should now have a new asset under `my_game/assets/levels/World.glb` and a companion file containing metadata.
<details>
<summary>The newly created level assets</summary>
<img src="img/level_asset.png" width = 50%/>
</details>
Now run your game with `cargo run`. It may crash because the scene is empty, but don't worry. The point is that now will have generated another new file, namely `assets/registry.json`. This file contains the information about all components exported by your game so that Blenvy can pso them in Blender.
<details>
<summary>The final file structure for an empty world</summary>
<img src="img/registry.png" width = 50%/>
</details>
This concludes the setup portion of the guide.
## Create a blueprint
Alright, let's jump into the actual workflow you'll be using to create your game.
We will first create an object for the `Player` component we defined earlier.
While still in the `Library` scene, right-click on the `Scene Collection` in the outliner to the right. Select `New Collection`. Double-click on the new collection that appeared and rename it to `Player`. Click on it to have it selected.
<details>
<summary>The player collection</summary>
<img src="img/player_collection.png" width = 50%/>
</details>
Now, on the upper left of Blender, click on `Add` -> `Mesh` -> `Cube`. This will be our player.
Select the `Player` collection again. We will now add the `Player` component to it.
Go to the Blenvy menu we opened earlier. Select the icon in its upper left corner. This is where components are managed.
<details>
<summary>The component manager</summary>
<img src="img/component_manager.png" width = 50%/>
</details>
If your component manager says "Select an object to edit its components", make sure you have the `Player` collection we just created selected by clicking on it in the outliner.
Click on `Components`. If it says "No results found", we need to explicitly reload the registry. In the Blenvy menu, click on the gear icon (the fourth one from the left). In the submenu, click on the second icon. Your menu should now look like this:
<details>
<summary>The registry settings</summary>
<img src="img/registry_settings.png" width = 50%/>
</details>
Click on `reload registry` to reload it manually.
Go back to the component manager and you should now be greeted by a whole bunch of components:
<details>
<summary>The registry settings</summary>
<img src="img/lots_of_components.png" width = 50%/>
</details>
Type in `Player` and click on `my_game::Player`.
> [!TIP]
> If this does not show up, you have forgotten to call `register_type` with it.
Now click `Add`. You can now set all the player's fields in a neat Blender UI. Note that while we are only using `f32` here, you can use any kind of field, even complicated nested enums or structs.
<details>
<summary>The player component in Blender</summary>
<img src="img/player_component.png" width = 50%/>
</details>
Congratulations, you have just created your first blueprint.
## Compose the world
Let's populate our world now. Switch back to the `World` scene in Blender as described before.
We will add the following kinds of objects to our world:
- Some basic ground, which we will create directly in the world
- A camera
- A light
- The player character, which will be a blueprint instance
First, let's add a camera through `Add` -> `Camera`. Blender will make the camera face whatever direction the viewport is aimed at, which will probably be completely arbitrary.
Move the camera a bit so that it faces the world origin and is rotated the right way.
Pressing `0` on your numpad will show you a preview of what the camera sees.
You can just copy our transform if you like:
- Location:
- X: `0`
- Y: `-10`
- Z: `2`
- Rotation
- X: `80`
- Y: `0`
- Z: `0`
- Scale
- X: `1`
- Y: `1`
- Z: `1`
All transforms we are using assume that you are using the `XYZ Euler` mode for rotation.
<details>
<summary>The camera transform in Blender</summary>
<img src="img/camera_transform.png" width = 50%/>
</details>
Now, let's add a ground with `Add` -> `Mesh` -> `Cube`. Again scale and move it so that it lies neatly under the camera. Our transform looks like this:
- Location:
- X: `0`
- Y: `0`
- Z: `0`
- Rotation
- X: `0`
- Y: `0`
- Z: `0`
- Scale
- X: `5`
- Y: `5`
- Z: `0.1`
Add a directional light with `Add` -> `Light` -> `Sun`.
This light's position and scale do not matter, only its rotation is actually used. We are using the following:
- Rotation
- X: `20`
- Y: `20`
- Z: `0`
Finally, it is time to create an instance of our player blueprint! For this, go to `Add` -> `Collection Instance` and select `Player`. It will spawn at origin, which means directly inside the ground for us. Move it a bit up. A location of `Z: 1.1` should placed it just a tiny bit above the ground.
And with that, our Blender scene is finished! The final result should look like this:
<details>
<summary>The final Blender world scene</summary>
<img src="img/final_blender.png" width = 100%/>
</details>
Save your Blender file again. This will update the `World.glb` file in the `assets` directory and create a new file for our player blueprint under `assets/blueprints/Player.glb`, including again a metadata file.
<details>
<summary>The new blueprint files</summary>
<img src="img/blueprint_file.png" width = 50%/>
</details>
## Run your game
Congrats, you're done! Just run your game with `cargo run` and you should see your world in all its glory:
<details>
<summary>The final Bevy render</summary>
<img src="img/final_bevy.png" width = 50%/>
</details>
Okay, maybe not that much glory. But the important part is that the player is visible in the scene and has a `Player` component on it. You can now add more components to the player, create more blueprints, and populate your world with them. They can have animations, materials, etc. Have fun!
## Next Steps
- Read the [Blenvy for Bevy](../../crates/blenvy/README.md) documentation for more features on the Bevy side.
- Read the [Blenvy for Blender](../../tools/blenvy/README.md) documentation for more features on the Blender side.
- Read about the [Avian Physics Integration](../avian/readme.md) to learn how to setup colliders in Blender that will be used by the Avian physics engine in Bevy.

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 448 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

Some files were not shown because too many files have changed in this diff Show More