mirror of
https://github.com/PrimeDecomp/retrotool.git
synced 2026-07-12 18:18:58 -07:00
Add MCON viewer (prefabs)
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
use std::io::Cursor;
|
||||
|
||||
use anyhow::{ensure, Result};
|
||||
use binrw::{binrw, BinReaderExt, Endian};
|
||||
use binrw_derive::binread;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::format::{
|
||||
chunk::ChunkDescriptor, peek_four_cc, rfrm::FormDescriptor, CColor4f, CTransform4f, FourCC,
|
||||
TaggedVec,
|
||||
};
|
||||
|
||||
// Texture
|
||||
pub const K_FORM_MCON: FourCC = FourCC(*b"MCON");
|
||||
|
||||
const K_CHUNK_MCVD: FourCC = FourCC(*b"MCVD");
|
||||
|
||||
#[binrw]
|
||||
#[derive(Clone, Debug)]
|
||||
struct SModConHeader {
|
||||
unk: u32,
|
||||
}
|
||||
|
||||
#[binrw]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ObjectTransform {
|
||||
#[br(map = Uuid::from_bytes_le)]
|
||||
#[bw(map = Uuid::to_bytes_le)]
|
||||
pub id: Uuid,
|
||||
pub xf: CTransform4f,
|
||||
}
|
||||
|
||||
#[binread]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct SModConVisualData {
|
||||
#[br(map = |v: TaggedVec<u32, uuid::Bytes>| v.data.into_iter().map(Uuid::from_bytes_le).collect())]
|
||||
pub models: Vec<Uuid>,
|
||||
#[br(map = |v: TaggedVec<u32, uuid::Bytes>| v.data.into_iter().map(Uuid::from_bytes_le).collect())]
|
||||
pub ids_2: Vec<Uuid>,
|
||||
#[br(map = |v: TaggedVec<u32, _>| v.data)]
|
||||
pub colors: Vec<CColor4f>,
|
||||
#[br(map = |v: TaggedVec<u32, _>| v.data)]
|
||||
pub transforms: Vec<CTransform4f>,
|
||||
#[br(map = |v: TaggedVec<u32, _>| v.data)]
|
||||
pub object_transforms: Vec<ObjectTransform>,
|
||||
#[br(map = |v: TaggedVec<u32, _>| v.data)]
|
||||
pub bytes_1: Vec<u8>,
|
||||
#[br(map = |v: TaggedVec<u32, _>| v.data)]
|
||||
pub shorts_1: Vec<u16>,
|
||||
#[br(map = |v: TaggedVec<u32, _>| v.data)]
|
||||
pub shorts_2: Vec<u16>,
|
||||
#[br(map = |v: TaggedVec<u32, _>| v.data)]
|
||||
pub bytes_2: Vec<u8>,
|
||||
#[br(map = |v: TaggedVec<u32, _>| v.data)]
|
||||
pub bytes_3: Vec<u8>,
|
||||
// TODO
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ModConData {
|
||||
pub visual_data: Option<SModConVisualData>,
|
||||
}
|
||||
|
||||
impl ModConData {
|
||||
pub fn slice(data: &[u8], e: Endian) -> Result<Self> {
|
||||
let (mcon_desc, mut mcon_data, _) = FormDescriptor::slice(data, Endian::Little)?;
|
||||
ensure!(mcon_desc.id == K_FORM_MCON);
|
||||
ensure!(mcon_desc.reader_version == 41);
|
||||
ensure!(mcon_desc.writer_version == 44);
|
||||
|
||||
let mut data = ModConData { visual_data: None };
|
||||
while !mcon_data.is_empty() {
|
||||
if peek_four_cc(mcon_data) == *b"PEEK" {
|
||||
break;
|
||||
}
|
||||
let (chunk_desc, chunk_data, remain) = ChunkDescriptor::slice(mcon_data, e)?;
|
||||
if chunk_desc.id == K_CHUNK_MCVD {
|
||||
data.visual_data = Some(Cursor::new(chunk_data).read_type(e)?);
|
||||
}
|
||||
mcon_data = remain;
|
||||
}
|
||||
Ok(data)
|
||||
}
|
||||
}
|
||||
+57
-1
@@ -1,6 +1,7 @@
|
||||
pub mod chunk;
|
||||
pub mod cmdl;
|
||||
pub mod foot;
|
||||
pub mod mcon;
|
||||
pub mod mtrl;
|
||||
pub mod pack;
|
||||
pub mod rfrm;
|
||||
@@ -8,10 +9,12 @@ pub mod txtr;
|
||||
|
||||
use std::{
|
||||
fmt::{Debug, Display, Formatter, Write},
|
||||
marker::PhantomData,
|
||||
num::TryFromIntError,
|
||||
string::FromUtf8Error,
|
||||
};
|
||||
|
||||
use binrw::binrw;
|
||||
use binrw::{binrw, BinRead, BinWrite};
|
||||
|
||||
use crate::array_ref;
|
||||
|
||||
@@ -85,6 +88,9 @@ impl CColor4f {
|
||||
impl From<CColor4f> for [f32; 4] {
|
||||
fn from(value: CColor4f) -> Self { value.to_array() }
|
||||
}
|
||||
impl Default for CColor4f {
|
||||
fn default() -> Self { Self { r: 0.0, g: 0.0, b: 0.0, a: 1.0 } }
|
||||
}
|
||||
|
||||
#[binrw]
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
@@ -125,6 +131,19 @@ pub struct CTransform4f {
|
||||
m23: f32,
|
||||
}
|
||||
|
||||
impl CTransform4f {
|
||||
#[inline]
|
||||
#[rustfmt::skip]
|
||||
pub fn to_matrix_array(&self) -> [f32; 16] {
|
||||
[
|
||||
self.m00, self.m01, self.m02, 0.0,
|
||||
self.m10, self.m11, self.m12, 0.0,
|
||||
self.m20, self.m21, self.m22, 0.0,
|
||||
self.m03, self.m13, self.m23, 1.0,
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
#[binrw]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct COBBox {
|
||||
@@ -149,3 +168,40 @@ impl CStringFixedName {
|
||||
|
||||
fn into_string(self) -> Result<String, FromUtf8Error> { String::from_utf8(self.text) }
|
||||
}
|
||||
|
||||
#[binrw]
|
||||
#[derive(Clone, Debug, Default)]
|
||||
struct TaggedVec<C, T>
|
||||
where
|
||||
C: for<'a> BinRead<Args<'a> = ()>
|
||||
+ for<'a> BinWrite<Args<'a> = ()>
|
||||
+ Copy
|
||||
+ TryFrom<usize, Error = TryFromIntError>
|
||||
+ 'static,
|
||||
T: for<'a> BinRead<Args<'a> = ()> + for<'a> BinWrite<Args<'a> = ()> + 'static,
|
||||
usize: TryFrom<C, Error = TryFromIntError>,
|
||||
{
|
||||
#[bw(try_calc(data.len().try_into()))]
|
||||
count: C,
|
||||
#[br(count(count))]
|
||||
data: Vec<T>,
|
||||
_marker: PhantomData<C>,
|
||||
}
|
||||
|
||||
impl<C, T> TaggedVec<C, T>
|
||||
where
|
||||
C: for<'a> BinRead<Args<'a> = ()>
|
||||
+ for<'a> BinWrite<Args<'a> = ()>
|
||||
+ Copy
|
||||
+ Default
|
||||
+ TryFrom<usize, Error = TryFromIntError>
|
||||
+ 'static,
|
||||
T: for<'a> BinRead<Args<'a> = ()> + for<'a> BinWrite<Args<'a> = ()> + Default + 'static,
|
||||
usize: TryFrom<C, Error = TryFromIntError>,
|
||||
{
|
||||
#[allow(dead_code)]
|
||||
fn new(inner: Vec<T>) -> Self {
|
||||
#[allow(clippy::needless_update)]
|
||||
Self { data: inner, ..Default::default() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod material;
|
||||
pub mod modcon;
|
||||
pub mod model;
|
||||
pub mod package;
|
||||
pub mod texture;
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::Error;
|
||||
use bevy::{
|
||||
asset::{AssetLoader, AssetPath, BoxedFuture, LoadContext, LoadedAsset},
|
||||
prelude::*,
|
||||
};
|
||||
use binrw::Endian;
|
||||
use retrolib::format::mcon::ModConData;
|
||||
|
||||
use crate::loaders::model::ModelAsset;
|
||||
|
||||
#[derive(Debug, Clone, bevy::reflect::TypeUuid)]
|
||||
#[uuid = "83269869-1209-408e-8835-bc6f2496e82b"]
|
||||
pub struct ModConAsset {
|
||||
pub inner: ModConData,
|
||||
pub models: Vec<Handle<ModelAsset>>,
|
||||
}
|
||||
|
||||
pub struct ModConAssetLoader;
|
||||
|
||||
impl Plugin for ModConAssetLoader {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_asset::<ModConAsset>().add_asset_loader(ModConAssetLoader);
|
||||
}
|
||||
}
|
||||
|
||||
impl AssetLoader for ModConAssetLoader {
|
||||
fn load<'a>(
|
||||
&'a self,
|
||||
bytes: &'a [u8],
|
||||
load_context: &'a mut LoadContext,
|
||||
) -> BoxedFuture<'a, anyhow::Result<(), Error>> {
|
||||
Box::pin(async move {
|
||||
let mcon = ModConData::slice(bytes, Endian::Little)?;
|
||||
// println!("Loaded MCON: {:?}", mcon);
|
||||
let mut dependencies = vec![];
|
||||
let mut models = vec![];
|
||||
if let Some(visual_data) = &mcon.visual_data {
|
||||
dependencies.reserve_exact(visual_data.models.len());
|
||||
models.reserve_exact(visual_data.models.len());
|
||||
for id in &visual_data.models {
|
||||
let path = AssetPath::new(PathBuf::from(format!("{id}.CMDL")), None);
|
||||
dependencies.push(path.clone());
|
||||
models.push(load_context.get_handle(path));
|
||||
}
|
||||
}
|
||||
load_context.set_default_asset(
|
||||
LoadedAsset::new(ModConAsset { inner: mcon, models })
|
||||
.with_dependencies(dependencies),
|
||||
);
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn extensions(&self) -> &[&str] { &["mcon"] }
|
||||
}
|
||||
@@ -3,7 +3,7 @@ use std::path::PathBuf;
|
||||
use anyhow::Error;
|
||||
use bevy::{
|
||||
app::{App, Plugin},
|
||||
asset::{AddAsset, AssetLoader, AssetPath, BoxedFuture, LoadContext, LoadedAsset},
|
||||
asset::{AddAsset, AssetLoader, AssetPath, BoxedFuture, LoadContext, LoadState, LoadedAsset},
|
||||
prelude::*,
|
||||
utils::HashMap,
|
||||
};
|
||||
@@ -23,6 +23,12 @@ pub struct ModelAsset {
|
||||
pub textures: HashMap<Uuid, Handle<TextureAsset>>,
|
||||
}
|
||||
|
||||
impl ModelAsset {
|
||||
pub fn get_load_state(&self, server: &AssetServer) -> LoadState {
|
||||
server.get_group_load_state(self.textures.values().map(|h| h.id()))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ModelAssetLoader;
|
||||
|
||||
impl Plugin for ModelAssetLoader {
|
||||
@@ -40,8 +46,8 @@ impl AssetLoader for ModelAssetLoader {
|
||||
Box::pin(async move {
|
||||
let meta = locate_meta(bytes, Endian::Little)?;
|
||||
let data = ModelData::slice(bytes, meta, Endian::Little)?;
|
||||
log::info!("Loaded model {:?}", data.head);
|
||||
log::info!("Loaded meshes {:#?}", data.mesh);
|
||||
// log::info!("Loaded model {:?}", data.head);
|
||||
// log::info!("Loaded meshes {:#?}", data.mesh);
|
||||
let mut dependencies = HashMap::<Uuid, AssetPath>::new();
|
||||
for mat in &data.mtrl.materials {
|
||||
for data in &mat.data {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
mod icon;
|
||||
mod loaders;
|
||||
mod material;
|
||||
mod render;
|
||||
mod tabs;
|
||||
|
||||
use std::{path::PathBuf, time::Duration};
|
||||
@@ -19,6 +20,7 @@ use walkdir::{DirEntry, WalkDir};
|
||||
use crate::{
|
||||
loaders::{
|
||||
material::MaterialAssetLoader,
|
||||
modcon::ModConAssetLoader,
|
||||
model::ModelAssetLoader,
|
||||
package::{
|
||||
package_loader_system, PackageAssetLoader, PackageDirectory, RetroAssetIoPlugin,
|
||||
@@ -26,7 +28,8 @@ use crate::{
|
||||
texture::TextureAssetLoader,
|
||||
},
|
||||
material::CustomMaterial,
|
||||
tabs::{load_tab, model::TemporaryLabel, project::ProjectTab, TabState, TabType, TabViewer},
|
||||
render::TemporaryLabel,
|
||||
tabs::{load_tab, project::ProjectTab, TabState, TabType, TabViewer},
|
||||
};
|
||||
|
||||
#[derive(Default, Resource)]
|
||||
@@ -76,6 +79,7 @@ fn main() {
|
||||
.add_plugin(TextureAssetLoader)
|
||||
.add_plugin(ModelAssetLoader)
|
||||
.add_plugin(MaterialAssetLoader)
|
||||
.add_plugin(ModConAssetLoader)
|
||||
.add_plugin(EguiPlugin)
|
||||
.add_startup_system(setup_icon_font)
|
||||
.add_system(file_drop)
|
||||
@@ -179,11 +183,15 @@ fn ui_system(world: &mut World) {
|
||||
}
|
||||
TabType::Texture(tab) => {
|
||||
load_tab(world, &mut ctx, tab);
|
||||
tab_assets.push(tab.asset_ref.clone());
|
||||
tab_assets.push(tab.asset_ref);
|
||||
}
|
||||
TabType::Model(tab) => {
|
||||
load_tab(world, &mut ctx, tab);
|
||||
tab_assets.push(tab.asset_ref.clone());
|
||||
tab_assets.push(tab.asset_ref);
|
||||
}
|
||||
TabType::ModCon(tab) => {
|
||||
load_tab(world, &mut ctx, tab);
|
||||
tab_assets.push(tab.asset_ref);
|
||||
}
|
||||
TabType::Empty => {}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
use bevy::{prelude::*, render::primitives::Aabb};
|
||||
use egui::PointerButton;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ModelCamera {
|
||||
pub transform: Transform,
|
||||
pub upside_down: bool,
|
||||
pub radius: f32,
|
||||
pub origin: Vec3,
|
||||
pub projection: Projection,
|
||||
}
|
||||
|
||||
impl ModelCamera {
|
||||
pub fn init(&mut self, aabb: &Aabb, center: bool) {
|
||||
let radius = (aabb.max() - aabb.min()).max_element() * 1.25;
|
||||
if center {
|
||||
self.origin = aabb.center.into();
|
||||
}
|
||||
let mut camera_xf =
|
||||
Transform::from_xyz(-radius, 5.0, radius).looking_at(self.origin, Vec3::Y);
|
||||
let rot_matrix = Mat3::from_quat(camera_xf.rotation);
|
||||
camera_xf.translation = self.origin + rot_matrix.mul_vec3(Vec3::new(0.0, 0.0, radius));
|
||||
self.transform = camera_xf;
|
||||
self.radius = radius;
|
||||
}
|
||||
|
||||
pub fn update(
|
||||
&mut self,
|
||||
rect: &egui::Rect,
|
||||
response: &egui::Response,
|
||||
scroll_delta: egui::Vec2,
|
||||
) {
|
||||
let mut any = false;
|
||||
let mut rotation_move = Vec2::ZERO;
|
||||
let mut pan = Vec2::ZERO;
|
||||
let scroll = {
|
||||
if response.hovered() {
|
||||
// let delta = ui.input(|i| i.scroll_delta);
|
||||
Vec2::new(scroll_delta.x, scroll_delta.y)
|
||||
} else {
|
||||
Vec2::ZERO
|
||||
}
|
||||
};
|
||||
if response.drag_started_by(PointerButton::Primary)
|
||||
|| response.drag_released_by(PointerButton::Primary)
|
||||
{
|
||||
// only check for upside down when orbiting started or ended this frame
|
||||
// if the camera is "upside" down, panning horizontally would be inverted, so invert the input to make it correct
|
||||
let up = self.transform.rotation * Vec3::Y;
|
||||
self.upside_down = up.y <= 0.0;
|
||||
}
|
||||
if response.dragged_by(PointerButton::Primary) {
|
||||
let delta = response.drag_delta();
|
||||
rotation_move = Vec2::new(delta.x, delta.y);
|
||||
} else if response.dragged_by(PointerButton::Middle) {
|
||||
let delta = response.drag_delta();
|
||||
pan = Vec2::new(delta.x, delta.y);
|
||||
}
|
||||
if rotation_move.length_squared() > 0.0 {
|
||||
any = true;
|
||||
let delta_x = {
|
||||
let delta = rotation_move.x / rect.width() * std::f32::consts::PI * 2.0;
|
||||
if self.upside_down {
|
||||
-delta
|
||||
} else {
|
||||
delta
|
||||
}
|
||||
};
|
||||
let delta_y = rotation_move.y / rect.height() * std::f32::consts::PI;
|
||||
let yaw = Quat::from_rotation_y(-delta_x);
|
||||
let pitch = Quat::from_rotation_x(-delta_y);
|
||||
self.transform.rotation = yaw * self.transform.rotation; // rotate around global y axis
|
||||
self.transform.rotation *= pitch; // rotate around local x axis
|
||||
} else if pan.length_squared() > 0.0 {
|
||||
any = true;
|
||||
if let Projection::Perspective(projection) = &self.projection {
|
||||
pan *= Vec2::new(projection.fov * projection.aspect_ratio, projection.fov)
|
||||
/ Vec2::new(rect.width(), rect.height());
|
||||
}
|
||||
// translate by local axes
|
||||
let right = self.transform.rotation * Vec3::X * -pan.x;
|
||||
let up = self.transform.rotation * Vec3::Y * pan.y;
|
||||
// make panning proportional to distance away from focus point
|
||||
let translation = (right + up) * self.radius;
|
||||
self.origin += translation;
|
||||
} else if scroll.y.abs() > 0.0 {
|
||||
any = true;
|
||||
self.radius -= (scroll.y / 50.0/* TODO ? */) * self.radius * 0.2;
|
||||
// dont allow zoom to reach zero or you get stuck
|
||||
self.radius = f32::max(self.radius, 0.05);
|
||||
}
|
||||
if any {
|
||||
// emulating parent/child to make the yaw/y-axis rotation behave like a turntable
|
||||
// parent = x and y rotation
|
||||
// child = z-offset
|
||||
let rot_matrix = Mat3::from_quat(self.transform.rotation);
|
||||
self.transform.translation =
|
||||
self.origin + rot_matrix.mul_vec3(Vec3::new(0.0, 0.0, self.radius));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
pub mod camera;
|
||||
pub mod model;
|
||||
|
||||
use bevy::prelude::*;
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct TemporaryLabel;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,4 @@
|
||||
pub mod modcon;
|
||||
pub mod model;
|
||||
pub mod project;
|
||||
pub mod texture;
|
||||
@@ -11,6 +12,7 @@ pub enum TabType {
|
||||
Project(project::ProjectTab),
|
||||
Texture(texture::TextureTab),
|
||||
Model(model::ModelTab),
|
||||
ModCon(modcon::ModConTab),
|
||||
Empty,
|
||||
}
|
||||
|
||||
@@ -79,6 +81,7 @@ impl egui_dock::TabViewer for TabViewer<'_> {
|
||||
TabType::Project(tab) => render_tab(self.world, ui, tab, &mut self.state),
|
||||
TabType::Texture(tab) => render_tab(self.world, ui, tab, &mut self.state),
|
||||
TabType::Model(tab) => render_tab(self.world, ui, tab, &mut self.state),
|
||||
TabType::ModCon(tab) => render_tab(self.world, ui, tab, &mut self.state),
|
||||
TabType::Empty => {}
|
||||
}
|
||||
}
|
||||
@@ -88,6 +91,7 @@ impl egui_dock::TabViewer for TabViewer<'_> {
|
||||
TabType::Project(tab) => tab.title(),
|
||||
TabType::Texture(tab) => tab.title(),
|
||||
TabType::Model(tab) => tab.title(),
|
||||
TabType::ModCon(tab) => tab.title(),
|
||||
TabType::Empty => "".into(),
|
||||
}
|
||||
}
|
||||
@@ -103,11 +107,15 @@ impl egui_dock::TabViewer for TabViewer<'_> {
|
||||
close_tab(self.world, tab);
|
||||
true
|
||||
}
|
||||
TabType::ModCon(tab) => {
|
||||
close_tab(self.world, tab);
|
||||
true
|
||||
}
|
||||
TabType::Empty => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn clear_background(&self, tab: &Self::Tab) -> bool {
|
||||
!matches!(tab, TabType::Empty | TabType::Model(_))
|
||||
!matches!(tab, TabType::Empty | TabType::Model(_) | TabType::ModCon(_))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,318 @@
|
||||
use bevy::{
|
||||
asset::LoadState,
|
||||
core_pipeline::{clear_color::ClearColorConfig, tonemapping::Tonemapping},
|
||||
ecs::system::{lifetimeless::*, *},
|
||||
math::Vec3A,
|
||||
prelude::*,
|
||||
render::{camera::Viewport, primitives::Aabb, view::RenderLayers},
|
||||
};
|
||||
use bevy_egui::EguiContext;
|
||||
use egui::{Sense, Widget};
|
||||
|
||||
use crate::{
|
||||
icon,
|
||||
loaders::{modcon::ModConAsset, model::ModelAsset, texture::TextureAsset},
|
||||
material::CustomMaterial,
|
||||
render::{
|
||||
camera::ModelCamera,
|
||||
model::{convert_transform, load_model},
|
||||
TemporaryLabel,
|
||||
},
|
||||
tabs::{SystemTab, TabState},
|
||||
AssetRef,
|
||||
};
|
||||
|
||||
pub struct LoadedModel {
|
||||
pub entity: Entity,
|
||||
pub visible: bool,
|
||||
}
|
||||
|
||||
pub struct ModelInfo {
|
||||
pub handle: Handle<ModelAsset>,
|
||||
pub loaded: Vec<LoadedModel>,
|
||||
pub transforms: Vec<Transform>,
|
||||
pub aabb: Aabb,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ModConTab {
|
||||
pub asset_ref: AssetRef,
|
||||
pub handle: Handle<ModConAsset>,
|
||||
pub models: Vec<ModelInfo>,
|
||||
pub camera: ModelCamera,
|
||||
pub diffuse_map: Handle<Image>,
|
||||
pub specular_map: Handle<Image>,
|
||||
pub combined_aabb: Aabb,
|
||||
}
|
||||
|
||||
impl ModConTab {
|
||||
fn get_load_state(
|
||||
&self,
|
||||
server: &AssetServer,
|
||||
assets: &Assets<ModConAsset>,
|
||||
models: &Assets<ModelAsset>,
|
||||
) -> LoadState {
|
||||
match server.get_load_state(&self.handle) {
|
||||
LoadState::Loaded => {}
|
||||
state => return state,
|
||||
};
|
||||
let asset = match assets.get(&self.handle) {
|
||||
Some(v) => v,
|
||||
None => return LoadState::Failed,
|
||||
};
|
||||
// Ensure all dependencies loaded
|
||||
match server.get_group_load_state(asset.models.iter().map(|h| h.id())) {
|
||||
LoadState::Loaded => {}
|
||||
state => return state,
|
||||
}
|
||||
for model in &asset.models {
|
||||
let model = models.get(model).unwrap();
|
||||
match model.get_load_state(server) {
|
||||
LoadState::Loaded => {}
|
||||
state => return state,
|
||||
}
|
||||
}
|
||||
LoadState::Loaded
|
||||
}
|
||||
}
|
||||
|
||||
impl SystemTab for ModConTab {
|
||||
type LoadParam = (
|
||||
SCommands,
|
||||
SResMut<Assets<Mesh>>,
|
||||
SResMut<Assets<CustomMaterial>>,
|
||||
SResMut<Assets<ModelAsset>>,
|
||||
SResMut<Assets<TextureAsset>>,
|
||||
SResMut<Assets<Image>>,
|
||||
SResMut<AssetServer>,
|
||||
SResMut<Assets<ModConAsset>>,
|
||||
);
|
||||
type UiParam =
|
||||
(SCommands, SRes<AssetServer>, SRes<Assets<ModelAsset>>, SRes<Assets<ModConAsset>>);
|
||||
|
||||
fn load(&mut self, _ctx: &mut EguiContext, query: SystemParamItem<'_, '_, Self::LoadParam>) {
|
||||
let (
|
||||
mut commands,
|
||||
mut meshes,
|
||||
mut materials,
|
||||
mut models,
|
||||
texture_assets,
|
||||
mut images,
|
||||
server,
|
||||
mod_con_assets,
|
||||
) = query;
|
||||
|
||||
if self.models.is_empty() {
|
||||
if let Some(mod_con) = mod_con_assets.get(&self.handle) {
|
||||
let data = match &mod_con.inner.visual_data {
|
||||
Some(value) => value,
|
||||
None => return,
|
||||
};
|
||||
for handle in &mod_con.models {
|
||||
self.models.push(ModelInfo {
|
||||
handle: handle.clone(),
|
||||
loaded: vec![],
|
||||
transforms: vec![],
|
||||
aabb: Default::default(),
|
||||
});
|
||||
}
|
||||
for (idx, &model_idx) in data.shorts_1.iter().enumerate() {
|
||||
self.models[model_idx as usize]
|
||||
.transforms
|
||||
.push(convert_transform(&data.transforms[idx]));
|
||||
}
|
||||
self.models.retain(|info| !info.transforms.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
let mut loaded = false;
|
||||
for info in &mut self.models {
|
||||
if !info.loaded.is_empty() {
|
||||
for loaded in &info.loaded {
|
||||
if let Some(mut commands) = commands.get_entity(loaded.entity) {
|
||||
commands.insert(Visibility::Hidden);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
let asset = match models.get_mut(&info.handle) {
|
||||
Some(v) => v,
|
||||
None => continue,
|
||||
};
|
||||
// Ensure all dependencies loaded
|
||||
match asset.get_load_state(&server) {
|
||||
LoadState::Loaded => println!("Loading model"),
|
||||
_ => continue,
|
||||
}
|
||||
|
||||
let result = load_model(
|
||||
asset,
|
||||
&mut commands,
|
||||
&texture_assets,
|
||||
&mut images,
|
||||
&mut materials,
|
||||
&mut meshes,
|
||||
);
|
||||
let built = match result {
|
||||
Ok(value) => value,
|
||||
Err(e) => {
|
||||
log::error!("Failed to load model: {e:?}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
for &transform in &info.transforms {
|
||||
let entity = commands
|
||||
.spawn(SpatialBundle { transform, visibility: Visibility::Hidden, ..default() })
|
||||
.with_children(|builder| {
|
||||
for idx in built.lod[0].meshes.iter() {
|
||||
let mesh = &built.meshes[idx];
|
||||
builder.spawn(MaterialMeshBundle {
|
||||
mesh: mesh.mesh.clone(),
|
||||
material: mesh.material.clone(),
|
||||
..default()
|
||||
});
|
||||
}
|
||||
})
|
||||
.id();
|
||||
info.loaded.push(LoadedModel { entity, visible: true });
|
||||
}
|
||||
info.aabb = built.aabb;
|
||||
loaded = true;
|
||||
}
|
||||
|
||||
if loaded {
|
||||
let all_loaded = self.models.iter().all(|m| !m.loaded.is_empty());
|
||||
if all_loaded {
|
||||
let mut min = Vec3A::splat(f32::MAX);
|
||||
let mut max = Vec3A::splat(f32::MIN);
|
||||
for info in &self.models {
|
||||
min = info.aabb.min().min(min);
|
||||
max = info.aabb.max().max(max);
|
||||
}
|
||||
self.camera.init(&Aabb::from_min_max(min.into(), max.into()), true);
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME
|
||||
if self.diffuse_map.is_weak() {
|
||||
self.diffuse_map = server.load("papermill_diffuse_rgb9e5_zstd.ktx2");
|
||||
self.specular_map = server.load("papermill_specular_rgb9e5_zstd.ktx2");
|
||||
}
|
||||
}
|
||||
|
||||
fn close(&mut self, query: SystemParamItem<'_, '_, Self::LoadParam>) {
|
||||
let (mut commands, _, _, _, _, _, _, _) = query;
|
||||
for model in self.models.iter().flat_map(|l| &l.loaded) {
|
||||
if let Some(commands) = commands.get_entity(model.entity) {
|
||||
commands.despawn_recursive();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ui(
|
||||
&mut self,
|
||||
ui: &mut egui::Ui,
|
||||
query: SystemParamItem<'_, '_, Self::UiParam>,
|
||||
state: &mut TabState,
|
||||
) {
|
||||
let scale = ui.ctx().pixels_per_point();
|
||||
let rect = ui.available_rect_before_wrap();
|
||||
let left_top = rect.left_top().to_vec2() * scale;
|
||||
let size = rect.size() * scale;
|
||||
let viewport = Viewport {
|
||||
physical_position: UVec2 { x: left_top.x as u32, y: left_top.y as u32 },
|
||||
physical_size: UVec2 { x: size.x as u32, y: size.y as u32 },
|
||||
depth: 0.0..1.0,
|
||||
};
|
||||
let response =
|
||||
ui.interact(rect, ui.make_persistent_id("background"), Sense::click_and_drag());
|
||||
self.camera.update(&rect, &response, ui.input(|i| i.scroll_delta));
|
||||
|
||||
let (mut commands, server, models, mod_con_assets) = query;
|
||||
let all_loaded = self.models.iter().all(|m| !m.loaded.is_empty());
|
||||
if !all_loaded {
|
||||
ui.centered_and_justified(|ui| {
|
||||
match self.get_load_state(&server, &mod_con_assets, &models) {
|
||||
LoadState::Failed => egui::Label::new(
|
||||
egui::RichText::from("Loading failed").heading().color(egui::Color32::RED),
|
||||
)
|
||||
.ui(ui),
|
||||
_ => egui::Spinner::new().size(50.0).ui(ui),
|
||||
};
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
egui::Frame::group(ui.style()).show(ui, |ui| {
|
||||
egui::ScrollArea::vertical().max_height(rect.height() * 0.25).show(ui, |ui| {
|
||||
ui.label(format!("Models: {}", self.models.len()));
|
||||
ui.label(format!(
|
||||
"Instances: {}",
|
||||
self.models.iter().map(|m| m.loaded.len()).sum::<usize>()
|
||||
))
|
||||
});
|
||||
});
|
||||
|
||||
commands.spawn((
|
||||
Camera3dBundle {
|
||||
camera_3d: Camera3d {
|
||||
clear_color: if state.render_layer == 0 {
|
||||
ClearColorConfig::Default
|
||||
} else {
|
||||
ClearColorConfig::None
|
||||
},
|
||||
..default()
|
||||
},
|
||||
camera: Camera {
|
||||
viewport: Some(viewport),
|
||||
order: state.render_layer as isize,
|
||||
// hdr: true,
|
||||
..default()
|
||||
},
|
||||
tonemapping: Tonemapping::TonyMcMapface,
|
||||
transform: self.camera.transform,
|
||||
..default()
|
||||
},
|
||||
// BloomSettings::default(),
|
||||
EnvironmentMapLight {
|
||||
diffuse_map: self.diffuse_map.clone(),
|
||||
specular_map: self.specular_map.clone(),
|
||||
},
|
||||
RenderLayers::layer(state.render_layer),
|
||||
TemporaryLabel,
|
||||
));
|
||||
// FIXME: https://github.com/bevyengine/bevy/issues/3462
|
||||
if state.render_layer == 0 {
|
||||
// commands.spawn((
|
||||
// DirectionalLightBundle {
|
||||
// directional_light: DirectionalLight { ..default() },
|
||||
// transform: Transform::from_xyz(-30.0, 5.0, 20.0)
|
||||
// .looking_at(Vec3::ZERO, Vec3::Y),
|
||||
// ..default()
|
||||
// },
|
||||
// RenderLayers::layer(state.render_layer),
|
||||
// TemporaryLabel,
|
||||
// ));
|
||||
}
|
||||
|
||||
for info in &self.models {
|
||||
for model in &info.loaded {
|
||||
if let Some(mut commands) = commands.get_entity(model.entity) {
|
||||
commands.insert((
|
||||
if model.visible { Visibility::Visible } else { Visibility::Hidden },
|
||||
RenderLayers::layer(state.render_layer),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
state.render_layer += 1;
|
||||
}
|
||||
|
||||
fn title(&mut self) -> egui::WidgetText {
|
||||
format!("{} {} {}", icon::SCENE_DATA, self.asset_ref.kind, self.asset_ref.id).into()
|
||||
}
|
||||
|
||||
fn id(&self) -> String { format!("{} {}", self.asset_ref.kind, self.asset_ref.id) }
|
||||
}
|
||||
+59
-723
File diff suppressed because it is too large
Load Diff
@@ -8,6 +8,7 @@ use bevy_egui::{EguiContext, EguiUserTextures};
|
||||
use egui::{text::LayoutJob, Color32, TextFormat, Widget};
|
||||
use retrolib::format::{
|
||||
cmdl::{K_FORM_CMDL, K_FORM_SMDL, K_FORM_WMDL},
|
||||
mcon::K_FORM_MCON,
|
||||
txtr::{ETextureFormat, ETextureType, K_FORM_TXTR},
|
||||
FourCC,
|
||||
};
|
||||
@@ -15,7 +16,7 @@ use retrolib::format::{
|
||||
use crate::{
|
||||
icon,
|
||||
loaders::{model::ModelAsset, package::PackageDirectory, texture::TextureAsset},
|
||||
tabs::{model::ModelTab, texture::TextureTab, SystemTab, TabState, TabType},
|
||||
tabs::{modcon::ModConTab, model::ModelTab, texture::TextureTab, SystemTab, TabState, TabType},
|
||||
AssetRef,
|
||||
};
|
||||
|
||||
@@ -172,7 +173,7 @@ impl SystemTab for ProjectTab {
|
||||
K_FORM_TXTR => icon::TEXTURE,
|
||||
K_FORM_CMDL | K_FORM_SMDL | K_FORM_WMDL => icon::FILE_3D,
|
||||
K_FORM_FMV0 => icon::FILE_MOVIE,
|
||||
K_FORM_ROOM => icon::SCENE_DATA,
|
||||
K_FORM_ROOM | K_FORM_MCON => icon::SCENE_DATA,
|
||||
_ => icon::FILE,
|
||||
},
|
||||
entry.kind,
|
||||
@@ -212,7 +213,7 @@ impl SystemTab for ProjectTab {
|
||||
entry.id, entry.kind
|
||||
));
|
||||
state.open_tab = Some(TabType::Texture(TextureTab {
|
||||
asset_ref: asset_ref.clone(),
|
||||
asset_ref,
|
||||
handle,
|
||||
loaded_texture: None,
|
||||
}));
|
||||
@@ -221,9 +222,17 @@ impl SystemTab for ProjectTab {
|
||||
let handle = server
|
||||
.load::<ModelAsset, _>(format!("{}.{}", entry.id, entry.kind));
|
||||
state.open_tab = Some(TabType::Model(ModelTab {
|
||||
asset_ref: asset_ref.clone(),
|
||||
asset_ref,
|
||||
handle,
|
||||
loaded: None,
|
||||
..default()
|
||||
}));
|
||||
}
|
||||
K_FORM_MCON => {
|
||||
let handle = server.load(format!("{}.{}", entry.id, entry.kind));
|
||||
state.open_tab = Some(TabType::ModCon(ModConTab {
|
||||
asset_ref,
|
||||
handle,
|
||||
..default()
|
||||
}));
|
||||
}
|
||||
_ => {}
|
||||
|
||||
Reference in New Issue
Block a user