#![warn(missing_docs)]
use bevy_app::{App, Plugin};
use bevy_asset::{AddAsset, AssetLoader, LoadContext, LoadedAsset};
use bevy_log::{info, warn};
use bevy_math::{Mat4, Quat, Vec3};
use bevy_pbr::StandardMaterial;
use bevy_render::color::Color;
use bevy_utils::BoxedFuture;
use blend::Blend;
mod material;
mod mesh;
mod object;
pub use object::{spawn_blender_object, BlenderObjectBundle};
pub struct BlenderPlugin;
impl Plugin for BlenderPlugin {
fn build(&self, app: &mut App) {
app.init_asset_loader::<BlenderLoader>();
}
}
#[derive(thiserror::Error, Debug)]
pub enum BevyBlenderError {
#[error("Invalid .blend file: The file {blend_file:?} does not appear to be a valid Blender file. Please make sure it is not compressed.")]
InvalidBlendFile {
blend_file: String,
},
#[error("Invalid instance type: Expected {expected:?}, got {found:?}.")]
InvalidInstanceType {
expected: String,
found: String,
},
#[error("Unsupported asset: The asset type {asset_type:?} is not currently supported.")]
UnsupportedAsset {
asset_type: String,
},
#[error("Missing asset: The asset {asset_name:?} could not be found in {blend_file:?}. Please make sure the asset name does not start with an underscore.")]
MissingAsset {
asset_name: String,
blend_file: String,
},
}
#[derive(Default)]
struct BlenderLoader;
impl AssetLoader for BlenderLoader {
fn load<'a>(
&'a self,
bytes: &'a [u8],
load_context: &'a mut LoadContext,
) -> BoxedFuture<'a, anyhow::Result<()>> {
Box::pin(async move { Ok(load_blend_assets(bytes, load_context).await?) })
}
fn extensions(&self) -> &[&str] {
static EXTENSIONS: &[&str] = &["blend"];
EXTENSIONS
}
}
async fn load_blend_assets<'a, 'b>(
bytes: &'a [u8],
load_context: &'a mut LoadContext<'b>,
) -> anyhow::Result<()> {
if bytes[0..7] != *b"BLENDER" {
return Err(anyhow::Error::new(BevyBlenderError::InvalidBlendFile {
blend_file: String::from(load_context.path().to_str().unwrap()),
}));
}
let blend = Blend::new(bytes);
let blend_version = get_blend_version(&blend);
for mesh in blend.get_by_code(*b"ME") {
let label = mesh.get("id").get_string("name");
if !label.starts_with("ME_") {
load_context.set_labeled_asset(
label.as_str(),
LoadedAsset::new(mesh::instance_to_mesh(mesh, blend_version)?),
);
info!("Loaded Blender mesh asset: {}", label);
}
}
let unsupported_material: StandardMaterial = StandardMaterial {
base_color: Color::rgb(0.9, 0.4, 0.3).into(),
reflectance: 0.1,
perceptual_roughness: 0.5,
..Default::default()
};
load_context.set_labeled_asset(
"bevy_blender_missing_material",
LoadedAsset::new(StandardMaterial {
base_color: Color::rgb(1.0, 0.0, 0.5),
reflectance: 0.0,
perceptual_roughness: 0.0,
..Default::default()
}),
);
for material in blend.get_by_code(*b"MA") {
let label = material.get("id").get_string("name");
if !label.starts_with("MA_") {
let mat = material::instance_to_material(material, blend_version);
if mat.is_ok() {
load_context.set_labeled_asset(label.as_str(), LoadedAsset::new(mat.unwrap()));
info!("Loaded Blender material asset: {}", label);
} else {
load_context.set_labeled_asset(
label.as_str(),
LoadedAsset::new(unsupported_material.clone()),
);
warn!(
"Attempted to load an unsupported Blender material: {}",
label
);
}
}
}
Ok(())
}
pub fn right_hand_zup_to_right_hand_yup(rhzup: &Mat4) -> Mat4 {
let (scale, rotation, translation) = rhzup.to_scale_rotation_translation();
let euler_rotation = rotation.to_euler(bevy_math::EulerRot::XYZ);
Mat4::from_scale_rotation_translation(
Vec3::new(scale[0], scale[2], scale[1]),
Quat::from_euler(
bevy_math::EulerRot::XZY,
euler_rotation.0,
-euler_rotation.1,
euler_rotation.2,
),
Vec3::new(translation[0], translation[2], -translation[1]),
)
}
pub fn get_blend_version(blend: &Blend) -> (u8, u8, u8) {
let version_raw = blend.blend.header.version;
(
version_raw[0] - 48,
version_raw[1] - 48,
version_raw[2] - 48,
)
}