Skip to content

Commit

Permalink
Correctly export largest mip only for xbox 360 resources.
Browse files Browse the repository at this point in the history
Change dxt1 to be 3 channels only.
Improve save/error message print.
  • Loading branch information
rob5300 committed Mar 20, 2024
1 parent 675d13f commit d92492a
Show file tree
Hide file tree
Showing 10 changed files with 109 additions and 60 deletions.
21 changes: 18 additions & 3 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,37 @@
"linux": {
"program": "${workspaceFolder}/target/debug/vtfx_reader"
},
"args": ["-i", "${workspaceFolder}/test/scout_head.360.vtf", "-o", "${workspaceFolder}/test", "--open", "--mip0-only"],
"args": ["-i", "${workspaceFolder}/test/scout_head.360.vtf", "-o", "${workspaceFolder}/test", "--open"],
"stopAtEntry": false,
"cwd": "${workspaceFolder}",
"environment": [{"name": "RUST_BACKTRACE", "value": "1"}],
"console": "integratedTerminal",
"preLaunchTask": "rust: cargo build"
},
{
"name": "leadpipe",
"name": "scout_blue",
"type": "cppvsdbg",
"request": "launch",
"program": "${workspaceFolder}/target/debug/vtfx_reader.exe",
"linux": {
"program": "${workspaceFolder}/target/debug/vtfx_reader"
},
"args": ["-i", "${workspaceFolder}/test/v_leadpipe.360.vtf", "-o", "${workspaceFolder}/test", "--open"],
"args": ["-i", "${workspaceFolder}/test/scout_blue.360.vtf", "-o", "${workspaceFolder}/test", "--open"],
"stopAtEntry": false,
"cwd": "${workspaceFolder}",
"environment": [{"name": "RUST_BACKTRACE", "value": "1"}],
"console": "integratedTerminal",
"preLaunchTask": "rust: cargo build"
},
{
"name": "brickwall001_normal",
"type": "cppvsdbg",
"request": "launch",
"program": "${workspaceFolder}/target/debug/vtfx_reader.exe",
"linux": {
"program": "${workspaceFolder}/target/debug/vtfx_reader"
},
"args": ["-i", "${workspaceFolder}/test/brickwall001_normal.360.vtf", "-o", "${workspaceFolder}/test", "--open"],
"stopAtEntry": false,
"cwd": "${workspaceFolder}",
"environment": [{"name": "RUST_BACKTRACE", "value": "1"}],
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "vtfx_reader"
version = "1.2.0"
version = "1.3.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
Expand Down
9 changes: 3 additions & 6 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@

A tool to read the header + output image resources (as png) from a [VTFX file](https://developer.valvesoftware.com/wiki/VTFX_file_format). Written in rust.

- Supports 360 vtf files [\*.360.vtf].
- PS3 files are also supported [\*.vtf] but use the ``--no-dxt-fix`` argument to get a desired output.
- Supports 360 vtf files [\*.360.vtf].
- PS3 files are also supported [\*.vtf], use the ``--no-dxt-fix`` argument to get a desired output.

*Note: X360 images usually have multiple mip levels packed into the texture, functionality to export only the max mip level is in progress and experimental.*
*Note: Xbox 360 vtfx's usually have multiple mip levels packed into the main resource, the largest mip level will be exported.*

## Working texture export formats (Open issue to request):
- DXT1
Expand Down Expand Up @@ -40,9 +40,6 @@ Download the latest release and run, using the arguments listed below to specify
--export-alpha
Export alpha channel

--mip0-only
Try to output only mip 0 (EXPERIMENTAL)

--no-dxt-fix
Do not use big to little endian fix on DXT images

Expand Down
4 changes: 0 additions & 4 deletions src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,6 @@ pub struct Args {
#[arg(long, default_value_t = false)]
pub export_alpha: bool,

/// Try to output only mip 0 (EXPERIMENTAL)
#[arg(long, default_value_t = false)]
pub mip0_only: bool,

/// Do not use big to little endian fix on DXT images
#[arg(long, default_value_t = false)]
pub no_dxt_fix: bool,
Expand Down
14 changes: 13 additions & 1 deletion src/image_format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,12 @@ impl image_format_info
bc_format: bc_format
}
}

pub fn try_get_bc_format(&self) -> Result<texpresso::Format, Box<dyn Error>>
{
let bc_format = self.bc_format.ok_or("Image format is not DXT")?;
Ok(bc_format)
}
}

