feat(materials): Materials libraries tooling & bevy code for material/texture reuse (#71)

* updates both gltf_auto_export & bevy_gltf_blueprints to avoid duplicating common materials across blueprints
* feat(tools/gltf_auto_export): added materials library export !
 * export all materials in use by blueprints to a separate temporary scene with cubes (for now)
   with the materials assigned
 * injecting MaterialInfo components to each blueprint above with Material name + library name
 * generated gltf is named based on the project/blend file, so multiple blend file projects
   can each have their own material libraries
 * added preferences & ui & handling to be able to toggle material library exports
* feat(bevy_gltf_blueprints): added support for materials library!
 * material injection (toggleable via the plugin configuration)
 * added example
 * added materials library + texture files + updated assets
 * added physics debug toggling
* updated documentation
* closes #63
This commit is contained in:
Mark Moissette 2023-12-12 13:21:53 +01:00 committed by GitHub
parent 529a68c844
commit e1aa510457
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
60 changed files with 6737 additions and 296 deletions

654
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -6,7 +6,9 @@ members = [
"examples/bevy_gltf_blueprints/basic/",
"examples/bevy_gltf_blueprints/basic_xpbd_physics/",
"examples/bevy_gltf_blueprints/animation/",
"examples/bevy_gltf_blueprints/multiple_levels/"
"examples/bevy_gltf_blueprints/multiple_levels/",
"examples/bevy_gltf_blueprints/materials/"
]
resolver = "2"

View File

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

View File

@ -26,7 +26,7 @@ Here's a minimal usage example:
# Cargo.toml
[dependencies]
bevy="0.12"
bevy_gltf_blueprints = { version = "0.3"}
bevy_gltf_blueprints = { version = "0.4"}
```
@ -64,7 +64,7 @@ fn spawn_blueprint(
Add the following to your `[dependencies]` section in `Cargo.toml`:
```toml
bevy_gltf_blueprints = "0.3"
bevy_gltf_blueprints = "0.4"
```
Or use `cargo add`:
@ -89,7 +89,7 @@ fn main() {
```
you may want to configure your "library"/"blueprints" path: (defaults to ```assets/models/library```) so the plugin know where to look for the blueprint files
you may want to configure your "library"/"blueprints" settings:
```rust no_run
use bevy::prelude::*;
@ -103,6 +103,8 @@ fn main() {
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()
}
)
@ -243,13 +245,40 @@ particularly from https://github.com/kaosat-dev/Blender_bevy_components_workflow
onward
## 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 https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/examples/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)
## Examples
https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/examples/basic
https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/examples/basic_xpbd_physics
https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/examples/animation
https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/examples/multiple_levels
https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/examples/materials
## Compatible Bevy versions
@ -258,8 +287,8 @@ The main branch is compatible with the latest Bevy release, while the branch `be
Compatibility of `bevy_gltf_blueprints` versions:
| `bevy_gltf_blueprints` | `bevy` |
| :-- | :-- |
| `0.3` | `0.12` |
| `0.1 -0.2` | `0.11` |
| `0.3 - 0.4` | `0.12` |
| `0.1 - 0.2` | `0.11` |
| branch `main` | `0.12` |
| branch `bevy_main` | `main` |

View File

@ -10,6 +10,9 @@ pub use animation::*;
pub mod aabb;
pub use aabb::*;
pub mod materials;
pub use materials::*;
pub mod clone_entity;
pub use clone_entity::*;
@ -47,8 +50,11 @@ 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)]
@ -77,7 +83,11 @@ pub struct BlueprintsPlugin {
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 {
@ -86,6 +96,8 @@ impl Default for BlueprintsPlugin {
format: GltfFormat::GLB,
library_folder: PathBuf::from("models/library"),
aabbs: false,
material_library: false,
material_library_folder: PathBuf::from("materials"),
}
}
}
@ -94,17 +106,27 @@ 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)
.register_type::<BlueprintName>()
.register_type::<MaterialInfo>()
.register_type::<SpawnHere>()
.register_type::<Animations>()
.insert_resource(BluePrintsConfig {
format: self.format.clone(),
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,
@ -118,6 +140,7 @@ impl Plugin for BlueprintsPlugin {
spawn_from_blueprints,
compute_scene_aabbs.run_if(aabbs_enabled),
apply_deferred.run_if(aabbs_enabled),
materials_inject.run_if(materials_library_enabled),
)
.chain()
.in_set(GltfBlueprintsSet::Spawn),

View File

@ -0,0 +1,96 @@
use std::path::Path;
use bevy::{
asset::{AssetServer, Assets, Handle},
ecs::{
component::Component,
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::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,
}
/// system that injects / replaces materials from material library
pub(crate) fn materials_inject(
mut blueprints_config: ResMut<BluePrintsConfig>,
material_infos: Query<(&MaterialInfo, &Children), Added<MaterialInfo>>,
with_materials_and_meshes: Query<(
With<Parent>,
With<Handle<StandardMaterial>>,
With<Handle<Mesh>>,
)>,
models: Res<Assets<bevy::gltf::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 my_gltf: Handle<Gltf> = asset_server.load(materials_path.clone());
let mat_gltf = models
.get(my_gltf.id())
.expect("material should have been preloaded");
if mat_gltf.named_materials.contains_key(material_name) {
let material = mat_gltf
.named_materials
.get(material_name)
.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

@ -82,7 +82,7 @@ pub(crate) fn spawn_from_blueprints(
transform: transform.clone(),
..Default::default()
},
bevy::prelude::Name::from(["scene_wrapper", &name.clone()].join("_")), //TODO: remove this convoluted bit
name.clone(),
// Parent(world) // FIXME/ would be good if this worked directly
SpawnedRoot,
BlueprintName(blupeprint_name.0.clone()),

View File

@ -22,7 +22,6 @@ pub(crate) fn update_spawned_root_first_child(
>,
mut commands: Commands,
// FIXME: not sure , but might be better if done at a more generic gltf level
animations: Query<&Animations>,
added_animation_players: Query<(Entity, &Parent), Added<AnimationPlayer>>,
) {
@ -64,7 +63,7 @@ pub(crate) fn update_spawned_root_first_child(
// also this is not something we want every time, this should be a settable parameter when requesting a spawn
// add missing name of entity, based on the wrapper's name
let name = name.clone().replace("scene_wrapper_", "");
let name = name.clone();
// this is our new actual entity
commands.entity(*root_entity).insert((

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -0,0 +1,14 @@
# Materials example/demo
Example of materials use & reuse (including textures) to avoid redundant materials in blueprints gltfs that lead to asset & memory bloat
- to be used together with ```gltf_auto_export``` version >0.6 with the "materials library" option for exports
- It shows you how ou can configure```Bevy_gltf_blueprints``` to support material libraries
- material library is [here](./assets/materials/)
## Running this example
```
cargo run --features bevy/dynamic_linking
```

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 243 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,25 @@
use bevy::{
ecs::system::Res,
input::{keyboard::KeyCode, Input},
prelude::{info, ResMut},
};
use bevy_rapier3d::{prelude::RapierConfiguration, render::DebugRenderContext};
pub fn pause_physics(mut physics_config: ResMut<RapierConfiguration>) {
info!("pausing physics");
physics_config.physics_pipeline_active = false;
}
pub fn resume_physics(mut physics_config: ResMut<RapierConfiguration>) {
info!("unpausing physics");
physics_config.physics_pipeline_active = true;
}
pub fn toggle_physics_debug(
mut debug_config: ResMut<DebugRenderContext>,
keycode: Res<Input<KeyCode>>,
) {
if keycode.just_pressed(KeyCode::D) {
debug_config.enabled = !debug_config.enabled;
}
}

View File

@ -0,0 +1,34 @@
pub mod physics_replace_proxies;
pub use physics_replace_proxies::*;
pub mod utils;
pub mod controls;
pub use controls::*;
use crate::state::GameState;
use bevy::prelude::*;
use bevy_gltf_blueprints::GltfBlueprintsSet;
use bevy_rapier3d::{
prelude::{NoUserData, RapierPhysicsPlugin},
render::RapierDebugRenderPlugin,
};
pub struct PhysicsPlugin;
impl Plugin for PhysicsPlugin {
fn build(&self, app: &mut App) {
app.add_plugins((
RapierPhysicsPlugin::<NoUserData>::default(),
RapierDebugRenderPlugin::default(),
))
.register_type::<AutoAABBCollider>()
.register_type::<physics_replace_proxies::Collider>()
.add_systems(
Update,
physics_replace_proxies.after(GltfBlueprintsSet::AfterSpawn),
)
.add_systems(Update, toggle_physics_debug)
.add_systems(OnEnter(GameState::InGame), resume_physics)
.add_systems(OnExit(GameState::InGame), pause_physics);
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,308 @@
use bevy_rapier3d::prelude::Velocity;
use rand::Rng;
use std::time::Duration;
use bevy::prelude::*;
use crate::{
assets::GameAssets,
state::{GameState, InAppRunning},
};
use bevy_gltf_blueprints::{
AnimationPlayerLink, Animations, BluePrintBundle, BlueprintName, GameWorldTag,
};
use super::{Fox, Player, Robot};
pub fn setup_game(
mut commands: Commands,
game_assets: Res<GameAssets>,
models: Res<Assets<bevy::gltf::Gltf>>,
mut next_game_state: ResMut<NextState<GameState>>,
) {
commands.insert_resource(AmbientLight {
color: Color::WHITE,
brightness: 0.2,
});
// here we actually spawn our game world/level
commands.spawn((
SceneBundle {
// note: because of this issue https://github.com/bevyengine/bevy/issues/10436, "world" is now a gltf file instead of a scene
scene: models
.get(game_assets.world.id())
.expect("main level should have been loaded")
.scenes[0]
.clone(),
..default()
},
bevy::prelude::Name::from("world"),
GameWorldTag,
InAppRunning,
));
next_game_state.set(GameState::InGame)
}
pub fn spawn_test(
keycode: Res<Input<KeyCode>>,
mut commands: Commands,
mut game_world: Query<(Entity, &Children), With<GameWorldTag>>,
) {
if keycode.just_pressed(KeyCode::T) {
let world = game_world.single_mut();
let world = world.1[0];
let mut rng = rand::thread_rng();
let range = 8.5;
let x: f32 = rng.gen_range(-range..range);
let y: f32 = rng.gen_range(-range..range);
let mut rng = rand::thread_rng();
let range = 0.8;
let vel_x: f32 = rng.gen_range(-range..range);
let vel_y: f32 = rng.gen_range(2.0..2.5);
let vel_z: f32 = rng.gen_range(-range..range);
let name_index: u64 = rng.gen();
let new_entity = commands
.spawn((
BluePrintBundle {
blueprint: BlueprintName("Watermelon2".to_string()),
transform: TransformBundle::from_transform(Transform::from_xyz(x, 3.0, y)),
..Default::default()
},
bevy::prelude::Name::from(format!("Watermelon{}", name_index)),
// BlueprintName("Health_Pickup".to_string()),
// SpawnHere,
// TransformBundle::from_transform(Transform::from_xyz(x, 2.0, y)),
Velocity {
linvel: Vec3::new(vel_x, vel_y, vel_z),
angvel: Vec3::new(0.0, 0.0, 0.0),
},
))
.id();
commands.entity(world).add_child(new_entity);
}
}
// 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();
}
}
}
// example of changing animation of entities based on proximity to the player, this time for the "robot" entities (Tag component)
pub fn animation_change_on_proximity_robots(
players: Query<&GlobalTransform, With<Player>>,
animated_robots: Query<(&GlobalTransform, &AnimationPlayerLink, &Animations), With<Robot>>,
mut animation_players: Query<&mut AnimationPlayer>,
) {
for player_transforms in players.iter() {
for (robot_tranforms, link, animations) in animated_robots.iter() {
let distance = player_transforms
.translation()
.distance(robot_tranforms.translation());
let mut anim_name = "Idle";
if distance < 8.5 {
anim_name = "Jump";
} else if distance >= 8.5 && distance < 10.0 {
anim_name = "Scan";
} else if distance >= 10.0 && distance < 15.0 {
anim_name = "Idle";
}
// 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();
}
}
}
pub fn animation_control(
animated_enemies: Query<(&AnimationPlayerLink, &Animations), With<Robot>>,
animated_foxes: Query<(&AnimationPlayerLink, &Animations), With<Fox>>,
mut animation_players: Query<&mut AnimationPlayer>,
keycode: Res<Input<KeyCode>>,
// mut entities_with_animations : Query<(&mut AnimationPlayer, &mut Animations)>,
) {
// robots
if keycode.just_pressed(KeyCode::B) {
for (link, animations) in animated_enemies.iter() {
let mut animation_player = animation_players.get_mut(link.0).unwrap();
let anim_name = "Scan";
animation_player
.play_with_transition(
animations
.named_animations
.get(anim_name)
.expect("animation name should be in the list")
.clone(),
Duration::from_secs(5),
)
.repeat();
}
}
// foxes
if keycode.just_pressed(KeyCode::W) {
for (link, animations) in animated_foxes.iter() {
let mut animation_player = animation_players.get_mut(link.0).unwrap();
let anim_name = "Walk";
animation_player
.play_with_transition(
animations
.named_animations
.get(anim_name)
.expect("animation name should be in the list")
.clone(),
Duration::from_secs(5),
)
.repeat();
}
}
if keycode.just_pressed(KeyCode::X) {
for (link, animations) in animated_foxes.iter() {
let mut animation_player = animation_players.get_mut(link.0).unwrap();
let anim_name = "Run";
animation_player
.play_with_transition(
animations
.named_animations
.get(anim_name)
.expect("animation name should be in the list")
.clone(),
Duration::from_secs(5),
)
.repeat();
}
}
if keycode.just_pressed(KeyCode::C) {
for (link, animations) in animated_foxes.iter() {
let mut animation_player = animation_players.get_mut(link.0).unwrap();
let anim_name = "Survey";
animation_player
.play_with_transition(
animations
.named_animations
.get(anim_name)
.expect("animation name should be in the list")
.clone(),
Duration::from_secs(5),
)
.repeat();
}
}
/* Improveement ideas for the future
// a bit more ideal API
if keycode.just_pressed(KeyCode::B) {
for (animation_player, animations) in animated_enemies.iter() {
let anim_name = "Scan";
if animations.named_animations.contains_key(anim_name) {
let clip = animations.named_animations.get(anim_name).unwrap();
animation_player.play_with_transition(clip.clone(), Duration::from_secs(5)).repeat();
}
}
}
// even better API
if keycode.just_pressed(KeyCode::B) {
for (animation_player, animations) in animated_enemies.iter() {
animation_player.play_with_transition("Scan", Duration::from_secs(5)).repeat(); // with a merged animationPlayer + animations storage
// alternative, perhaps more realistic, and better seperation of concerns
animation_player.play_with_transition(animations, "Scan", Duration::from_secs(5)).repeat();
}
}*/
/*for (mut anim_player, animations) in entities_with_animations.iter_mut(){
if keycode.just_pressed(KeyCode::W) {
let anim_name = "Walk";
if animations.named_animations.contains_key(anim_name) {
let clip = animations.named_animations.get(anim_name).unwrap();
anim_player.play_with_transition(clip.clone(), Duration::from_secs(5)).repeat();
}
}
if keycode.just_pressed(KeyCode::X) {
let anim_name = "Run";
if animations.named_animations.contains_key(anim_name) {
let clip = animations.named_animations.get(anim_name).unwrap();
anim_player.play_with_transition(clip.clone(), Duration::from_secs(5)).repeat();
}
}
if keycode.just_pressed(KeyCode::C) {
let anim_name = "Survey";
if animations.named_animations.contains_key(anim_name) {
let clip = animations.named_animations.get(anim_name).unwrap();
anim_player.play_with_transition(clip.clone(), Duration::from_secs(5)).repeat();
}
}
if keycode.just_pressed(KeyCode::S) {
let anim_name = "Scan";
if animations.named_animations.contains_key(anim_name) {
let clip = animations.named_animations.get(anim_name).unwrap();
anim_player.play_with_transition(clip.clone(), Duration::from_secs(5)).repeat();
}
}
if keycode.just_pressed(KeyCode::I) {
let anim_name = "Idle";
if animations.named_animations.contains_key(anim_name) {
let clip = animations.named_animations.get(anim_name).unwrap();
anim_player.play_with_transition(clip.clone(), Duration::from_secs(5)).repeat();
}
}
}*/
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -37,7 +37,7 @@ This [Blender addon](./)
![blender addon use3](./docs/blender_addon_use3.png)
- export folder
- export folder: root folder to export models too
- pick your main (level) scenes and library scenes (see the chapter about Blueprints below)
- click in the scene picker & select your scene
@ -51,7 +51,15 @@ This [Blender addon](./)
![select scene3](./docs/blender_addon_add_scene3.png)
- export blueprints: check this if you want to automatically export blueprints (default: True)
- blueprints path: the path to export blueprints to , relative to the main **export folder** (default: library)
- export materials library: check this if you want to automatically export material libraries (default: False)
please read the dedicated section below for more information
> This only works together with blueprints !
- materials path: where to export materials to
* and your standard gltf export parameters in the **gltf** panel
@ -92,6 +100,26 @@ You can enable this option to automatically replace all the **collection instanc
![exported collections](./docs/exported_collections.png)
### Materials
You can enable this option to automatically generate a **material library** file that combines all the materials in use in your blueprints.
![material_library](./docs/blender_addon_materials2.png)
Since each blueprint is normally a completely independant gltf file, without this option, if you have a material with a large texture for example,
**ALL** of your blueprints using that material will embed that large texture, leading to **significant bloat & memory use**.
- When this option is enabled, you get a single material library per Blender project, and a **MaterialInfo** component is inserted into each object using a material.
- The correct material will then be inserted on the Bevy side (that loads any number of material libraries that you need) into the correct mesh (see the configuration
options in **bevy_gltf_blueprints** for more information on that)
- Only one material per object is supported at this stage, ie the last material slot's material is the one that is going to be used
![material_library](./docs/blender_addon_materials.png)
TLDR: Use this option to make sure that each blueprint file does not contain a copy of the same materials
#### Process
This is the internal logic of the export process with blueprints

View File

@ -1,7 +1,7 @@
bl_info = {
"name": "gltf_auto_export",
"author": "kaosigh",
"version": (0, 5, 4),
"version": (0, 6, 0),
"blender": (3, 4, 0),
"location": "File > Import-Export",
"description": "glTF/glb auto-export",

View File

@ -6,6 +6,7 @@ from .helpers_scenes import (get_scenes, )
from .helpers_collections import (get_exportable_collections, get_collections_per_scene)
from .helpers_export import (export_main_scene, export_blueprints_from_collections)
from .helpers import (check_if_blueprints_exist, check_if_blueprint_on_disk)
from .materials import cleanup_materials, clear_material_info, clear_materials_scene, export_materials, generate_materials_scenes, get_all_materials
from .config import scene_key
"""Main function"""
@ -56,6 +57,8 @@ def auto_export(changes_per_scene, changed_export_parameters):
export_blueprints = getattr(addon_prefs,"export_blueprints")
export_output_folder = getattr(addon_prefs,"export_output_folder")
export_materials_library = getattr(addon_prefs,"export_materials_library")
[main_scene_names, level_scenes, library_scene_names, library_scenes] = get_scenes(addon_prefs)
print("main scenes", main_scene_names, "library_scenes", library_scene_names)
@ -94,13 +97,19 @@ def auto_export(changes_per_scene, changed_export_parameters):
# we need to re_export everything if the export parameters have been changed
collections_to_export = collections if changed_export_parameters else collections_to_export
collections_per_scene = get_collections_per_scene(collections_to_export, library_scenes)
# collections that do not come from a library should not be exported as seperate blueprints
library_collections = [name for sublist in collections_per_scene.values() for name in sublist]
collections_to_export = list(set(collections_to_export).intersection(set(library_collections)))
# since materials export adds components we need to call this before blueprints are exported
# export materials & inject materials components into relevant objects
if export_materials_library:
export_materials(collections, library_scenes, folder_path, addon_prefs)
print("--------------")
print("collections: all:", collections)
print("collections: changed:", changed_collections)
@ -133,13 +142,16 @@ def auto_export(changes_per_scene, changed_export_parameters):
library_scene = bpy.data.scenes[scene_name]
export_blueprints_from_collections(collections_to_export, library_scene, folder_path, addon_prefs)
# reset current scene from backup
bpy.context.window.scene = old_current_scene
# reset selections
for obj in old_selections:
obj.select_set(True)
if export_materials_library:
cleanup_materials(collections, library_scenes)
else:
for scene_name in main_scene_names:
export_main_scene(bpy.data.scenes[scene_name], folder_path, addon_prefs)

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -78,10 +78,15 @@ def export_collections(collections, folder_path, library_scene, addon_prefs, glt
layerColl = recurLayerCollection(layer_collection, collection_name)
# set active collection to the collection
bpy.context.view_layer.active_layer_collection = layerColl
gltf_output_path = os.path.join(folder_path, collection_name)
export_settings = { **gltf_export_preferences, 'use_active_scene': True, 'use_active_collection': True, 'use_active_collection_with_nested':True} #'use_visible': False,
export_settings = { **gltf_export_preferences, 'use_active_scene': True, 'use_active_collection': True, 'use_active_collection_with_nested':True}
# if we are using the material library option, do not export materials, use placeholder instead
export_materials_library = getattr(addon_prefs,"export_materials_library")
if export_materials_library:
export_settings['export_materials'] = 'PLACEHOLDER'
export_gltf(gltf_output_path, export_settings)
# reset active collection to the one we save before

View File

@ -0,0 +1,123 @@
from .helpers_export import export_gltf, generate_gltf_export_preferences
from .helpers import traverse_tree
import bpy
import os
from pathlib import Path
# get materials per object, and injects the materialInfo component
def get_materials(object):
material_slots = object.material_slots
used_materials_names = []
#materials_per_object = {}
current_project_name = Path(bpy.context.blend_data.filepath).stem
for m in material_slots:
material = m.material
# print(" slot", m, "material", material)
used_materials_names.append(material.name)
# TODO:, also respect slots & export multiple materials if applicable !
object['MaterialInfo'] = '(name: "'+material.name+'", source: "'+current_project_name + '")'
return used_materials_names
def clear_material_info(collection_names, library_scenes):
for scene in library_scenes:
root_collection = scene.collection
for cur_collection in traverse_tree(root_collection):
if cur_collection.name in collection_names:
for object in cur_collection.all_objects:
if 'MaterialInfo' in dict(object): # FIXME: hasattr does not work ????
del object["MaterialInfo"]
def get_all_materials(collection_names, library_scenes):
#print("collecton", layerColl, "otot", layerColl.all_objects) #all_objects
used_material_names = []
for scene in library_scenes:
root_collection = scene.collection
for cur_collection in traverse_tree(root_collection):
if cur_collection.name in collection_names:
#print("collection: ", cur_collection.name)
for object in cur_collection.all_objects:
# print(" object:", object.name)
used_material_names = used_material_names + get_materials(object)
# we only want unique names
used_material_names = list(set(used_material_names))
return used_material_names
def make_material_object(name, location, rotation, scale, material):
original_active_object = bpy.context.active_object
# bpy.ops.object.empty_add(type='PLAIN_AXES', location=location, rotation=rotation, scale=scale)
bpy.ops.mesh.primitive_cube_add(size=0.1, location=location)
object = bpy.context.active_object
object.name = name
#obj.scale = scale # scale is not set correctly ?????
if object.data.materials:
# assign to 1st material slot
object.data.materials[0] = material
else:
# no slots
object.data.materials.append(material)
bpy.context.view_layer.objects.active = original_active_object
return object
# generates a materials scene:
def generate_materials_scenes(used_material_names):
temp_scene = bpy.data.scenes.new(name="__materials_scene")
bpy.context.window.scene = temp_scene
for index, material_name in enumerate(used_material_names):
material = bpy.data.materials[material_name]
make_material_object("Material_"+material_name, [index * 0.2,0,0], [], [], material)
# we set our active scene to be this one : this is needed otherwise the stand-in objects get generated in the wrong scene
return temp_scene
def clear_materials_scene(temp_scene):
root_collection = temp_scene.collection
scene_objects = [o for o in root_collection.objects]
for object in scene_objects:
bpy.data.objects.remove(object, do_unlink=True)
bpy.data.scenes.remove(temp_scene)
# exports the materials used inside the current project:
# the name of the output path is <materials_folder>/<name_of_your_blend_file>_materials_library.gltf/glb
def export_materials(collections, library_scenes, folder_path, addon_prefs):
gltf_export_preferences = generate_gltf_export_preferences(addon_prefs)
export_materials_path = getattr(addon_prefs,"export_materials_path")
used_material_names = get_all_materials(collections, library_scenes)
current_project_name = Path(bpy.context.blend_data.filepath).stem
print("materials", used_material_names)
# save the current active scene
current_scene = bpy.context.window.scene
mat_scene = generate_materials_scenes(used_material_names)
gltf_output_path = os.path.join(folder_path, export_materials_path, current_project_name + "_materials_library")
print(" exporting Materials to", gltf_output_path, ".gltf/glb")
export_settings = { **gltf_export_preferences,
'use_active_scene': True,
'use_active_collection':True,
'use_active_collection_with_nested':True,
'use_visible': False,
'use_renderable': False,
'export_apply':True
}
export_gltf(gltf_output_path, export_settings)
# remove materials scenes
clear_materials_scene(mat_scene)
# reset scene to previously selected scene
bpy.context.window.scene = current_scene
def cleanup_materials(collections, library_scenes):
# remove temporary components
clear_material_info(collections, library_scenes)

View File

@ -18,6 +18,10 @@ AutoExportGltfPreferenceNames = [
'export_blueprints',
'export_blueprints_path',
'export_materials_library',
'export_materials_path',
'main_scenes',
'library_scenes',
'main_scenes_index',
@ -62,6 +66,17 @@ class AutoExportGltfAddonPreferences(AddonPreferences):
default='library'
)
export_materials_library: BoolProperty(
name='Export materials library',
description='remove materials from blueprints and use the material library instead',
default=False
)
export_materials_path: StringProperty(
name='Materials path',
description='path to export the materials libraries to (relative to the root folder)',
default='materials'
)
main_scenes: CollectionProperty(name="main scenes", type=CUSTOM_PG_sceneName)
main_scenes_index: IntProperty(name = "Index for main scenes list", default = 0)

View File

@ -285,6 +285,11 @@ class GLTF_PT_auto_export_blueprints(bpy.types.Panel):
layout.prop(operator, "export_blueprints")
layout.prop(operator, "export_blueprints_path")
# materials
layout.prop(operator, "export_materials_library")
layout.prop(operator, "export_materials_path")
class GLTF_PT_auto_export_collections_list(bpy.types.Panel):
bl_space_type = 'FILE_BROWSER'
bl_region_type = 'TOOL_PROPS'