2023-08-06 08:25:04 -04:00
|
|
|
// SPDX-FileCopyrightText: 2023 Joshua Goins <josh@redstrate.com>
|
|
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
|
2023-09-22 19:17:24 -04:00
|
|
|
#![allow(clippy::needless_range_loop)]
|
|
|
|
|
2022-08-16 11:52:07 -04:00
|
|
|
use std::io::{Cursor, Read, Seek, SeekFrom};
|
2023-08-06 08:25:04 -04:00
|
|
|
|
2024-04-20 13:18:03 -04:00
|
|
|
use crate::ByteSpan;
|
|
|
|
use binrw::BinRead;
|
2025-03-06 18:52:26 -05:00
|
|
|
use binrw::binrw;
|
2023-08-06 08:25:04 -04:00
|
|
|
use bitflags::bitflags;
|
2024-04-20 10:46:59 -04:00
|
|
|
use texture2ddecoder::{decode_bc1, decode_bc3, decode_bc5};
|
2023-08-06 08:25:04 -04:00
|
|
|
|
2022-08-11 09:23:15 -04:00
|
|
|
// Attributes and Format are adapted from Lumina (https://github.com/NotAdam/Lumina/blob/master/src/Lumina/Data/Files/TexFile.cs)
|
|
|
|
bitflags! {
|
2022-10-13 17:11:03 -04:00
|
|
|
#[binrw]
|
2022-08-11 09:23:15 -04:00
|
|
|
struct TextureAttribute : u32 {
|
2022-09-15 16:31:56 -04:00
|
|
|
const DISCARD_PER_FRAME = 0x1;
|
|
|
|
const DISCARD_PER_MAP = 0x2;
|
2022-08-11 09:23:15 -04:00
|
|
|
|
2022-09-15 16:31:56 -04:00
|
|
|
const MANAGED = 0x4;
|
|
|
|
const USER_MANAGED = 0x8;
|
|
|
|
const CPU_READ = 0x10;
|
|
|
|
const LOCATION_MAIN = 0x20;
|
|
|
|
const NO_GPU_READ = 0x40;
|
|
|
|
const ALIGNED_SIZE = 0x80;
|
|
|
|
const EDGE_CULLING = 0x100;
|
|
|
|
const LOCATION_ONION = 0x200;
|
|
|
|
const READ_WRITE = 0x400;
|
|
|
|
const IMMUTABLE = 0x800;
|
2022-08-11 09:23:15 -04:00
|
|
|
|
2022-09-15 16:31:56 -04:00
|
|
|
const TEXTURE_RENDER_TARGET = 0x100000;
|
|
|
|
const TEXTURE_DEPTH_STENCIL = 0x200000;
|
|
|
|
const TEXTURE_TYPE1_D = 0x400000;
|
|
|
|
const TEXTURE_TYPE2_D = 0x800000;
|
|
|
|
const TEXTURE_TYPE3_D = 0x1000000;
|
|
|
|
const TEXTURE_TYPE_CUBE = 0x2000000;
|
|
|
|
const TEXTURE_TYPE_MASK = 0x3C00000;
|
|
|
|
const TEXTURE_SWIZZLE = 0x4000000;
|
|
|
|
const TEXTURE_NO_TILED = 0x8000000;
|
|
|
|
const TEXTURE_NO_SWIZZLE = 0x80000000;
|
2022-08-11 09:23:15 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-10-13 17:11:03 -04:00
|
|
|
#[binrw]
|
|
|
|
#[brw(repr = u32)]
|
2022-08-11 15:03:12 -04:00
|
|
|
#[derive(Debug)]
|
|
|
|
enum TextureFormat {
|
2024-04-20 10:00:13 -04:00
|
|
|
B4G4R4A4 = 0x1440,
|
2022-08-11 15:03:12 -04:00
|
|
|
B8G8R8A8 = 0x1450,
|
|
|
|
BC1 = 0x3420,
|
2024-04-17 20:26:45 -04:00
|
|
|
BC3 = 0x3431,
|
|
|
|
BC5 = 0x6230,
|
2022-08-11 09:23:15 -04:00
|
|
|
}
|
2022-08-09 19:31:32 -04:00
|
|
|
|
2022-10-13 17:11:03 -04:00
|
|
|
#[binrw]
|
2022-08-09 19:31:32 -04:00
|
|
|
#[derive(Debug)]
|
2022-09-15 16:26:31 -04:00
|
|
|
#[allow(dead_code)]
|
2022-10-13 17:11:03 -04:00
|
|
|
#[brw(little)]
|
2022-08-09 19:31:32 -04:00
|
|
|
struct TexHeader {
|
2022-08-16 11:52:07 -04:00
|
|
|
attribute: TextureAttribute,
|
2022-08-11 09:23:15 -04:00
|
|
|
format: TextureFormat,
|
2022-08-09 19:31:32 -04:00
|
|
|
|
2022-08-16 11:52:07 -04:00
|
|
|
width: u16,
|
|
|
|
height: u16,
|
|
|
|
depth: u16,
|
|
|
|
mip_levels: u16,
|
2022-08-09 19:31:32 -04:00
|
|
|
|
2022-08-16 11:52:07 -04:00
|
|
|
lod_offsets: [u32; 3],
|
|
|
|
offset_to_surface: [u32; 13],
|
2022-08-09 19:31:32 -04:00
|
|
|
}
|
|
|
|
|
2024-04-30 18:01:09 -04:00
|
|
|
#[repr(C)]
|
|
|
|
#[derive(Clone, Copy)]
|
|
|
|
pub enum TextureType {
|
|
|
|
TwoDimensional,
|
2024-05-18 09:42:07 -04:00
|
|
|
ThreeDimensional,
|
2024-04-30 18:01:09 -04:00
|
|
|
}
|
|
|
|
|
2022-08-09 19:31:32 -04:00
|
|
|
pub struct Texture {
|
2024-04-30 18:01:09 -04:00
|
|
|
/// Type of texture
|
|
|
|
pub texture_type: TextureType,
|
2023-12-02 19:55:50 -05:00
|
|
|
/// Width of the texture in pixels
|
2022-08-11 15:48:28 -04:00
|
|
|
pub width: u32,
|
2023-12-02 19:55:50 -05:00
|
|
|
/// Height of the texture in pixels
|
2022-08-11 15:48:28 -04:00
|
|
|
pub height: u32,
|
2024-04-30 18:01:09 -04:00
|
|
|
/// Depth of the texture in pixels
|
|
|
|
pub depth: u32,
|
2023-12-02 19:55:50 -05:00
|
|
|
/// Raw RGBA data
|
2022-08-16 11:52:07 -04:00
|
|
|
pub rgba: Vec<u8>,
|
2022-08-09 19:31:32 -04:00
|
|
|
}
|
|
|
|
|
2024-04-20 10:46:59 -04:00
|
|
|
type DecodeFunction = fn(&[u8], usize, usize, &mut [u32]) -> Result<(), &'static str>;
|
|
|
|
|
2022-08-09 19:31:32 -04:00
|
|
|
impl Texture {
|
2023-12-02 19:55:50 -05:00
|
|
|
/// Reads an existing TEX file
|
2023-10-13 16:16:04 -04:00
|
|
|
pub fn from_existing(buffer: ByteSpan) -> Option<Texture> {
|
2022-08-09 19:31:32 -04:00
|
|
|
let mut cursor = Cursor::new(buffer);
|
2024-04-20 13:00:07 -04:00
|
|
|
let header = TexHeader::read(&mut cursor).ok()?;
|
2022-08-09 19:31:32 -04:00
|
|
|
|
2022-08-16 11:52:07 -04:00
|
|
|
cursor
|
2024-04-17 20:26:45 -04:00
|
|
|
.seek(SeekFrom::Start(std::mem::size_of::<TexHeader>() as u64))
|
2022-08-16 11:52:07 -04:00
|
|
|
.ok()?;
|
2022-08-11 15:03:12 -04:00
|
|
|
|
2024-04-18 22:07:57 -04:00
|
|
|
let mut src = vec![0u8; buffer.len() - std::mem::size_of::<TexHeader>()];
|
2022-08-11 15:03:12 -04:00
|
|
|
cursor.read_exact(src.as_mut_slice()).ok()?;
|
|
|
|
|
2024-04-20 13:18:03 -04:00
|
|
|
let mut dst: Vec<u8>;
|
2022-08-11 15:03:12 -04:00
|
|
|
|
|
|
|
match header.format {
|
2024-04-20 10:00:13 -04:00
|
|
|
TextureFormat::B4G4R4A4 => {
|
2024-05-18 09:42:07 -04:00
|
|
|
dst =
|
|
|
|
vec![
|
|
|
|
0u8;
|
|
|
|
header.width as usize * header.height as usize * header.depth as usize * 4
|
|
|
|
];
|
2024-04-20 10:00:13 -04:00
|
|
|
|
|
|
|
let mut offset = 0;
|
|
|
|
let mut dst_offset = 0;
|
|
|
|
|
2024-06-26 23:45:50 -04:00
|
|
|
for _ in 0..header.width as usize * header.height as usize {
|
2024-04-20 10:00:13 -04:00
|
|
|
let short: u16 = ((src[offset] as u16) << 8) | src[offset + 1] as u16;
|
|
|
|
|
|
|
|
let src_b = short & 0xF;
|
2024-04-20 13:18:03 -04:00
|
|
|
let src_g = (short >> 4) & 0xF;
|
2024-04-20 10:00:13 -04:00
|
|
|
let src_r = (short >> 8) & 0xF;
|
|
|
|
let src_a = (short >> 12) & 0xF;
|
|
|
|
|
|
|
|
dst[dst_offset] = (17 * src_r) as u8;
|
|
|
|
dst[dst_offset + 1] = (17 * src_g) as u8;
|
|
|
|
dst[dst_offset + 2] = (17 * src_b) as u8;
|
|
|
|
dst[dst_offset + 3] = (17 * src_a) as u8;
|
|
|
|
|
|
|
|
offset += 2;
|
|
|
|
dst_offset += 4;
|
|
|
|
}
|
|
|
|
}
|
2022-08-11 15:03:12 -04:00
|
|
|
TextureFormat::B8G8R8A8 => {
|
2024-05-18 09:42:07 -04:00
|
|
|
dst =
|
|
|
|
vec![
|
|
|
|
0u8;
|
|
|
|
header.width as usize * header.height as usize * header.depth as usize * 4
|
|
|
|
];
|
2024-04-27 21:16:01 -04:00
|
|
|
|
|
|
|
let mut offset = 0;
|
|
|
|
|
2024-06-26 23:45:50 -04:00
|
|
|
for _ in 0..header.width as usize * header.height as usize * header.depth as usize {
|
2024-04-27 21:16:01 -04:00
|
|
|
let src_b = src[offset];
|
|
|
|
let src_g = src[offset + 1];
|
|
|
|
let src_r = src[offset + 2];
|
|
|
|
let src_a = src[offset + 3];
|
|
|
|
|
|
|
|
dst[offset] = src_r;
|
|
|
|
dst[offset + 1] = src_g;
|
|
|
|
dst[offset + 2] = src_b;
|
|
|
|
dst[offset + 3] = src_a;
|
|
|
|
|
|
|
|
offset += 4;
|
|
|
|
}
|
2022-08-11 15:03:12 -04:00
|
|
|
}
|
|
|
|
TextureFormat::BC1 => {
|
2024-04-20 13:18:03 -04:00
|
|
|
dst = Texture::decode(
|
|
|
|
&src,
|
|
|
|
header.width as usize,
|
2024-04-30 18:01:09 -04:00
|
|
|
header.height as usize * header.depth as usize,
|
2024-04-20 13:18:03 -04:00
|
|
|
decode_bc1,
|
|
|
|
);
|
2022-08-11 15:03:12 -04:00
|
|
|
}
|
2024-04-17 20:26:45 -04:00
|
|
|
TextureFormat::BC3 => {
|
2024-04-20 13:18:03 -04:00
|
|
|
dst = Texture::decode(
|
|
|
|
&src,
|
|
|
|
header.width as usize,
|
2024-04-30 18:01:09 -04:00
|
|
|
header.height as usize * header.depth as usize,
|
2024-04-20 13:18:03 -04:00
|
|
|
decode_bc3,
|
|
|
|
);
|
2022-08-11 15:03:12 -04:00
|
|
|
}
|
2024-04-17 20:26:45 -04:00
|
|
|
TextureFormat::BC5 => {
|
2024-04-20 13:18:03 -04:00
|
|
|
dst = Texture::decode(
|
|
|
|
&src,
|
|
|
|
header.width as usize,
|
2024-04-30 18:01:09 -04:00
|
|
|
header.height as usize * header.depth as usize,
|
2024-04-20 13:18:03 -04:00
|
|
|
decode_bc5,
|
|
|
|
);
|
2024-04-17 20:26:45 -04:00
|
|
|
}
|
2022-08-11 15:03:12 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
Some(Texture {
|
2024-05-18 09:42:07 -04:00
|
|
|
texture_type: if header.attribute.contains(TextureAttribute::TEXTURE_TYPE3_D) {
|
|
|
|
TextureType::ThreeDimensional
|
|
|
|
} else {
|
|
|
|
TextureType::TwoDimensional
|
|
|
|
},
|
2022-08-11 15:47:02 -04:00
|
|
|
width: header.width as u32,
|
|
|
|
height: header.height as u32,
|
2024-04-30 18:01:09 -04:00
|
|
|
depth: header.depth as u32,
|
2022-08-16 11:52:07 -04:00
|
|
|
rgba: dst,
|
2022-08-11 15:03:12 -04:00
|
|
|
})
|
2022-08-09 19:31:32 -04:00
|
|
|
}
|
2024-04-20 10:46:59 -04:00
|
|
|
|
|
|
|
fn decode(src: &[u8], width: usize, height: usize, decode_func: DecodeFunction) -> Vec<u8> {
|
2024-04-20 13:17:11 -04:00
|
|
|
let mut image: Vec<u32> = vec![0; width * height];
|
2024-04-20 13:18:03 -04:00
|
|
|
decode_func(src, width, height, &mut image).unwrap();
|
2024-04-20 10:46:59 -04:00
|
|
|
|
|
|
|
image
|
|
|
|
.iter()
|
|
|
|
.flat_map(|x| {
|
|
|
|
let v = x.to_le_bytes();
|
|
|
|
[v[2], v[1], v[0], v[3]]
|
|
|
|
})
|
|
|
|
.collect::<Vec<u8>>()
|
|
|
|
}
|
2022-08-16 11:52:07 -04:00
|
|
|
}
|
2024-04-16 22:08:22 -04:00
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
|
|
|
use std::fs::read;
|
|
|
|
use std::path::PathBuf;
|
|
|
|
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_invalid() {
|
|
|
|
let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
|
|
|
d.push("resources/tests");
|
|
|
|
d.push("random");
|
|
|
|
|
|
|
|
// Feeding it invalid data should not panic
|
|
|
|
Texture::from_existing(&read(d).unwrap());
|
|
|
|
}
|
|
|
|
}
|