///Correct endianness of dxt bc data
Expand Down Expand Up @@ -278,7 +284,7 @@ pub fn GetMipMapLevelByteOffset(mut width: i32, mut height: i32, image_format: &

static IMAGE_FORMAT_INFO_MAP: Lazy<HashMap<ImageFormat, image_format_info>> = Lazy::new(|| {
let mut map = HashMap::new();
map.insert(ImageFormat::IMAGE_FORMAT_DXT1, image_format_info::new_with_bc(4, 1, vec![0,1,2,3], Option::from(texpresso::Format::Bc1)));
map.insert(ImageFormat::IMAGE_FORMAT_DXT1, image_format_info::new_with_bc(3, 1, vec![0,1,2], Option::from(texpresso::Format::Bc1)));
map.insert(ImageFormat::IMAGE_FORMAT_DXT5, image_format_info::new_with_bc(4, 1, vec![0,1,2,3], Option::from(texpresso::Format::Bc3)));
map.insert(ImageFormat::IMAGE_FORMAT_RGBA16161616, image_format_info::new(4, 2, vec![0,1,2,3]));
map.insert(ImageFormat::IMAGE_FORMAT_BGRX8888, image_format_info::new(4, 1, vec![2,1,0,3]));
Expand All @@ -304,4 +310,10 @@ impl ImageFormat
}
format_info
}

pub fn try_get_format_info(&self) -> Result<&image_format_info, Box<dyn Error>>
{
let image_format = self.get_format_info().ok_or("vtfx is an unsupported/unknown format")?;
Ok(image_format)
}
}
90 changes: 47 additions & 43 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use std::path::Path;
use std::path::PathBuf;
use std::process::exit;
use args::Args;
use clap::builder::FalseyValueParser;
use clap::Parser;
use image::DynamicImage;
use image::GenericImage;
Expand Down Expand Up @@ -102,6 +103,11 @@ fn read_vtfx(path: &Path) -> Result<VTFXHEADER, Box<dyn Error>> {
println!("[Debug] Has dxt5 hint flag");
}

if cfg!(debug_assertions) && vtfx.has_onebit_alpha()
{
println!("[Debug] Has onebit alpha flag");
}

println!("{}", vtfx);

