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:
parent
529a68c844
commit
e1aa510457
File diff suppressed because it is too large
Load Diff
|
@ -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"
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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` |
|
||||
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()),
|
||||
|
|
|
@ -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
|
@ -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"
|
|
@ -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
|
||||
```
|
|
@ -0,0 +1 @@
|
|||
({})
|
|
@ -0,0 +1,9 @@
|
|||
({
|
||||
"world":File (path: "models/Level1.glb"),
|
||||
"models": Folder (
|
||||
path: "models/library",
|
||||
),
|
||||
"materials": Folder (
|
||||
path: "materials",
|
||||
),
|
||||
})
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
After Width: | Height: | Size: 243 KiB |
Binary file not shown.
After Width: | Height: | Size: 174 KiB |
|
@ -0,0 +1,5 @@
|
|||
use bevy::prelude::*;
|
||||
use bevy_asset_loader::prelude::*;
|
||||
|
||||
#[derive(AssetCollection, Resource)]
|
||||
pub struct CoreAssets {}
|
|
@ -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>>,
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
pub mod assets_core;
|
||||
pub use assets_core::*;
|
||||
|
||||
pub mod assets_game;
|
||||
pub use assets_game::*;
|
||||
|
||||
use bevy::prelude::*;
|
||||
use bevy_asset_loader::prelude::*;
|
||||
|
||||
use crate::state::AppState;
|
||||
|
||||
pub struct AssetsPlugin;
|
||||
impl Plugin for AssetsPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app
|
||||
// load core assets (ie assets needed in the main menu, and everywhere else before loading more assets in game)
|
||||
.add_loading_state(
|
||||
LoadingState::new(AppState::CoreLoading).continue_to_state(AppState::MenuRunning),
|
||||
)
|
||||
.add_dynamic_collection_to_loading_state::<_, StandardDynamicAssetCollection>(
|
||||
AppState::CoreLoading,
|
||||
"assets_core.assets.ron",
|
||||
)
|
||||
.add_collection_to_loading_state::<_, CoreAssets>(AppState::CoreLoading)
|
||||
// load game assets
|
||||
.add_loading_state(
|
||||
LoadingState::new(AppState::AppLoading).continue_to_state(AppState::AppRunning),
|
||||
)
|
||||
.add_dynamic_collection_to_loading_state::<_, StandardDynamicAssetCollection>(
|
||||
AppState::AppLoading,
|
||||
"assets_game.assets.ron",
|
||||
)
|
||||
.add_collection_to_loading_state::<_, GameAssets>(AppState::AppLoading);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
use bevy::core_pipeline::bloom::{BloomCompositeMode, BloomSettings};
|
||||
use bevy::core_pipeline::tonemapping::{DebandDither, Tonemapping};
|
||||
use bevy::prelude::*;
|
||||
|
||||
use super::CameraTrackingOffset;
|
||||
|
||||
pub fn camera_replace_proxies(
|
||||
mut commands: Commands,
|
||||
mut added_cameras: Query<(Entity, &mut Camera), (Added<Camera>, With<CameraTrackingOffset>)>,
|
||||
) {
|
||||
for (entity, mut camera) in added_cameras.iter_mut() {
|
||||
info!("detected added camera, updating proxy");
|
||||
camera.hdr = true;
|
||||
commands
|
||||
.entity(entity)
|
||||
.insert(DebandDither::Enabled)
|
||||
.insert(Tonemapping::BlenderFilmic)
|
||||
.insert(BloomSettings {
|
||||
intensity: 0.01,
|
||||
composite_mode: BloomCompositeMode::Additive,
|
||||
..default()
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
mod lighting_replace_proxies;
|
||||
use lighting_replace_proxies::*;
|
||||
|
||||
use bevy::pbr::{DirectionalLightShadowMap, NotShadowCaster};
|
||||
use bevy::prelude::*;
|
||||
|
||||
pub struct LightingPlugin;
|
||||
impl Plugin for LightingPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app
|
||||
.insert_resource(DirectionalLightShadowMap { size: 4096 })
|
||||
// FIXME: adding these since they are missing
|
||||
.register_type::<NotShadowCaster>()
|
||||
|
||||
.add_systems(PreUpdate, lighting_replace_proxies) // FIXME: you should actually run this in a specific state most likely
|
||||
;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,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()
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,175 @@
|
|||
use bevy::prelude::*;
|
||||
use bevy::render::mesh::{MeshVertexAttributeId, PrimitiveTopology, VertexAttributeValues};
|
||||
// TAKEN VERBATIB FROM https://github.com/janhohenheim/foxtrot/blob/src/util/trait_extension.rs
|
||||
|
||||
pub(crate) trait Vec3Ext: Copy {
|
||||
fn is_approx_zero(self) -> bool;
|
||||
fn split(self, up: Vec3) -> SplitVec3;
|
||||
}
|
||||
impl Vec3Ext for Vec3 {
|
||||
#[inline]
|
||||
fn is_approx_zero(self) -> bool {
|
||||
self.length_squared() < 1e-5
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn split(self, up: Vec3) -> SplitVec3 {
|
||||
let vertical = up * self.dot(up);
|
||||
let horizontal = self - vertical;
|
||||
SplitVec3 {
|
||||
vertical,
|
||||
horizontal,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub(crate) struct SplitVec3 {
|
||||
pub(crate) vertical: Vec3,
|
||||
pub(crate) horizontal: Vec3,
|
||||
}
|
||||
|
||||
pub(crate) trait Vec2Ext: Copy {
|
||||
fn is_approx_zero(self) -> bool;
|
||||
fn x0y(self) -> Vec3;
|
||||
}
|
||||
impl Vec2Ext for Vec2 {
|
||||
#[inline]
|
||||
fn is_approx_zero(self) -> bool {
|
||||
self.length_squared() < 1e-5
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn x0y(self) -> Vec3 {
|
||||
Vec3::new(self.x, 0., self.y)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) trait MeshExt {
|
||||
fn transform(&mut self, transform: Transform);
|
||||
fn transformed(&self, transform: Transform) -> Mesh;
|
||||
fn read_coords_mut(&mut self, id: impl Into<MeshVertexAttributeId>) -> &mut Vec<[f32; 3]>;
|
||||
fn search_in_children<'a>(
|
||||
parent: Entity,
|
||||
children: &'a Query<&Children>,
|
||||
meshes: &'a Assets<Mesh>,
|
||||
mesh_handles: &'a Query<&Handle<Mesh>>,
|
||||
) -> Vec<(Entity, &'a Mesh)>;
|
||||
}
|
||||
|
||||
impl MeshExt for Mesh {
|
||||
fn transform(&mut self, transform: Transform) {
|
||||
for coords in self.read_coords_mut(Mesh::ATTRIBUTE_POSITION.clone()) {
|
||||
let vec3 = (*coords).into();
|
||||
let transformed = transform.transform_point(vec3);
|
||||
*coords = transformed.into();
|
||||
}
|
||||
for normal in self.read_coords_mut(Mesh::ATTRIBUTE_NORMAL.clone()) {
|
||||
let vec3 = (*normal).into();
|
||||
let transformed = transform.rotation.mul_vec3(vec3);
|
||||
*normal = transformed.into();
|
||||
}
|
||||
}
|
||||
|
||||
fn transformed(&self, transform: Transform) -> Mesh {
|
||||
let mut mesh = self.clone();
|
||||
mesh.transform(transform);
|
||||
mesh
|
||||
}
|
||||
|
||||
fn read_coords_mut(&mut self, id: impl Into<MeshVertexAttributeId>) -> &mut Vec<[f32; 3]> {
|
||||
// Guaranteed by Bevy for the current usage
|
||||
match self
|
||||
.attribute_mut(id)
|
||||
.expect("Failed to read unknown mesh attribute")
|
||||
{
|
||||
VertexAttributeValues::Float32x3(values) => values,
|
||||
// Guaranteed by Bevy for the current usage
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn search_in_children<'a>(
|
||||
parent: Entity,
|
||||
children_query: &'a Query<&Children>,
|
||||
meshes: &'a Assets<Mesh>,
|
||||
mesh_handles: &'a Query<&Handle<Mesh>>,
|
||||
) -> Vec<(Entity, &'a Mesh)> {
|
||||
if let Ok(children) = children_query.get(parent) {
|
||||
let mut result: Vec<_> = children
|
||||
.iter()
|
||||
.filter_map(|entity| mesh_handles.get(*entity).ok().map(|mesh| (*entity, mesh)))
|
||||
.map(|(entity, mesh_handle)| {
|
||||
(
|
||||
entity,
|
||||
meshes
|
||||
.get(mesh_handle)
|
||||
.expect("Failed to get mesh from handle"),
|
||||
)
|
||||
})
|
||||
.map(|(entity, mesh)| {
|
||||
assert_eq!(mesh.primitive_topology(), PrimitiveTopology::TriangleList);
|
||||
(entity, mesh)
|
||||
})
|
||||
.collect();
|
||||
let mut inner_result = children
|
||||
.iter()
|
||||
.flat_map(|entity| {
|
||||
Self::search_in_children(*entity, children_query, meshes, mesh_handles)
|
||||
})
|
||||
.collect();
|
||||
result.append(&mut inner_result);
|
||||
result
|
||||
} else {
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) trait F32Ext: Copy {
|
||||
fn is_approx_zero(self) -> bool;
|
||||
fn squared(self) -> f32;
|
||||
fn lerp(self, other: f32, ratio: f32) -> f32;
|
||||
}
|
||||
|
||||
impl F32Ext for f32 {
|
||||
#[inline]
|
||||
fn is_approx_zero(self) -> bool {
|
||||
self.abs() < 1e-5
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn squared(self) -> f32 {
|
||||
self * self
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn lerp(self, other: f32, ratio: f32) -> f32 {
|
||||
self.mul_add(1. - ratio, other * ratio)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) trait TransformExt: Copy {
|
||||
fn horizontally_looking_at(self, target: Vec3, up: Vec3) -> Transform;
|
||||
fn lerp(self, other: Transform, ratio: f32) -> Transform;
|
||||
}
|
||||
|
||||
impl TransformExt for Transform {
|
||||
fn horizontally_looking_at(self, target: Vec3, up: Vec3) -> Transform {
|
||||
let direction = target - self.translation;
|
||||
let horizontal_direction = direction - up * direction.dot(up);
|
||||
let look_target = self.translation + horizontal_direction;
|
||||
self.looking_at(look_target, up)
|
||||
}
|
||||
|
||||
fn lerp(self, other: Transform, ratio: f32) -> Transform {
|
||||
let translation = self.translation.lerp(other.translation, ratio);
|
||||
let rotation = self.rotation.slerp(other.rotation, ratio);
|
||||
let scale = self.scale.lerp(other.scale, ratio);
|
||||
Transform {
|
||||
translation,
|
||||
rotation,
|
||||
scale,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
pub mod relationships_insert_dependant_components;
|
||||
pub use relationships_insert_dependant_components::*;
|
||||
|
||||
use bevy::prelude::*;
|
||||
|
||||
pub struct EcsRelationshipsPlugin;
|
||||
impl Plugin for EcsRelationshipsPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
use bevy::prelude::*;
|
||||
|
||||
pub fn insert_dependant_component<
|
||||
Dependant: Component,
|
||||
Dependency: Component + std::default::Default,
|
||||
>(
|
||||
mut commands: Commands,
|
||||
entities_without_depency: Query<(Entity, &Name), (With<Dependant>, Without<Dependency>)>,
|
||||
) {
|
||||
for (entity, name) in entities_without_depency.iter() {
|
||||
let name = name.clone().to_string();
|
||||
commands.entity(entity).insert(Dependency::default());
|
||||
warn!("found an entity called {} with a {} component but without an {}, please check your assets", name.clone(), std::any::type_name::<Dependant>(), std::any::type_name::<Dependency>());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,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();
|
||||
}
|
||||
}
|
||||
}*/
|
||||
}
|
|
@ -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() })
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
use bevy::prelude::*;
|
||||
use bevy_editor_pls::prelude::*;
|
||||
|
||||
mod core;
|
||||
use crate::core::*;
|
||||
|
||||
pub mod assets;
|
||||
use assets::*;
|
||||
|
||||
pub mod state;
|
||||
use state::*;
|
||||
|
||||
mod game;
|
||||
use game::*;
|
||||
|
||||
mod test_components;
|
||||
use test_components::*;
|
||||
|
||||
fn main() {
|
||||
App::new()
|
||||
.add_plugins((
|
||||
DefaultPlugins.set(AssetPlugin::default()),
|
||||
// editor
|
||||
EditorPlugin::default(),
|
||||
// our custom plugins
|
||||
StatePlugin,
|
||||
AssetsPlugin,
|
||||
CorePlugin, // reusable plugins
|
||||
GamePlugin, // specific to our game
|
||||
ComponentsTestPlugin, // Showcases different type of components /structs
|
||||
))
|
||||
.run();
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
use bevy::app::AppExit;
|
||||
use bevy::prelude::*;
|
||||
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Default, States)]
|
||||
pub enum AppState {
|
||||
#[default]
|
||||
CoreLoading,
|
||||
MenuRunning,
|
||||
AppLoading,
|
||||
AppRunning,
|
||||
AppEnding,
|
||||
|
||||
// FIXME: not sure
|
||||
LoadingGame,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Default, States)]
|
||||
pub enum GameState {
|
||||
#[default]
|
||||
None,
|
||||
|
||||
InMenu,
|
||||
InGame,
|
||||
|
||||
InGameOver,
|
||||
|
||||
InSaving,
|
||||
InLoading,
|
||||
}
|
||||
|
||||
// tag components for all entities within a certain state (for despawning them if needed) , FIXME: seems kinda hack-ish
|
||||
#[derive(Component)]
|
||||
pub struct InCoreLoading;
|
||||
#[derive(Component, Default)]
|
||||
pub struct InMenuRunning;
|
||||
#[derive(Component)]
|
||||
pub struct InAppLoading;
|
||||
#[derive(Component)]
|
||||
pub struct InAppRunning;
|
||||
|
||||
// components for tagging in game vs in game menu stuff
|
||||
#[derive(Component, Default)]
|
||||
pub struct InMainMenu;
|
||||
#[derive(Component, Default)]
|
||||
pub struct InMenu;
|
||||
#[derive(Component, Default)]
|
||||
pub struct InGame;
|
||||
|
||||
pub struct StatePlugin;
|
||||
impl Plugin for StatePlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_state::<AppState>().add_state::<GameState>();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,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>>();
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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 |
|
@ -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
|
||||
|
|
|
@ -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)
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
|
Loading…
Reference in New Issue