Add LTPB (light probe) viewer

This commit is contained in:
Luke Street
2023-03-22 18:14:08 -04:00
parent 5661f558a0
commit 92018a183a
10 changed files with 425 additions and 55 deletions
+102
View File
@@ -0,0 +1,102 @@
use std::io::Cursor;
use anyhow::{bail, ensure, Result};
use binrw::{binrw, BinReaderExt, Endian};
use crate::format::{
chunk::ChunkDescriptor,
rfrm::FormDescriptor,
txtr::{STextureMetaData, TextureData},
CVector3f, CVector3i, FourCC, TaggedVec,
};
// Texture
pub const K_FORM_LTPB: FourCC = FourCC(*b"LTPB");
// Probe header
pub const K_CHUNK_PHDR: FourCC = FourCC(*b"PHDR");
// Probe texture
pub const K_CHUNK_PTEX: FourCC = FourCC(*b"PTEX");
#[binrw]
#[derive(Clone, Debug)]
pub struct CBakedLightingUniformProbeGridIndex {
pub x: u16,
pub y: u16,
pub z: u16,
}
#[binrw]
#[derive(Clone, Debug)]
pub struct LightProbeBundleHeader {
pub unk1: u32,
pub unk2: u32,
pub unk_vec: CVector3f,
pub grid_idx1: CBakedLightingUniformProbeGridIndex,
pub grid_idx2: CBakedLightingUniformProbeGridIndex,
}
#[binrw]
#[derive(Clone, Debug)]
pub struct LightProbeExtra {
pub vec: CVector3i,
pub unk: u32,
}
#[binrw]
#[derive(Clone, Debug)]
struct SLightProbeMetaData {
unk1: u32,
unk2: u32,
#[br(map = |v: TaggedVec<u32, _>| v.data)]
meta_offsets: Vec<u64>,
#[br(map = |v: TaggedVec<u32, _>| v.data)]
txtr_offsets: Vec<u64>,
}
#[derive(Debug, Clone)]
pub struct LightProbeData {
pub head: LightProbeBundleHeader,
pub textures: Vec<TextureData>,
pub extra: Vec<LightProbeExtra>,
}
impl LightProbeData {
pub fn slice(data: &[u8], meta: &[u8], e: Endian) -> Result<LightProbeData> {
let (ltpb_desc, mut ltpb_data, _) = FormDescriptor::slice(data, e)?;
ensure!(ltpb_desc.id == K_FORM_LTPB);
ensure!(ltpb_desc.reader_version == 66);
ensure!(ltpb_desc.writer_version == 73);
let meta: SLightProbeMetaData = Cursor::new(meta).read_type(e)?;
ensure!(meta.meta_offsets.len() == meta.txtr_offsets.len());
let texture_count = meta.meta_offsets.len();
let mut head: Option<LightProbeBundleHeader> = None;
while !ltpb_data.is_empty() {
let (chunk_desc, chunk_data, remain) = ChunkDescriptor::slice(ltpb_data, e)?;
let mut reader = Cursor::new(chunk_data);
match chunk_desc.id {
K_CHUNK_PHDR => head = Some(reader.read_type(e)?),
K_CHUNK_PTEX => {}
id => bail!("Unknown LTPB chunk ID {id:?}"),
}
ltpb_data = remain;
}
let Some(head) = head else { bail!("Failed to locate PHDR") };
let mut textures = Vec::with_capacity(texture_count);
let mut extra: Vec<LightProbeExtra> = Vec::with_capacity(texture_count);
for (meta_offset, txtr_offset) in meta.meta_offsets.into_iter().zip(meta.txtr_offsets) {
let meta = &data[meta_offset as usize..];
// Skip metadata to read extra fields
let mut reader = Cursor::new(meta);
reader.read_type::<STextureMetaData>(e)?;
extra.push(reader.read_type(e)?);
textures.push(TextureData::slice(&data[txtr_offset as usize..], meta, e)?);
}
Ok(LightProbeData { head, textures, extra })
}
}
+36
View File
@@ -1,6 +1,7 @@
pub mod chunk;
pub mod cmdl;
pub mod foot;
pub mod ltpb;
pub mod mcon;
pub mod mtrl;
pub mod pack;
@@ -178,6 +179,41 @@ impl mint::IntoMint for CColor4f {
type MintType = mint::Vector4<f32>;
}
#[binrw]
#[derive(Copy, Clone, Debug, Default)]
pub struct CVector3i {
pub x: i32,
pub y: i32,
pub z: i32,
}
impl CVector3i {
#[inline]
pub fn new(x: i32, y: i32, z: i32) -> Self { Self { x, y, z } }
#[inline]
pub fn splat(xyz: i32) -> Self { Self { x: xyz, y: xyz, z: xyz } }
#[inline]
pub fn to_array(self) -> [i32; 3] { [self.x, self.y, self.z] }
}
impl From<[i32; 3]> for CVector3i {
fn from(value: [i32; 3]) -> Self { Self { x: value[0], y: value[1], z: value[2] } }
}
impl From<CVector3i> for [i32; 3] {
fn from(value: CVector3i) -> Self { value.to_array() }
}
impl From<CVector3i> for mint::Vector3<i32> {
fn from(value: CVector3i) -> Self { Self::from([value.x, value.y, value.z]) }
}
impl From<mint::Vector3<i32>> for CVector3i {
fn from(value: mint::Vector3<i32>) -> Self { Self { x: value.x, y: value.y, z: value.z } }
}
impl mint::IntoMint for CVector3i {
type MintType = mint::Vector3<i32>;
}
#[binrw]
#[derive(Copy, Clone, Debug, Default)]
pub struct CVector4i {
+2 -2
View File
@@ -581,9 +581,9 @@ impl TextureData {
ensure!(txtr_desc.reader_version == 47);
ensure!(txtr_desc.writer_version == 51);
let (head_desc, head_data, _) = ChunkDescriptor::slice(txtr_data, Endian::Little)?;
let (head_desc, head_data, _) = ChunkDescriptor::slice(txtr_data, e)?;
ensure!(head_desc.id == K_CHUNK_HEAD);
let head: STextureHeader = Cursor::new(head_data).read_type(Endian::Little)?;
let head: STextureHeader = Cursor::new(head_data).read_type(e)?;
// log::debug!("META: {meta:#?}");
// log::debug!("HEAD: {head:#?}");
+63
View File
@@ -0,0 +1,63 @@
use anyhow::Error;
use bevy::{
asset::{AssetLoader, BoxedFuture, LoadContext, LoadedAsset},
prelude::*,
render::{renderer::RenderDevice, texture::CompressedImageFormats},
};
use binrw::Endian;
use retrolib::format::{
foot::{locate_asset_id, locate_meta},
ltpb::{LightProbeBundleHeader, LightProbeData, LightProbeExtra},
};
use crate::loaders::texture::{load_texture_asset, TextureAsset};
#[derive(Debug, Clone, bevy::reflect::TypeUuid)]
#[uuid = "f5d65a8b-ffcc-47ea-8c9d-1ab30cca723c"]
pub struct LightProbeAsset {
pub head: LightProbeBundleHeader,
pub textures: Vec<TextureAsset>,
pub extra: Vec<LightProbeExtra>,
}
pub struct LightProbeAssetLoader {
supported_formats: CompressedImageFormats,
}
impl FromWorld for LightProbeAssetLoader {
fn from_world(world: &mut World) -> Self {
let supported_formats = match world.get_resource::<RenderDevice>() {
Some(render_device) => CompressedImageFormats::from_features(render_device.features()),
None => CompressedImageFormats::all(),
};
Self { supported_formats }
}
}
impl AssetLoader for LightProbeAssetLoader {
fn load<'a>(
&'a self,
bytes: &'a [u8],
load_context: &'a mut LoadContext,
) -> BoxedFuture<'a, Result<(), Error>> {
Box::pin(async move {
let id = locate_asset_id(bytes, Endian::Little)?;
let meta = locate_meta(bytes, Endian::Little)?;
let data = LightProbeData::slice(bytes, meta, Endian::Little)?;
info!("Loading light probe {} {:?}", id, data.head);
let mut textures = Vec::with_capacity(data.textures.len());
for texture_data in data.textures {
textures.push(load_texture_asset(id, texture_data, &self.supported_formats)?);
}
load_context.set_default_asset(LoadedAsset::new(LightProbeAsset {
head: data.head,
textures,
extra: data.extra,
}));
Ok(())
})
}
fn extensions(&self) -> &[&str] { &["ltpb"] }
}
+3
View File
@@ -1,3 +1,4 @@
pub mod lightprobe;
pub mod material;
pub mod modcon;
pub mod model;
@@ -18,11 +19,13 @@ impl Plugin for RetroAssetPlugin {
.add_asset::<model::ModelAsset>()
.add_asset::<package::PackageDirectory>()
.add_asset::<texture::TextureAsset>()
.add_asset::<lightprobe::LightProbeAsset>()
.init_asset_loader::<material::MaterialAssetLoader>()
.init_asset_loader::<modcon::ModConAssetLoader>()
.init_asset_loader::<model::ModelAssetLoader>()
.init_asset_loader::<package::PackageAssetLoader>()
.init_asset_loader::<texture::TextureAssetLoader>()
.init_asset_loader::<lightprobe::LightProbeAssetLoader>()
.add_system(package::package_loader_system);
}
}
+56 -52
View File
@@ -17,6 +17,7 @@ use retrolib::format::{
decompress_image, slice_texture, ETextureFormat, ETextureType, TextureData, K_FORM_TXTR,
},
};
use uuid::Uuid;
use crate::AssetRef;
@@ -55,58 +56,8 @@ impl AssetLoader for TextureAssetLoader {
let data = TextureData::slice(bytes, meta, Endian::Little)?;
info!("Loading texture {} {:?}", id, data.head);
let is_srgb = data.head.format.is_srgb();
let slices = slice_texture(&data)?;
let (bw, bh, _) = data.head.format.block_size();
let format = wgpu_format(data.head.format)
.ok_or_else(|| anyhow!("Texture format unsupported: {:?}", data.head.format))?;
let supported =
texture_format_supported(data.head.kind, format, &self.supported_formats);
let mut images = Vec::with_capacity(slices.len());
for mip in &slices {
let mut slice_images = Vec::with_capacity(mip.len());
for slice in mip {
let slice_data = &data.data[slice.data_range.clone()];
slice_images.push(if supported {
texture_slice_to_image(
format,
slice_data.to_vec(),
slice.width,
slice.height,
bw,
bh,
)
} else {
Image::from_dynamic(
decompress_image(
data.head.format,
slice.width,
slice.height,
slice_data,
)?,
is_srgb,
)
});
}
images.push(slice_images);
}
let (image_data, format) = if supported {
(data.data.clone(), format)
} else {
(
images.iter().flatten().flat_map(|i| &i.data).cloned().collect(),
if is_srgb { TextureFormat::Rgba8UnormSrgb } else { TextureFormat::Rgba8Unorm },
)
};
let texture = texture_to_image(&data, format, image_data)?;
load_context.set_default_asset(LoadedAsset::new(TextureAsset {
asset_ref: AssetRef { id, kind: K_FORM_TXTR },
inner: data,
texture,
slices: images,
}));
let asset = load_texture_asset(id, data, &self.supported_formats)?;
load_context.set_default_asset(LoadedAsset::new(asset));
Ok(())
})
}
@@ -114,6 +65,59 @@ impl AssetLoader for TextureAssetLoader {
fn extensions(&self) -> &[&str] { &["txtr"] }
}
pub fn load_texture_asset(
id: Uuid,
data: TextureData,
supported_formats: &CompressedImageFormats,
) -> Result<TextureAsset> {
let is_srgb = data.head.format.is_srgb();
let slices = slice_texture(&data)?;
let (bw, bh, _) = data.head.format.block_size();
let format = wgpu_format(data.head.format)
.ok_or_else(|| anyhow!("Texture format unsupported: {:?}", data.head.format))?;
let supported = texture_format_supported(data.head.kind, format, supported_formats);
let mut images = Vec::with_capacity(slices.len());
for mip in &slices {
let mut slice_images = Vec::with_capacity(mip.len());
for slice in mip {
let slice_data = &data.data[slice.data_range.clone()];
slice_images.push(if supported {
texture_slice_to_image(
format,
slice_data.to_vec(),
slice.width,
slice.height,
bw,
bh,
)
} else {
Image::from_dynamic(
decompress_image(data.head.format, slice.width, slice.height, slice_data)?,
is_srgb,
)
});
}
images.push(slice_images);
}
let (image_data, format) = if supported {
(data.data.clone(), format)
} else {
(
images.iter().flatten().flat_map(|i| &i.data).cloned().collect(),
if is_srgb { TextureFormat::Rgba8UnormSrgb } else { TextureFormat::Rgba8Unorm },
)
};
let texture = texture_to_image(&data, format, image_data)?;
Ok(TextureAsset {
asset_ref: AssetRef { id, kind: K_FORM_TXTR },
inner: data,
texture,
slices: images,
})
}
/// Create an [Image] from a 2D texture slice.
fn texture_slice_to_image(
format: TextureFormat,
+4
View File
@@ -223,6 +223,10 @@ fn ui_system(world: &mut World) {
load_tab(world, &mut ctx, tab.as_mut());
tab_assets.push(tab.asset_ref);
}
TabType::LightProbe(tab) => {
load_tab(world, &mut ctx, tab.as_mut());
tab_assets.push(tab.asset_ref);
}
TabType::Empty => {}
}
}
+136
View File
@@ -0,0 +1,136 @@
use bevy::{
asset::LoadState,
ecs::system::{lifetimeless::*, *},
prelude::*,
};
use bevy_egui::{EguiContext, EguiUserTextures};
use retrolib::format::txtr::ETextureType;
use crate::{
icon,
loaders::lightprobe::LightProbeAsset,
tabs::{texture::LoadedTexture, SystemTab, TabState},
AssetRef,
};
#[derive(Default)]
pub struct LightProbeTab {
pub asset_ref: AssetRef,
pub handle: Handle<LightProbeAsset>,
pub loaded_textures: Vec<Vec<LoadedTexture>>,
}
impl SystemTab for LightProbeTab {
type LoadParam =
(SRes<Assets<LightProbeAsset>>, SResMut<Assets<Image>>, SResMut<EguiUserTextures>);
type UiParam = (SRes<AssetServer>, SRes<Assets<LightProbeAsset>>);
fn load(&mut self, _ctx: &mut EguiContext, query: SystemParamItem<'_, '_, Self::LoadParam>) {
if !self.loaded_textures.is_empty() {
return;
}
let (assets, mut images, mut egui_textures) = query;
let Some(asset) = assets.get(&self.handle) else { return; };
self.loaded_textures.reserve_exact(asset.textures.len());
for texture in &asset.textures {
let mut slices = Vec::with_capacity(texture.slices.len());
for mip in &texture.slices {
let mut texture_ids = Vec::with_capacity(mip.len());
for image in mip {
let handle = images.add(image.clone());
texture_ids.push(egui_textures.add_image(handle));
}
let size = mip.first().map(|m| m.texture_descriptor.size).unwrap_or_default();
slices.push(LoadedTexture { texture_ids, width: size.width, height: size.height });
}
self.loaded_textures.push(slices);
}
}
fn ui(
&mut self,
ui: &mut egui::Ui,
query: SystemParamItem<'_, '_, Self::UiParam>,
_state: &mut TabState,
) {
let (server, assets) = query;
ui.label(format!("{} {}", self.asset_ref.kind, self.asset_ref.id));
match server.get_load_state(&self.handle) {
LoadState::NotLoaded => {
return;
}
LoadState::Loading => {
ui.spinner();
return;
}
LoadState::Loaded => {}
LoadState::Failed => {
ui.colored_label(egui::Color32::RED, "Loading failed");
return;
}
LoadState::Unloaded => {
return;
}
};
let Some(asset) = assets.get(&self.handle) else { return; };
for (txtr_idx, txtr) in asset.textures.iter().enumerate() {
ui.group(|ui| {
ui.label(format!("Type: {}", txtr.inner.head.kind));
ui.label(format!("Format: {}", txtr.inner.head.format));
ui.label(format!(
"Size: {}x{}x{} (mips: {})",
txtr.inner.head.width,
txtr.inner.head.height,
txtr.inner.head.layers,
txtr.inner.head.mip_sizes.len()
));
let mip = &self.loaded_textures[txtr_idx][0];
if self.loaded_textures.len() > 1 {
ui.label(format!(
"Mipmap size: {}x{}x{}",
mip.width,
mip.height,
mip.texture_ids.len(),
));
}
let size = egui::Vec2::new(mip.width as f32, mip.height as f32);
let draw_image =
|ui: &mut egui::Ui, rect: &egui::Rect, i: usize, x: u32, y: u32, flip: bool| {
let min = rect.min + size * egui::Vec2::new(x as f32, y as f32);
let y_range = if flip { 1.0..=0.0 } else { 0.0..=1.0 };
egui::widgets::Image::new(mip.texture_ids[i], size)
.uv(egui::Rect::from_x_y_ranges(0.0..=1.0, y_range))
.paint_at(ui, egui::Rect::from_min_size(min, size));
};
if txtr.inner.head.kind == ETextureType::Cube && mip.texture_ids.len() == 6 {
let (_, rect) = ui.allocate_space(size * egui::Vec2::new(4.0, 3.0));
draw_image(ui, &rect, 2, 1, 0, false);
draw_image(ui, &rect, 1, 0, 1, false);
draw_image(ui, &rect, 4, 1, 1, false);
draw_image(ui, &rect, 0, 2, 1, false);
draw_image(ui, &rect, 5, 3, 1, false);
draw_image(ui, &rect, 3, 1, 2, false);
} else {
let (_, rect) = ui
.allocate_space(size * egui::Vec2::new(mip.texture_ids.len() as f32, 1.0));
for i in 0..mip.texture_ids.len() {
draw_image(ui, &rect, i, i as u32, 0, false);
}
}
});
}
}
fn title(&mut self) -> egui::WidgetText {
format!("{} {} {}", icon::LIGHTPROBE_CUBEMAP, self.asset_ref.kind, self.asset_ref.id).into()
}
fn id(&self) -> String { format!("{} {}", self.asset_ref.kind, self.asset_ref.id) }
}
+8
View File
@@ -1,3 +1,4 @@
pub mod lightprobe;
pub mod modcon;
pub mod model;
pub mod project;
@@ -13,6 +14,7 @@ pub enum TabType {
Texture(Box<texture::TextureTab>),
Model(Box<model::ModelTab>),
ModCon(Box<modcon::ModConTab>),
LightProbe(Box<lightprobe::LightProbeTab>),
Empty,
}
@@ -82,6 +84,7 @@ impl egui_dock::TabViewer for TabViewer<'_> {
TabType::Texture(tab) => render_tab(self.world, ui, tab.as_mut(), &mut self.state),
TabType::Model(tab) => render_tab(self.world, ui, tab.as_mut(), &mut self.state),
TabType::ModCon(tab) => render_tab(self.world, ui, tab.as_mut(), &mut self.state),
TabType::LightProbe(tab) => render_tab(self.world, ui, tab.as_mut(), &mut self.state),
TabType::Empty => {}
}
}
@@ -92,6 +95,7 @@ impl egui_dock::TabViewer for TabViewer<'_> {
TabType::Texture(tab) => tab.title(),
TabType::Model(tab) => tab.title(),
TabType::ModCon(tab) => tab.title(),
TabType::LightProbe(tab) => tab.title(),
TabType::Empty => "".into(),
}
}
@@ -111,6 +115,10 @@ impl egui_dock::TabViewer for TabViewer<'_> {
close_tab(self.world, tab.as_mut());
true
}
TabType::LightProbe(tab) => {
close_tab(self.world, tab.as_mut());
true
}
TabType::Empty => false,
}
}
+15 -1
View File
@@ -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},
ltpb::K_FORM_LTPB,
mcon::K_FORM_MCON,
txtr::{ETextureFormat, ETextureType, K_FORM_TXTR},
FourCC,
@@ -16,7 +17,10 @@ use retrolib::format::{
use crate::{
icon,
loaders::{model::ModelAsset, package::PackageDirectory, texture::TextureAsset},
tabs::{modcon::ModConTab, model::ModelTab, texture::TextureTab, SystemTab, TabState, TabType},
tabs::{
lightprobe::LightProbeTab, modcon::ModConTab, model::ModelTab, texture::TextureTab,
SystemTab, TabState, TabType,
},
AssetRef,
};
@@ -178,6 +182,7 @@ impl SystemTab for ProjectTab {
K_FORM_CMDL | K_FORM_SMDL | K_FORM_WMDL => icon::FILE_3D,
K_FORM_FMV0 => icon::FILE_MOVIE,
K_FORM_ROOM | K_FORM_MCON => icon::SCENE_DATA,
K_FORM_LTPB => icon::LIGHTPROBE_GRID,
_ => icon::FILE,
},
entry.kind,
@@ -239,6 +244,15 @@ impl SystemTab for ProjectTab {
..default()
})));
}
K_FORM_LTPB => {
let handle = server.load(format!("{}.{}", entry.id, entry.kind));
state.open_tab =
Some(TabType::LightProbe(Box::new(LightProbeTab {
asset_ref,
handle,
..default()
})));
}
_ => {}
}
}