let mut res_num = 0;
Expand All @@ -117,7 +123,7 @@ fn read_vtfx(path: &Path) -> Result<VTFXHEADER, Box<dyn Error>> {

if !ARGS.no_resource_export
{
match resource_to_image(&buffer, &resource, &vtfx) {
match resource_to_image(&buffer, &resource, &vtfx, &res_num) {
Ok(image) => {
let filename = path.file_stem().unwrap().to_str().unwrap();
let new_image_name = format!("{filename}_resource_{res_num}.png");
Expand All @@ -127,15 +133,15 @@ fn read_vtfx(path: &Path) -> Result<VTFXHEADER, Box<dyn Error>> {
false => PathBuf::from(&new_image_name)
};
image.save_with_format(save_path, image::ImageFormat::Png)?;
println!(" Saved resource image data to '{}'", save_path.as_path().to_string_lossy());
println!(" Saved resource image data to '{}'", save_path.as_path().to_string_lossy());

if ARGS.open
{
println!(" Opening image...");
opener::open(save_path.as_path())?;
}
},
Err(error) => {println!(" Resource to image error: {}", error)},
Err(error) => {println!(" ❌ Error converting resource {} to image: {}", res_num, error)},
}
}

Expand All @@ -151,7 +157,7 @@ fn read_vtfx(path: &Path) -> Result<VTFXHEADER, Box<dyn Error>> {
}

///Extract image resource and return it as DynamicImage
fn resource_to_image(buffer: &[u8], resource_entry_info: &ResourceEntryInfo, vtfx: &VTFXHEADER) -> Result<DynamicImage, Box<dyn Error>>
fn resource_to_image(buffer: &[u8], resource_entry_info: &ResourceEntryInfo, vtfx: &VTFXHEADER, res_num: &i32) -> Result<DynamicImage, Box<dyn Error>>
{
let res_start: usize = resource_entry_info.resData.try_into()?;
//Copy input buffer
Expand All @@ -161,28 +167,31 @@ fn resource_to_image(buffer: &[u8], resource_entry_info: &ResourceEntryInfo, vtf
if format_info.is_some()
{
let format_info_u = format_info.unwrap();
let channels = format_info_u.channels as u32;
let size: u32 = channels * vtfx.width as u32 * vtfx.height as u32;
let mut image_vec: Vec<u8>;

let width = vtfx.width as usize;
let height = vtfx.height as usize;

let mut bc_read_offset = 0;

println!("Resource #{res_num}: w: {width}, h: {height}");

//If this format is BC encoded
if format_info_u.bc_format.is_some()
{
let bc_format = format_info_u.bc_format.unwrap();
image_vec = vec![0; size.try_into()?];
//Allocate space for 4 channels
image_vec = vec![0; width * height * 4];
let image_vec_slice = image_vec.as_mut_slice();
//let mut decompress_buffer: Rc<Vec<u8>>;

if cfg!(debug_assertions){ println!("[Debug] In slice size: {}. Allocated {} bytes for decompression. Channels: {}", resource_buffer.len(), size, channels); }

//What the dxt data size should be
let expected_compressed_size = bc_format.compressed_size(vtfx.width.into(), vtfx.height.into());
let expected_compressed_size = bc_format.compressed_size(width, height);
if expected_compressed_size != resource_buffer.len()
{
//is this lzma?
if &resource_buffer[0..4] == LZMA_MAGIC
{
println!(" Image resource is lzma compressed");
println!(" Image resource is LZMA compressed, decompressing...");
//Decompress and replace resource buffer
resource_buffer = decompress_lzma(&mut resource_buffer, expected_compressed_size, vtfx)?;
}
Expand All @@ -195,17 +204,24 @@ fn resource_to_image(buffer: &[u8], resource_entry_info: &ResourceEntryInfo, vtf

if !ARGS.no_dxt_fix
{
println!(" Image resource requires endian fix before dxt decode");
println!(" Applying endianness fix to resource '{res_num}' before dxt decode...");
correct_dxt_endianness(&bc_format, &mut resource_buffer)?;
}
else
{
println!("! Will skip applying dxt endian fix !")
println!("! Will skip applying dxt endian fix for image resource '{res_num}' !")
}

println!(" Decoding image from {:?}", format_info_u.bc_format.unwrap());
if vtfx.mip_count > 1
{
println!(" Resource {res_num} contains {} mip levels, only mip 0 will be exported", vtfx.mip_count);
bc_read_offset = resource_buffer.len() - vtfx.get_dxt_size()?;
}

println!(" Decoding image from {:?}, DTX buffer offset: {}", format_info_u.bc_format.unwrap(), bc_read_offset);
//Decompress dxt image, if its still compressed this will fail
bc_format.decompress(resource_buffer.as_mut_slice(), vtfx.width.into(), vtfx.height.into(), image_vec_slice);
//Use read offset when getting dtx buffer slice
bc_format.decompress(&mut resource_buffer[bc_read_offset..], width, height, image_vec_slice);
}
else
{
Expand All @@ -214,39 +230,29 @@ fn resource_to_image(buffer: &[u8], resource_entry_info: &ResourceEntryInfo, vtf
}

//Take decompressed data and put into image
let width = vtfx.width as u32;
let height = vtfx.height as u32;
let depth = format_info_u.depth as u32;
let mut output_offset: usize = 0;

if ARGS.mip0_only && vtfx.mip_count > 1
{
output_offset = vtfx.get_mip0_start();//408925 * 4;
//if cfg!(debug_assertions) { println!("Offset {}, diff: {}", vtfx.get_mip0_start(), output_offset - vtfx.get_mip0_start()); }
println!(" (EXPERIMENTAL) Image resource output will try to just be mip0. Some of the image may be missing!");
}

let mut output_image = DynamicImage::new_rgba8(width, height);
let mut output_image = DynamicImage::new_rgba8(vtfx.width.into(), vtfx.height.into());

if image_vec.len() != size as usize
if ARGS.export_alpha
{
let err = io::Error::new(io::ErrorKind::Other, format!("decoded image data is wrong size. Expected: {}, Got: {}", size, image_vec.len()));
return Err(Box::new(err));
println!(" Alpha will be included in the export for image resource '{res_num}'");
}

for y in 0..height
let width_u32 = width as u32;
let depth_u32 = format_info_u.depth as u32;
let channels = format_info_u.channels as usize;
for y in 0..output_image.height()
{
for x in 0..width
for x in 0..output_image.width()
{
let mut pixel: Rgba<u8> = Rgba([255;4]);
//Index of pixel data to read from decoded output
let pixel_index: u32 = output_offset as u32 + ((x + y * width) * depth * channels);
for channel in 0..channels as usize
let pixel_index = (x + y * width_u32) * depth_u32 * 4;
for channel in 0..channels
{
//Using format data, construct index and copy source image pixel colour data
let channel_offset = format_info_u.channel_order[channel] as u32;
//Add channel offset to pixel index.
let from_index: usize = (pixel_index + (channel_offset * depth)) as usize;
let from_index: usize = (pixel_index + (channel_offset * depth_u32)) as usize;

if from_index < image_vec.len()
{
Expand All @@ -260,10 +266,7 @@ fn resource_to_image(buffer: &[u8], resource_entry_info: &ResourceEntryInfo, vtf
pixel[3] = 255;
}

if x < width && y < height
{
output_image.put_pixel(x, y, pixel);
}
output_image.put_pixel(x, y, pixel);
}
}

Expand Down Expand Up @@ -299,7 +302,8 @@ fn decompress_lzma(resource_buffer: &mut Vec<u8>, expected_compressed_size: usiz
new_header_resource_buffer.push(resource_buffer[12]);

//Print valves properties if debug build
if cfg!(debug_assertions) {
#[cfg(debug_assertions)]
{
println!("[Debug LZMA] Dictionary size: {}", dictionary_size);
let mut prop0: u8 = u8::from(resource_buffer[12]);
let original_prop0 = prop0.clone();
Expand All @@ -320,7 +324,7 @@ fn decompress_lzma(resource_buffer: &mut Vec<u8>, expected_compressed_size: usiz
}

lzma_rs::lzma_decompress(&mut &resource_buffer[..], &mut decomp)?;
println!("Decompressed to: {}, Expected: {}", decomp.len(), expected_compressed_size);
println!(" Decompressed to: {}, Expected: {}", decomp.len(), expected_compressed_size);
Ok(decomp)
}

Expand Down
27 changes: 26 additions & 1 deletion src/vtfx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ impl VTFXHEADER
(self.flags & 0x0020) != 0
}

/// Get start of largest mip (width / 2, height / 2)
/// Get start of largest mip
pub fn get_mip0_start(&self) -> usize
{
let mut mips = 0;
Expand All @@ -194,6 +194,31 @@ impl VTFXHEADER
println!("Mip count: {}. Header count: {}", mips, self.mip_count);
lower_mip_sizes
}

///Get start of mip 0 in compressed dxt sizing
pub fn get_mip0_dxt_start(&self) -> Result<usize, Box<dyn Error>>
{
let mip0_start = self.get_mip0_start();
let bc_format = self.image_format.try_get_format_info()?.try_get_bc_format()?;
let bc_ratio: usize = (4 * 4 * self.get_channels() as usize) / bc_format.block_size();

let dxt_offset = mip0_start / bc_ratio;
Ok(dxt_offset)
}

///Get the size of dxt data for this vtfx's mip 0
pub fn get_dxt_size(&self) -> Result<usize, Box<dyn Error>>
{
let decoded_size: usize = self.get_total_size();
let bc_format = self.image_format.try_get_format_info()?.try_get_bc_format()?;
let bc_ratio: usize = (4 * 4 * self.get_channels() as usize) / bc_format.block_size();
Ok(decoded_size / bc_ratio)
}

pub fn get_total_size(&self) -> usize
{
self.width as usize * self.height as usize * self.get_channels() as usize
}
}

impl fmt::Display for VTFXHEADER {
Expand Down
Binary file added test/brickwall001_normal.360.vtf
Binary file not shown.
Binary file removed test/v_leadpipe.360.vtf
Binary file not shown.

0 comments on commit d92492a

Please sign in to comment.