Task 799: .VTF File Format
Task 799: .VTF File Format
1. Properties of the .VTF File Format
The .VTF (Valve Texture Format) is a binary file format used for textures in Valve's Source engine. Its intrinsic properties are derived from its header structure and associated data sections, which vary slightly by version (major version is always 7, minor versions range from 0 to 6). The format supports 2D textures, cubemaps, volumetric textures, mipmaps, animations, and various compression schemes. Below is a comprehensive list of properties based on the file structure, including header fields, enums, and data sections. These are intrinsic to the file's binary layout and are consistent across implementations.
Header Properties (Common to All Versions Unless Noted)
- Signature: A 4-byte string identifier, always "VTF\0" (offset 0).
- Major Version: A 32-bit unsigned integer, always 7 (offset 4).
- Minor Version: A 32-bit unsigned integer (0–6), indicating format revisions (offset 8). Examples: 0 (initial), 2 (adds depth), 3 (adds resources), 5 (revises flags).
- Header Size: A 32-bit unsigned integer specifying the size of the header in bytes (offset 12). Typically 80 bytes for v7.0–v7.2, 96 bytes for v7.3+.
- Width: A 16-bit unsigned integer for the base texture width in pixels (offset 16). Must be a power of 2 or multiple of 4 for certain formats.
- Height: A 16-bit unsigned integer for the base texture height in pixels (offset 18). Same constraints as width.
- Flags: A 32-bit bitfield of texture flags (offset 20). Bit values include:
- 0x00000001: Point sampling (no filtering).
- 0x00000002: Trilinear filtering.
- 0x00000004: Clamp S (X-axis wrapping).
- 0x00000008: Clamp T (Y-axis wrapping).
- 0x00000010: Anisotropic filtering.
- 0x00000040: sRGB gamma correction (position varies by version; 0x00000040 in v7.4, 0x00080000 in v7.5+).
- 0x00000080: Normal map.
- 0x00000100: No mipmaps.
- 0x00000200: No level-of-detail.
- 0x00000400: All mips (v7.3+) or minimum mip (v7.2–v7.3).
- 0x00000800: Procedural texture.
- 0x00001000: One-bit alpha.
- 0x00002000: Eight-bit alpha.
- 0x00004000: Environment map (cubemap).
- 0x00020000: No debug override.
- 0x00040000: Single copy (unused).
- 0x00080000: sRGB (v7.5+).
- 0x00800000: No depth buffer (v7.2+).
- 0x02000000: Clamp U (Z-axis wrapping, v7.2+).
- 0x04000000: Vertex texture (v7.3+).
- 0x08000000: Self-shadowing bump map (v7.3+).
- 0x10000000: Most mips (v7.5+).
- 0x20000000: Border clamping (v7.3+).
- Additional game-specific flags in certain branches (e.g., 0x00200000 for combined textures in CS:GO).
- Frame Count: A 16-bit unsigned integer for the number of animation frames (offset 24). Default: 1.
- Start Frame: A 16-bit unsigned integer for the first frame in the animation sequence (offset 26). Default: 0.
- Padding 0: 4 bytes reserved (offset 28).
- Reflectivity: Three 32-bit floats (RGB vector) for surface reflectivity (offset 32).
- Padding 1: 4 bytes reserved (offset 44).
- Bump Map Scale: A 32-bit float for bump map intensity (offset 48).
- Image Format: A 32-bit unsigned integer ID for the pixel format (offset 52). Supported values include:
- 0: RGBA8888
- 1: ABGR8888
- 2: RGB888
- 3: BGR888
- 4: RGB565
- 5: I8 (greyscale)
- 6: IA88 (greyscale + alpha)
- 7: P8 (palette, unused)
- 8: A8 (alpha only)
- 9: RGB888_BLUESCREEN (blue = transparent)
- 10: BGR888_BLUESCREEN (red = transparent)
- 11: ARGB8888
- 12: BGRA8888
- 13: DXT1 (compressed)
- 14: DXT3 (compressed with alpha)
- 15: DXT5 (compressed with alpha)
- 16: BGRX8888
- 17: BGR565
- 18: BGRX5551
- 19: BGRA4444
- 20: DXT1_ONE_BIT_ALPHA
- 21: BGRA5551
- 22: UV88
- 23: UVWQ8888
- 24: RGBA16161616F (HDR)
- 25: RGBA16161616
- 26: UVLX8888
- 27: R32F
- 28: RGB323232F
- 29: RGBA32323232F
- Additional formats in later branches (e.g., 30: RG1616F, 34: ATI2N, 70: BC7 in Strata Source).
- Mip Count: An 8-bit unsigned integer for the number of mipmap levels (offset 56). Calculated as log2(max(width, height)) + 1.
- Thumbnail Format: A 32-bit unsigned integer for low-resolution thumbnail format (offset 57). Always DXT1 (13), regardless of value.
- Thumbnail Width: An 8-bit unsigned integer for thumbnail width (offset 61). Typically 16 or proportional (max dimension 16).
- Thumbnail Height: An 8-bit unsigned integer for thumbnail height (offset 62).
Additional Properties (v7.2+)
- Depth: A 16-bit unsigned integer for texture depth (volumetric textures) (offset 64). Default: 1 (2D).
Additional Properties (v7.3+)
- Padding 2: 3 bytes reserved (offset 66).
- Resource Count: A 32-bit unsigned integer for the number of resource entries (offset 69). Max: 32.
- Padding 3: 8 bytes reserved (offset 73).
- Resources: An array of resource entries immediately after the header. Each is 8 bytes:
- Type: 3-byte identifier (e.g., "\x30\0\0" for image data).
- Flags: 1-byte bitfield (bit 2: inline data if set).
- Data: 32-bit value (offset to data or inline value if flagged).
- Supported types: Thumbnail (\x01\0\0), Particle Sheet (\x10\0\0), Image Data (\x30\0\0), CRC ("CRC"), LOD Control ("LOD"), Extra Flags ("TSO"), KeyValues ("KVD"), Compression Info ("AXC" in Strata).
Data Sections
- Thumbnail Data: Low-res DXT1-compressed image data (after header in pre-v7.3; as resource in v7.3+).
- Image Data: High-res image data, interleaved by mip, frame, face, slice. Order: smallest mip first. Compression optional (e.g., Deflate/Zstd via AXC resource).
- Face Count: Implied (1 for 2D, 6 for cubemap, 7 for cubemap + spheremap in v7.1–v7.4).
- Compression Info (AXC Resource, Strata): Versioned struct with compression method (Deflate/Zstd) and per-block sizes.
The file ends with resource data blocks, aligned to the offsets specified.
2. Direct Download Links for .VTF Files
- https://raw.githubusercontent.com/bouletmarc/hl2_ep2_content/master/materials/matsys_regressiontest/background.vtf
- https://raw.githubusercontent.com/bouletmarc/hl2_ep2_content/master/materials/detail/noise_detail_01.vtf
3. HTML/JavaScript for Drag-and-Drop .VTF Property Dumper
This is an embeddable HTML snippet with JavaScript that can be placed in a blog post (e.g., Ghost platform). It creates a drag-and-drop area. When a .VTF file is dropped, it parses the binary data and displays all properties from the list above in a readable format.
4. Python Class for .VTF Handling
This class can open a .VTF file, decode its properties, print them to console, and write a new .VTF file with modified properties (basic implementation; assumes v7.5 for simplicity, without full image data writing).
import struct
import sys
class VTFHandler:
def __init__(self, filepath=None):
self.properties = {}
if filepath:
self.read(filepath)
def read(self, filepath):
with open(filepath, 'rb') as f:
data = f.read()
offset = 0
# Signature
self.properties['signature'] = struct.unpack_from('<4s', data, offset)[0].decode('utf-8')
offset += 4
# Versions
self.properties['major_version'] = struct.unpack_from('<I', data, offset)[0]
offset += 4
self.properties['minor_version'] = struct.unpack_from('<I', data, offset)[0]
offset += 4
# Header size
self.properties['header_size'] = struct.unpack_from('<I', data, offset)[0]
offset += 4
# Width, height
self.properties['width'] = struct.unpack_from('<H', data, offset)[0]
offset += 2
self.properties['height'] = struct.unpack_from('<H', data, offset)[0]
offset += 2
# Flags
self.properties['flags'] = struct.unpack_from('<I', data, offset)[0]
offset += 4
# Frames
self.properties['frame_count'] = struct.unpack_from('<H', data, offset)[0]
offset += 2
self.properties['start_frame'] = struct.unpack_from('<H', data, offset)[0]
offset += 2
# Padding 0
offset += 4
# Reflectivity
self.properties['reflectivity'] = list(struct.unpack_from('<3f', data, offset))
offset += 12
# Padding 1
offset += 4
# Bump scale
self.properties['bump_scale'] = struct.unpack_from('<f', data, offset)[0]
offset += 4
# Image format
self.properties['image_format'] = struct.unpack_from('<I', data, offset)[0]
offset += 4
# Mip count
self.properties['mip_count'] = struct.unpack_from('<B', data, offset)[0]
offset += 1
# Thumbnail
self.properties['thumbnail_format'] = struct.unpack_from('<I', data, offset)[0]
offset += 4
self.properties['thumbnail_width'] = struct.unpack_from('<B', data, offset)[0]
offset += 1
self.properties['thumbnail_height'] = struct.unpack_from('<B', data, offset)[0]
offset += 1
# Depth (v7.2+)
if self.properties['minor_version'] >= 2:
self.properties['depth'] = struct.unpack_from('<H', data, offset)[0]
offset += 2
# Resources (v7.3+)
if self.properties['minor_version'] >= 3:
offset += 3 # Padding 2
self.properties['resource_count'] = struct.unpack_from('<I', data, offset)[0]
offset += 4
offset += 8 # Padding 3
self.properties['resources'] = []
for _ in range(self.properties['resource_count']):
res_type = data[offset:offset+3].decode('utf-8', errors='ignore')
res_flags = struct.unpack_from('<B', data, offset+3)[0]
res_data = struct.unpack_from('<I', data, offset+4)[0]
self.properties['resources'].append({'type': res_type, 'flags': res_flags, 'data': res_data})
offset += 8
# Note: Image data parsing omitted for brevity; can be added based on offsets.
def print_properties(self):
for key, value in self.properties.items():
print(f"{key}: {value}")
def write(self, filepath):
# Basic write: Reconstruct header from properties (assumes no image data changes)
with open(filepath, 'wb') as f:
offset = 0
data = bytearray(1024) # Buffer
# Signature
struct.pack_into('<4s', data, offset, self.properties['signature'].encode('utf-8'))
offset += 4
# Versions
struct.pack_into('<I', data, offset, self.properties['major_version'])
offset += 4
struct.pack_into('<I', data, offset, self.properties['minor_version'])
offset += 4
# Header size (placeholder; update later)
header_size_pos = offset
offset += 4
# Width, height
struct.pack_into('<H', data, offset, self.properties['width'])
offset += 2
struct.pack_into('<H', data, offset, self.properties['height'])
offset += 2
# Flags
struct.pack_into('<I', data, offset, self.properties['flags'])
offset += 4
# Frames
struct.pack_into('<H', data, offset, self.properties['frame_count'])
offset += 2
struct.pack_into('<H', data, offset, self.properties['start_frame'])
offset += 2
# Padding 0
offset += 4
# Reflectivity
struct.pack_into('<3f', data, offset, *self.properties['reflectivity'])
offset += 12
# Padding 1
offset += 4
# Bump scale
struct.pack_into('<f', data, offset, self.properties['bump_scale'])
offset += 4
# Image format
struct.pack_into('<I', data, offset, self.properties['image_format'])
offset += 4
# Mip count
struct.pack_into('<B', data, offset, self.properties['mip_count'])
offset += 1
# Thumbnail
struct.pack_into('<I', data, offset, self.properties['thumbnail_format'])
offset += 4
struct.pack_into('<B', data, offset, self.properties['thumbnail_width'])
offset += 1
struct.pack_into('<B', data, offset, self.properties['thumbnail_height'])
offset += 1
# Depth (v7.2+)
if self.properties['minor_version'] >= 2:
struct.pack_into('<H', data, offset, self.properties['depth'])
offset += 2
# Resources (v7.3+)
if self.properties['minor_version'] >= 3:
offset += 3 # Padding 2
struct.pack_into('<I', data, offset, self.properties['resource_count'])
offset += 4
offset += 8 # Padding 3
for res in self.properties['resources']:
struct.pack_into('<3s', data, offset, res['type'].encode('utf-8'))
struct.pack_into('<B', data, offset + 3, res['flags'])
struct.pack_into('<I', data, offset + 4, res['data'])
offset += 8
# Update header size
struct.pack_into('<I', data, header_size_pos, offset)
# Write header (image data omitted for brevity)
f.write(data[:offset])
# Example usage
if __name__ == '__main__':
if len(sys.argv) > 1:
vtf = VTFHandler(sys.argv[1])
vtf.print_properties()
vtf.write('output.vtf')
5. Java Class for .VTF Handling
This class handles opening, decoding, printing, and writing .VTF files (basic; v7.5 assumed, little-endian).
import java.io.*;
import java.nio.*;
import java.util.*;
public class VTFHandler {
private Map<String, Object> properties = new HashMap<>();
public VTFHandler(String filepath) throws IOException {
if (filepath != null) {
read(filepath);
}
}
public void read(String filepath) throws IOException {
try (RandomAccessFile raf = new RandomAccessFile(filepath, "r")) {
ByteBuffer buffer = ByteBuffer.allocate((int) raf.length()).order(ByteOrder.LITTLE_ENDIAN);
raf.getChannel().read(buffer);
buffer.flip();
// Signature
byte[] sigBytes = new byte[4];
buffer.get(sigBytes);
properties.put("signature", new String(sigBytes));
// Versions
properties.put("major_version", buffer.getInt());
properties.put("minor_version", buffer.getInt());
// Header size
properties.put("header_size", buffer.getInt());
// Width, height
properties.put("width", (int) buffer.getShort());
properties.put("height", (int) buffer.getShort());
// Flags
properties.put("flags", buffer.getInt());
// Frames
properties.put("frame_count", (int) buffer.getShort());
properties.put("start_frame", (int) buffer.getShort());
// Padding 0
buffer.getInt();
// Reflectivity
float[] reflectivity = new float[3];
for (int i = 0; i < 3; i++) reflectivity[i] = buffer.getFloat();
properties.put("reflectivity", reflectivity);
// Padding 1
buffer.getInt();
// Bump scale
properties.put("bump_scale", buffer.getFloat());
// Image format
properties.put("image_format", buffer.getInt());
// Mip count
properties.put("mip_count", Byte.toUnsignedInt(buffer.get()));
// Thumbnail
properties.put("thumbnail_format", buffer.getInt());
properties.put("thumbnail_width", Byte.toUnsignedInt(buffer.get()));
properties.put("thumbnail_height", Byte.toUnsignedInt(buffer.get()));
// Depth (v7.2+)
int minor = (int) properties.get("minor_version");
if (minor >= 2) {
properties.put("depth", (int) buffer.getShort());
}
// Resources (v7.3+)
if (minor >= 3) {
buffer.position(buffer.position() + 3); // Padding 2
int resourceCount = buffer.getInt();
properties.put("resource_count", resourceCount);
buffer.position(buffer.position() + 8); // Padding 3
List<Map<String, Object>> resources = new ArrayList<>();
for (int i = 0; i < resourceCount; i++) {
byte[] typeBytes = new byte[3];
buffer.get(typeBytes);
String type = new String(typeBytes);
int flags = Byte.toUnsignedInt(buffer.get());
int data = buffer.getInt();
Map<String, Object> res = new HashMap<>();
res.put("type", type);
res.put("flags", flags);
res.put("data", data);
resources.add(res);
}
properties.put("resources", resources);
}
}
}
public void printProperties() {
for (Map.Entry<String, Object> entry : properties.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
}
public void write(String filepath) throws IOException {
try (RandomAccessFile raf = new RandomAccessFile(filepath, "rw")) {
ByteBuffer buffer = ByteBuffer.allocate(1024).order(ByteOrder.LITTLE_ENDIAN);
// Signature
buffer.put(((String) properties.get("signature")).getBytes());
// Versions
buffer.putInt((int) properties.get("major_version"));
buffer.putInt((int) properties.get("minor_version"));
// Header size (placeholder)
int headerSizePos = buffer.position();
buffer.putInt(0);
// Width, height
buffer.putShort((short) (int) properties.get("width"));
buffer.putShort((short) (int) properties.get("height"));
// Flags
buffer.putInt((int) properties.get("flags"));
// Frames
buffer.putShort((short) (int) properties.get("frame_count"));
buffer.putShort((short) (int) properties.get("start_frame"));
// Padding 0
buffer.putInt(0);
// Reflectivity
float[] reflectivity = (float[]) properties.get("reflectivity");
for (float val : reflectivity) buffer.putFloat(val);
// Padding 1
buffer.putInt(0);
// Bump scale
buffer.putFloat((float) properties.get("bump_scale"));
// Image format
buffer.putInt((int) properties.get("image_format"));
// Mip count
buffer.put((byte) (int) properties.get("mip_count"));
// Thumbnail
buffer.putInt((int) properties.get("thumbnail_format"));
buffer.put((byte) (int) properties.get("thumbnail_width"));
buffer.put((byte) (int) properties.get("thumbnail_height"));
// Depth (v7.2+)
int minor = (int) properties.get("minor_version");
if (minor >= 2) {
buffer.putShort((short) (int) properties.get("depth"));
}
// Resources (v7.3+)
if (minor >= 3) {
buffer.position(buffer.position() + 3); // Padding 2
int resourceCount = (int) properties.get("resource_count");
buffer.putInt(resourceCount);
buffer.position(buffer.position() + 8); // Padding 3
@SuppressWarnings("unchecked")
List<Map<String, Object>> resources = (List<Map<String, Object>>) properties.get("resources");
for (Map<String, Object> res : resources) {
buffer.put(((String) res.get("type")).getBytes());
buffer.put((byte) (int) res.get("flags"));
buffer.putInt((int) res.get("data"));
}
}
// Update header size
int headerSize = buffer.position();
buffer.putInt(headerSizePos, headerSize);
// Write
buffer.flip();
raf.getChannel().write(buffer);
}
}
public static void main(String[] args) throws IOException {
if (args.length > 0) {
VTFHandler vtf = new VTFHandler(args[0]);
vtf.printProperties();
vtf.write("output.vtf");
}
}
}
6. JavaScript Class for .VTF Handling
This class works in Node.js (requires 'fs' module). It opens, decodes, prints (to console), and writes .VTF files.
const fs = require('fs');
class VTFHandler {
constructor(filepath) {
this.properties = {};
if (filepath) {
this.read(filepath);
}
}
read(filepath) {
const data = fs.readFileSync(filepath);
const dv = new DataView(data.buffer);
let offset = 0;
// Signature
this.properties.signature = String.fromCharCode(dv.getUint8(offset++), dv.getUint8(offset++), dv.getUint8(offset++), dv.getUint8(offset++));
// Versions
this.properties.major_version = dv.getUint32(offset, true); offset += 4;
this.properties.minor_version = dv.getUint32(offset, true); offset += 4;
// Header size
this.properties.header_size = dv.getUint32(offset, true); offset += 4;
// Width, height
this.properties.width = dv.getUint16(offset, true); offset += 2;
this.properties.height = dv.getUint16(offset, true); offset += 2;
// Flags
this.properties.flags = dv.getUint32(offset, true); offset += 4;
// Frames
this.properties.frame_count = dv.getUint16(offset, true); offset += 2;
this.properties.start_frame = dv.getUint16(offset, true); offset += 2;
// Padding 0
offset += 4;
// Reflectivity
this.properties.reflectivity = [dv.getFloat32(offset, true), dv.getFloat32(offset + 4, true), dv.getFloat32(offset + 8, true)];
offset += 12;
// Padding 1
offset += 4;
// Bump scale
this.properties.bump_scale = dv.getFloat32(offset, true); offset += 4;
// Image format
this.properties.image_format = dv.getUint32(offset, true); offset += 4;
// Mip count
this.properties.mip_count = dv.getUint8(offset++);
// Thumbnail
this.properties.thumbnail_format = dv.getUint32(offset, true); offset += 4;
this.properties.thumbnail_width = dv.getUint8(offset++);
this.properties.thumbnail_height = dv.getUint8(offset++);
// Depth (v7.2+)
if (this.properties.minor_version >= 2) {
this.properties.depth = dv.getUint16(offset, true); offset += 2;
}
// Resources (v7.3+)
if (this.properties.minor_version >= 3) {
offset += 3; // Padding 2
this.properties.resource_count = dv.getUint32(offset, true); offset += 4;
offset += 8; // Padding 3
this.properties.resources = [];
for (let i = 0; i < this.properties.resource_count; i++) {
const type = String.fromCharCode(dv.getUint8(offset), dv.getUint8(offset + 1), dv.getUint8(offset + 2));
const flags = dv.getUint8(offset + 3);
const resData = dv.getUint32(offset + 4, true);
this.properties.resources.push({ type, flags, data: resData });
offset += 8;
}
}
}
printProperties() {
console.log(JSON.stringify(this.properties, null, 2));
}
write(filepath) {
const buffer = new ArrayBuffer(1024);
const dv = new DataView(buffer);
let offset = 0;
// Signature
for (let char of this.properties.signature) {
dv.setUint8(offset++, char.charCodeAt(0));
}
// Versions
dv.setUint32(offset, this.properties.major_version, true); offset += 4;
dv.setUint32(offset, this.properties.minor_version, true); offset += 4;
// Header size (placeholder)
const headerSizePos = offset;
offset += 4;
// Width, height
dv.setUint16(offset, this.properties.width, true); offset += 2;
dv.setUint16(offset, this.properties.height, true); offset += 2;
// Flags
dv.setUint32(offset, this.properties.flags, true); offset += 4;
// Frames
dv.setUint16(offset, this.properties.frame_count, true); offset += 2;
dv.setUint16(offset, this.properties.start_frame, true); offset += 2;
// Padding 0
offset += 4;
// Reflectivity
for (let val of this.properties.reflectivity) {
dv.setFloat32(offset, val, true); offset += 4;
}
// Padding 1
offset += 4;
// Bump scale
dv.setFloat32(offset, this.properties.bump_scale, true); offset += 4;
// Image format
dv.setUint32(offset, this.properties.image_format, true); offset += 4;
// Mip count
dv.setUint8(offset++, this.properties.mip_count);
// Thumbnail
dv.setUint32(offset, this.properties.thumbnail_format, true); offset += 4;
dv.setUint8(offset++, this.properties.thumbnail_width);
dv.setUint8(offset++, this.properties.thumbnail_height);
// Depth (v7.2+)
if (this.properties.minor_version >= 2) {
dv.setUint16(offset, this.properties.depth, true); offset += 2;
}
// Resources (v7.3+)
if (this.properties.minor_version >= 3) {
offset += 3; // Padding 2
dv.setUint32(offset, this.properties.resource_count, true); offset += 4;
offset += 8; // Padding 3
for (let res of this.properties.resources) {
for (let char of res.type) {
dv.setUint8(offset++, char.charCodeAt(0));
}
dv.setUint8(offset++, res.flags);
dv.setUint32(offset, res.data, true); offset += 4;
}
}
// Update header size
dv.setUint32(headerSizePos, offset, true);
// Write
fs.writeFileSync(filepath, new Uint8Array(buffer, 0, offset));
}
}
// Example usage: node script.js input.vtf
if (process.argv.length > 2) {
const vtf = new VTFHandler(process.argv[2]);
vtf.printProperties();
vtf.write('output.vtf');
}
7. C "Class" for .VTF Handling
In C, we use a struct with functions. This handles opening, decoding, printing, and writing (basic; assumes little-endian host).
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
typedef struct {
char signature[5];
uint32_t major_version;
uint32_t minor_version;
uint32_t header_size;
uint16_t width;
uint16_t height;
uint32_t flags;
uint16_t frame_count;
uint16_t start_frame;
float reflectivity[3];
float bump_scale;
uint32_t image_format;
uint8_t mip_count;
uint32_t thumbnail_format;
uint8_t thumbnail_width;
uint8_t thumbnail_height;
uint16_t depth; // v7.2+
uint32_t resource_count; // v7.3+
struct Resource {
char type[4];
uint8_t flags;
uint32_t data;
} *resources; // Array of resources
} VTFProperties;
void read_vtf(const char* filepath, VTFProperties* props) {
FILE* f = fopen(filepath, "rb");
if (!f) return;
fseek(f, 0, SEEK_END);
long size = ftell(f);
fseek(f, 0, SEEK_SET);
uint8_t* data = malloc(size);
fread(data, 1, size, f);
fclose(f);
uint32_t offset = 0;
// Signature
memcpy(props->signature, data + offset, 4);
props->signature[4] = '\0';
offset += 4;
// Versions
memcpy(&props->major_version, data + offset, 4); offset += 4;
memcpy(&props->minor_version, data + offset, 4); offset += 4;
// Header size
memcpy(&props->header_size, data + offset, 4); offset += 4;
// Width, height
memcpy(&props->width, data + offset, 2); offset += 2;
memcpy(&props->height, data + offset, 2); offset += 2;
// Flags
memcpy(&props->flags, data + offset, 4); offset += 4;
// Frames
memcpy(&props->frame_count, data + offset, 2); offset += 2;
memcpy(&props->start_frame, data + offset, 2); offset += 2;
// Padding 0
offset += 4;
// Reflectivity
memcpy(props->reflectivity, data + offset, 12); offset += 12;
// Padding 1
offset += 4;
// Bump scale
memcpy(&props->bump_scale, data + offset, 4); offset += 4;
// Image format
memcpy(&props->image_format, data + offset, 4); offset += 4;
// Mip count
props->mip_count = data[offset++];
// Thumbnail
memcpy(&props->thumbnail_format, data + offset, 4); offset += 4;
props->thumbnail_width = data[offset++];
props->thumbnail_height = data[offset++];
// Depth (v7.2+)
if (props->minor_version >= 2) {
memcpy(&props->depth, data + offset, 2); offset += 2;
} else {
props->depth = 1;
}
// Resources (v7.3+)
if (props->minor_version >= 3) {
offset += 3; // Padding 2
memcpy(&props->resource_count, data + offset, 4); offset += 4;
offset += 8; // Padding 3
props->resources = malloc(sizeof(struct Resource) * props->resource_count);
for (uint32_t i = 0; i < props->resource_count; i++) {
memcpy(props->resources[i].type, data + offset, 3);
props->resources[i].type[3] = '\0';
props->resources[i].flags = data[offset + 3];
memcpy(&props->resources[i].data, data + offset + 4, 4);
offset += 8;
}
} else {
props->resource_count = 0;
props->resources = NULL;
}
free(data);
}
void print_properties(const VTFProperties* props) {
printf("signature: %s\n", props->signature);
printf("major_version: %u\n", props->major_version);
printf("minor_version: %u\n", props->minor_version);
printf("header_size: %u\n", props->header_size);
printf("width: %u\n", props->width);
printf("height: %u\n", props->height);
printf("flags: %u\n", props->flags);
printf("frame_count: %u\n", props->frame_count);
printf("start_frame: %u\n", props->start_frame);
printf("reflectivity: [%f, %f, %f]\n", props->reflectivity[0], props->reflectivity[1], props->reflectivity[2]);
printf("bump_scale: %f\n", props->bump_scale);
printf("image_format: %u\n", props->image_format);
printf("mip_count: %u\n", props->mip_count);
printf("thumbnail_format: %u\n", props->thumbnail_format);
printf("thumbnail_width: %u\n", props->thumbnail_width);
printf("thumbnail_height: %u\n", props->thumbnail_height);
printf("depth: %u\n", props->depth);
printf("resource_count: %u\n", props->resource_count);
for (uint32_t i = 0; i < props->resource_count; i++) {
printf("resource %u: type=%s, flags=%u, data=%u\n", i, props->resources[i].type, props->resources[i].flags, props->resources[i].data);
}
}
void write_vtf(const char* filepath, const VTFProperties* props) {
FILE* f = fopen(filepath, "wb");
if (!f) return;
uint32_t offset = 0;
uint8_t buffer[1024] = {0};
// Signature
memcpy(buffer + offset, props->signature, 4); offset += 4;
// Versions
memcpy(buffer + offset, &props->major_version, 4); offset += 4;
memcpy(buffer + offset, &props->minor_version, 4); offset += 4;
// Header size (placeholder)
uint32_t header_size_pos = offset;
offset += 4;
// Width, height
memcpy(buffer + offset, &props->width, 2); offset += 2;
memcpy(buffer + offset, &props->height, 2); offset += 2;
// Flags
memcpy(buffer + offset, &props->flags, 4); offset += 4;
// Frames
memcpy(buffer + offset, &props->frame_count, 2); offset += 2;
memcpy(buffer + offset, &props->start_frame, 2); offset += 2;
// Padding 0
offset += 4;
// Reflectivity
memcpy(buffer + offset, props->reflectivity, 12); offset += 12;
// Padding 1
offset += 4;
// Bump scale
memcpy(buffer + offset, &props->bump_scale, 4); offset += 4;
// Image format
memcpy(buffer + offset, &props->image_format, 4); offset += 4;
// Mip count
buffer[offset++] = props->mip_count;
// Thumbnail
memcpy(buffer + offset, &props->thumbnail_format, 4); offset += 4;
buffer[offset++] = props->thumbnail_width;
buffer[offset++] = props->thumbnail_height;
// Depth (v7.2+)
if (props->minor_version >= 2) {
memcpy(buffer + offset, &props->depth, 2); offset += 2;
}
// Resources (v7.3+)
if (props->minor_version >= 3) {
offset += 3; // Padding 2
memcpy(buffer + offset, &props->resource_count, 4); offset += 4;
offset += 8; // Padding 3
for (uint32_t i = 0; i < props->resource_count; i++) {
memcpy(buffer + offset, props->resources[i].type, 3); offset += 3;
buffer[offset++] = props->resources[i].flags;
memcpy(buffer + offset, &props->resources[i].data, 4); offset += 4;
}
}
// Update header size
memcpy(buffer + header_size_pos, &offset, 4);
// Write
fwrite(buffer, 1, offset, f);
fclose(f);
}
void free_vtf(VTFProperties* props) {
if (props->resources) free(props->resources);
}
int main(int argc, char** argv) {
if (argc > 1) {
VTFProperties props = {0};
read_vtf(argv[1], &props);
print_properties(&props);
write_vtf("output.vtf", &props);
free_vtf(&props);
}
return 0;
}