Task 364: .LWO File Format
Task 364: .LWO File Format
The .LWO file format is the LightWave 3D Object format, specifically the LWO2 variant introduced in LightWave 6.0 in 1999. It is an IFF (Interchange File Format)-based binary format that uses big-endian byte order. The file starts with a FORM header followed by a series of chunks and subchunks that define 3D object data, including geometry (points, polygons), surfaces, textures, envelopes, and other attributes. Data is aligned to even byte boundaries, and unknown chunks can be skipped using their length field.
- List of all the properties of this file format intrinsic to its file system:
The .LWO format is chunk-based, with the following intrinsic properties:
- File Header: ID4 ('FORM'), U4 (form length), ID4 ('LWO2').
- Byte Order: Big-endian for all multi-byte integers and floats.
- Alignment: All strings and chunks padded to even byte lengths if necessary.
- Data Types:
- ID4: 4-byte ASCII tag (e.g., 'FORM', 'LWO2').
- U1, U2, U4: Unsigned integers (1, 2, 4 bytes).
- I1, I2, I4: Signed integers (1, 2, 4 bytes).
- F4: 4-byte IEEE float.
- S0: Null-terminated string (padded to even length).
- VX: Variable-length index (U2 if < 0xFF00, else U4 with mask).
- VEC12: 3 x F4 (XYZ vector).
- COL12: 3 x F4 (RGB color).
- FP4: F4 percentage (1.0 = 100%).
- ANG4: F4 angle in radians.
- FNAM0: S0 filename in neutral format.
- Chunk Structure: ID4 (type), U4 (length), data [bytes of length], optional U1 pad if length odd.
- Subchunk Structure: ID4 (type), U2 (length), data [bytes of length], optional U1 pad if length odd.
- All Chunk Types and Descriptions (these define the core properties stored in the file):
- AAST: Image Map Antialiasing Strength.
- ADTR: Surface Additive Transparency.
- ALPH: Surface Alpha Mode.
- ANIM: Clip Animation.
- AXIS: Displacement Axis, Image Map Major Axis, Procedural Texture Axis.
- BBOX: Bounding Box.
- BLOK: Surface Block.
- BRIT: Clip Brightness.
- BUMP: Surface Bump Intensity.
- CHAN: Channel Plug-in (Envelope, Texture Layer Channel).
- CLIP: Image or Image Sequence.
- CLRA: Color Space Alpha.
- CLRF: Surface Color Filter.
- CLRH: Surface Color Highlights.
- CLRS: Color Space RGB.
- CNTR: Texture Center.
- COLR: Surface Base Color.
- CONT: Clip Contrast.
- CSYS: Texture Coordinate System.
- DESC: Description Line.
- DIFF: Surface Diffuse.
- DITH: Image Dithering.
- ENAB: Surface Block Enable.
- ENVL: Envelope.
- FALL: Texture Falloff.
- FILT: Image Filtering.
- FKEY: Gradient Key Values.
- FORM: IFF Format File (top-level).
- FUNC: Algorithm and Parameters (Procedural Texture, Surface Shader).
- GAMM: Clip Gamma Correction.
- GLOS: Surface Specular Glossiness.
- GLOW: Surface Glow Effect.
- GREN: Gradient End.
- GRPT: Gradient Repeat Mode.
- GRST: Gradient Start.
- HUE: Clip Hue.
- ICON: Thumbnail Icon Image.
- IFLT: Clip Image Filter.
- IKEY: Gradient Key Parameters.
- IMAG: Image Map Image.
- INAM: Gradient Item Name.
- ISEQ: Clip Image Sequence.
- KEY: Keyframe Time and Value.
- LAYR: Layer.
- LINE: Surface Render Outlines.
- LUMI: Surface Luminosity.
- NAME: Envelope Channel Name.
- NEGA: Clip Negative.
- OPAC: Texture Layer Opacity.
- OREF: Texture Reference Object.
- PFLT: Clip Pixel Filter.
- PIXB: Image Map Pixel Blending.
- PNAM: Gradient Parameter Name.
- PNTS: Point List.
- POLS: Polygon List.
- POST: Envelope Post-Behavior.
- PRE: Envelope Pre-Behavior.
- PROJ: Image Map Projection Mode.
- PTAG: Polygon Tag Mapping.
- RBLR: Reflection Blurring.
- REFL: Surface Reflectivity.
- RFOP: Surface Reflection Options.
- RIMG: Surface Reflection Map Image.
- RIND: Surface Refractive Index.
- ROTA: Texture Rotation.
- RSAN: Surface Reflection Map Image Seam Angle.
- SPAN: Envelope Interval Interpolation.
- SATR: Clip Saturation.
- SHRP: Surface Diffuse Sharpness.
- SIDE: Surface Polygon Sidedness.
- SIZE: Texture Size.
- SMAN: Surface Max Smoothing Angle.
- SPEC: Surface Specularity.
- STCC: Clip Color-cycling Still.
- STCK: Sticky Projection.
- STIL: Clip Still Image.
- SURF: Surface Definition.
- TAGS: Tag Strings.
- TAMP: Image Map Texture Amplitude.
- TBLR: Refraction Blurring.
- TEXT: Commentary Text.
- TIME: Clip Time.
- TIMG: Surface Refraction Map Image.
- TMAP: Texture Mapping.
- TRAN: Surface Transparency.
- TRNL: Surface Translucency.
- TROP: Surface Transparency Options.
- TYPE: Envelope Type.
- VALU: Texture Value.
- Two direct download links for files of format .LWO:
- https://www.3drender.com/challenges/theCabin/theCabin_LWO.rar (RAR archive containing a .LWO file from a lighting challenge model).
- https://www.3drender.com/challenges/bedroom/Bedroom_LWO.rar (RAR archive containing a .LWO file from a lighting challenge model).
- Ghost blog embedded HTML JavaScript for drag-and-drop .LWO file dumping properties to screen:
Drag and drop .LWO file here
- Python class for opening, decoding, reading, writing, and printing .LWO properties:
import struct
import os
class LWOParser:
def __init__(self, filename=None):
self.filename = filename
self.data = None
self.properties = []
if filename:
self.read(filename)
def read(self, filename):
with open(filename, 'rb') as f:
self.data = f.read()
self.parse()
def parse(self):
if not self.data:
return
offset = 0
form, = struct.unpack('>4s', self.data[offset:offset+4])
offset += 4
if form.decode() != 'FORM':
self.properties.append('Invalid header')
return
length, = struct.unpack('>I', self.data[offset:offset+4])
offset += 4
type_, = struct.unpack('>4s', self.data[offset:offset+4])
offset += 4
if type_.decode() != 'LWO2':
self.properties.append('Not LWO2 format')
return
self.properties.append(f'Header: FORM, Length: {length}, Type: {type_.decode()}')
while offset < len(self.data):
chunk_id, = struct.unpack('>4s', self.data[offset:offset+4])
offset += 4
chunk_len, = struct.unpack('>I', self.data[offset:offset+4])
offset += 4
chunk_id_str = chunk_id.decode()
self.properties.append(f'Chunk: {chunk_id_str}, Length: {chunk_len}')
start_offset = offset
# Simplified parsing for key chunks
if chunk_id_str == 'LAYR':
num, flags = struct.unpack('>HH', self.data[offset:offset+4])
offset += 4
pivot = struct.unpack('>fff', self.data[offset:offset+12])
offset += 12
name = self.read_s0(offset)
offset += len(name) + 1 + (len(name) + 1) % 2
parent = -1
if chunk_len > 26:
parent, = struct.unpack('>H', self.data[offset:offset+2])
offset += 2
self.properties.append(f' Layer: Number {num}, Flags {flags}, Pivot {pivot}, Name {name}, Parent {parent}')
elif chunk_id_str == 'PNTS':
num_points = chunk_len // 12
self.properties.append(f' Points: {num_points}')
for i in range(num_points):
point = struct.unpack('>fff', self.data[offset:offset+12])
self.properties.append(f' Point {i}: {point}')
offset += 12
elif chunk_id_str == 'POLS':
type_ = struct.unpack('>4s', self.data[offset:offset+4])[0].decode()
offset += 4
self.properties.append(f' Polygons Type: {type_}')
while offset < start_offset + chunk_len:
num_vert_flags, = struct.unpack('>H', self.data[offset:offset+2])
offset += 2
num_vert = num_vert_flags & 0x03FF
flags = num_vert_flags >> 10
self.properties.append(f' Polygon: Verts {num_vert}, Flags {flags}')
for j in range(num_vert):
vert = self.read_vx(offset)
self.properties.append(f' Vert {j}: {vert}')
offset += 2 if vert < 0xFF00 else 4
elif chunk_id_str == 'SURF':
name = self.read_s0(offset)
offset += len(name) + 1 + (len(name) + 1) % 2
source = self.read_s0(offset)
offset += len(source) + 1 + (len(source) + 1) % 2
self.properties.append(f' Surface: Name {name}, Source {source}')
while offset < start_offset + chunk_len:
sub_id = struct.unpack('>4s', self.data[offset:offset+4])[0].decode()
offset += 4
sub_len, = struct.unpack('>H', self.data[offset:offset+2])
offset += 2
self.properties.append(f' Subchunk: {sub_id}, Length {sub_len}')
offset += sub_len + (sub_len % 2)
else:
offset = start_offset + chunk_len
if chunk_len % 2 != 0:
offset += 1
def read_s0(self, offset):
str_ = b''
while True:
byte = self.data[offset:offset+1]
offset += 1
if byte == b'\x00':
break
str_ += byte
return str_.decode()
def read_vx(self, offset):
val, = struct.unpack('>H', self.data[offset:offset+2])
if val >= 0xFF00:
val = ((val & 0x00FF) << 16) | struct.unpack('>H', self.data[offset+2:offset+4])[0]
return val
return val
def print_properties(self):
for prop in self.properties:
print(prop)
def write(self, filename):
if not self.data:
return
with open(filename, 'wb') as f:
f.write(self.data)
# Example usage:
# parser = LWOParser('example.lwo')
# parser.print_properties()
# parser.write('output.lwo')
- Java class for opening, decoding, reading, writing, and printing .LWO properties:
import java.io.*;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
public class LWOParser {
private byte[] data;
private ByteBuffer bb;
private StringBuilder properties = new StringBuilder();
public LWOParser(String filename) throws IOException {
read(filename);
}
public void read(String filename) throws IOException {
FileInputStream fis = new FileInputStream(filename);
data = new byte[(int) new File(filename).length()];
fis.read(data);
fis.close();
bb = ByteBuffer.wrap(data).order(ByteOrder.BIG_ENDIAN);
parse();
}
private void parse() {
String form = readID4();
if (!form.equals("FORM")) {
properties.append("Invalid header\n");
return;
}
int length = bb.getInt();
String type = readID4();
if (!type.equals("LWO2")) {
properties.append("Not LWO2 format\n");
return;
}
properties.append("Header: FORM, Length: ").append(length).append(", Type: ").append(type).append("\n");
while (bb.hasRemaining()) {
String chunkID = readID4();
int chunkLen = bb.getInt();
int startPos = bb.position();
properties.append("Chunk: ").append(chunkID).append(", Length: ").append(chunkLen).append("\n");
// Simplified parsing
if (chunkID.equals("LAYR")) {
short num = bb.getShort();
short flags = bb.getShort();
float[] pivot = {bb.getFloat(), bb.getFloat(), bb.getFloat()};
String name = readS0();
short parent = (chunkLen > 26) ? bb.getShort() : -1;
properties.append(" Layer: Number ").append(num).append(", Flags ").append(flags)
.append(", Pivot ").append(java.util.Arrays.toString(pivot))
.append(", Name ").append(name).append(", Parent ").append(parent).append("\n");
} else if (chunkID.equals("PNTS")) {
int numPoints = chunkLen / 12;
properties.append(" Points: ").append(numPoints).append("\n");
for (int i = 0; i < numPoints; i++) {
float[] point = {bb.getFloat(), bb.getFloat(), bb.getFloat()};
properties.append(" Point ").append(i).append(": ").append(java.util.Arrays.toString(point)).append("\n");
}
} else if (chunkID.equals("POLS")) {
String polyType = readID4();
properties.append(" Polygons Type: ").append(polyType).append("\n");
while (bb.position() < startPos + chunkLen) {
short numVertFlags = bb.getShort();
int numVert = numVertFlags & 0x03FF;
int flags = numVertFlags >> 10;
properties.append(" Polygon: Verts ").append(numVert).append(", Flags ").append(flags).append("\n");
for (int j = 0; j < numVert; j++) {
int vert = readVX();
properties.append(" Vert ").append(j).append(": ").append(vert).append("\n");
}
}
} else if (chunkID.equals("SURF")) {
String name = readS0();
String source = readS0();
properties.append(" Surface: Name ").append(name).append(", Source ").append(source).append("\n");
while (bb.position() < startPos + chunkLen) {
String subID = readID4();
short subLen = bb.getShort();
properties.append(" Subchunk: ").append(subID).append(", Length ").append(subLen).append("\n");
bb.position(bb.position() + subLen + (subLen % 2 == 1 ? 1 : 0));
}
} else {
bb.position(startPos + chunkLen);
}
if (chunkLen % 2 == 1) bb.get(); // pad
}
}
private String readID4() {
byte[] bytes = new byte[4];
bb.get(bytes);
return new String(bytes);
}
private String readS0() {
StringBuilder sb = new StringBuilder();
byte b;
while ((b = bb.get()) != 0) {
sb.append((char) b);
}
if (bb.position() % 2 != 0) bb.get(); // pad
return sb.toString();
}
private int readVX() {
int val = bb.getShort() & 0xFFFF;
if (val >= 0xFF00) {
val = ((val & 0x00FF) << 16) | (bb.getShort() & 0xFFFF);
}
return val;
}
public void printProperties() {
System.out.println(properties.toString());
}
public void write(String filename) throws IOException {
if (data == null) return;
FileOutputStream fos = new FileOutputStream(filename);
fos.write(data);
fos.close();
}
// Example usage:
// public static void main(String[] args) throws IOException {
// LWOParser parser = new LWOParser("example.lwo");
// parser.printProperties();
// parser.write("output.lwo");
// }
}
- JavaScript class for opening, decoding, reading, writing, and printing .LWO properties (note: JS file I/O requires Node.js; this assumes Node for read/write):
const fs = require('fs');
class LWOParser {
constructor(filename = null) {
this.data = null;
this.dv = null;
this.properties = [];
if (filename) {
this.read(filename);
}
}
read(filename) {
this.data = fs.readFileSync(filename);
this.dv = new DataView(this.data.buffer);
this.offset = 0;
this.parse();
}
parse() {
const form = this.readID4();
if (form !== 'FORM') {
this.properties.push('Invalid header');
return;
}
const length = this.dv.getUint32(this.offset);
this.offset += 4;
const type = this.readID4();
if (type !== 'LWO2') {
this.properties.push('Not LWO2 format');
return;
}
this.properties.push(`Header: FORM, Length: ${length}, Type: ${type}`);
while (this.offset < this.data.length) {
const chunkID = this.readID4();
const chunkLen = this.dv.getUint32(this.offset);
this.offset += 4;
this.properties.push(`Chunk: ${chunkID}, Length: ${chunkLen}`);
const startOffset = this.offset;
// Simplified parsing
if (chunkID === 'LAYR') {
const num = this.dv.getUint16(this.offset);
this.offset += 2;
const flags = this.dv.getUint16(this.offset);
this.offset += 2;
const pivot = [this.readF4(), this.readF4(), this.readF4()];
const name = this.readS0();
const parent = (chunkLen > 26) ? this.dv.getUint16(this.offset) : -1;
if (chunkLen > 26) this.offset += 2;
this.properties.push(` Layer: Number ${num}, Flags ${flags}, Pivot ${pivot}, Name ${name}, Parent ${parent}`);
} else if (chunkID === 'PNTS') {
const numPoints = chunkLen / 12;
this.properties.push(` Points: ${numPoints}`);
for (let i = 0; i < numPoints; i++) {
const point = [this.readF4(), this.readF4(), this.readF4()];
this.properties.push(` Point ${i}: ${point}`);
}
} else if (chunkID === 'POLS') {
const polyType = this.readID4();
this.properties.push(` Polygons Type: ${polyType}`);
while (this.offset < startOffset + chunkLen) {
const numVertFlags = this.dv.getUint16(this.offset);
this.offset += 2;
const numVert = numVertFlags & 0x03FF;
const flags = numVertFlags >> 10;
this.properties.push(` Polygon: Verts ${numVert}, Flags ${flags}`);
for (let j = 0; j < numVert; j++) {
const vert = this.readVX();
this.properties.push(` Vert ${j}: ${vert}`);
}
}
} else if (chunkID === 'SURF') {
const name = this.readS0();
const source = this.readS0();
this.properties.push(` Surface: Name ${name}, Source ${source}`);
while (this.offset < startOffset + chunkLen) {
const subID = this.readID4();
const subLen = this.dv.getUint16(this.offset);
this.offset += 2;
this.properties.push(` Subchunk: ${subID}, Length ${subLen}`);
this.offset += subLen + (subLen % 2 ? 1 : 0);
}
} else {
this.offset = startOffset + chunkLen;
}
if (chunkLen % 2 !== 0) this.offset++;
}
}
readID4() {
const str = String.fromCharCode(this.dv.getUint8(this.offset++), this.dv.getUint8(this.offset++), this.dv.getUint8(this.offset++), this.dv.getUint8(this.offset++));
return str;
}
readF4() {
const val = this.dv.getFloat32(this.offset);
this.offset += 4;
return val;
}
readS0() {
let str = '';
while (true) {
const byte = this.dv.getUint8(this.offset++);
if (byte === 0) break;
str += String.fromCharCode(byte);
}
if (this.offset % 2 !== 0) this.offset++;
return str;
}
readVX() {
let val = this.dv.getUint16(this.offset);
this.offset += 2;
if (val >= 0xFF00) {
val = ((val & 0x00FF) << 16) | this.dv.getUint16(this.offset);
this.offset += 2;
}
return val;
}
printProperties() {
console.log(this.properties.join('\n'));
}
write(filename) {
if (!this.data) return;
fs.writeFileSync(filename, this.data);
}
}
// Example usage:
// const parser = new LWOParser('example.lwo');
// parser.printProperties();
// parser.write('output.lwo');
- C class (using C++ for class support) for opening, decoding, reading, writing, and printing .LWO properties:
#include <iostream>
#include <fstream>
#include <vector>
#include <string>
#include <cstring>
#include <iomanip>
class LWOParser {
private:
std::vector<char> data;
size_t offset = 0;
std::vector<std::string> properties;
std::string readID4() {
char buf[5] = {0};
memcpy(buf, &data[offset], 4);
offset += 4;
return std::string(buf);
}
uint32_t readU4() {
uint32_t val;
memcpy(&val, &data[offset], 4);
offset += 4;
return __builtin_bswap32(val); // big-endian
}
uint16_t readU2() {
uint16_t val;
memcpy(&val, &data[offset], 2);
offset += 2;
return __builtin_bswap16(val);
}
float readF4() {
uint32_t val = readU4();
float f;
memcpy(&f, &val, 4);
return f;
}
std::string readS0() {
std::string str;
char b;
while ((b = data[offset++]) != 0) {
str += b;
}
if (offset % 2 != 0) offset++;
return str;
}
uint32_t readVX() {
uint16_t val = readU2();
if (val >= 0xFF00) {
uint16_t low = readU2();
val = ((val & 0x00FF) << 16) | low;
}
return val;
}
public:
LWOParser(const std::string& filename = "") {
if (!filename.empty()) {
read(filename);
}
}
void read(const std::string& filename) {
std::ifstream file(filename, std::ios::binary | std::ios::ate);
std::streamsize size = file.tellg();
file.seekg(0, std::ios::beg);
data.resize(size);
file.read(data.data(), size);
parse();
}
void parse() {
std::string form = readID4();
if (form != "FORM") {
properties.push_back("Invalid header");
return;
}
uint32_t length = readU4();
std::string type = readID4();
if (type != "LWO2") {
properties.push_back("Not LWO2 format");
return;
}
properties.push_back("Header: FORM, Length: " + std::to_string(length) + ", Type: " + type);
while (offset < data.size()) {
std::string chunkID = readID4();
uint32_t chunkLen = readU4();
properties.push_back("Chunk: " + chunkID + ", Length: " + std::to_string(chunkLen));
size_t startOffset = offset;
// Simplified parsing
if (chunkID == "LAYR") {
uint16_t num = readU2();
uint16_t flags = readU2();
float pivot[3] = {readF4(), readF4(), readF4()};
std::string name = readS0();
int16_t parent = (chunkLen > 26) ? readU2() : -1;
std::string prop = " Layer: Number " + std::to_string(num) + ", Flags " + std::to_string(flags);
prop += ", Pivot [" + std::to_string(pivot[0]) + ", " + std::to_string(pivot[1]) + ", " + std::to_string(pivot[2]) + "]";
prop += ", Name " + name + ", Parent " + std::to_string(parent);
properties.push_back(prop);
} else if (chunkID == "PNTS") {
uint32_t numPoints = chunkLen / 12;
properties.push_back(" Points: " + std::to_string(numPoints));
for (uint32_t i = 0; i < numPoints; i++) {
float point[3] = {readF4(), readF4(), readF4()};
std::string prop = " Point " + std::to_string(i) + ": [" + std::to_string(point[0]) + ", " + std::to_string(point[1]) + ", " + std::to_string(point[2]) + "]";
properties.push_back(prop);
}
} else if (chunkID == "POLS") {
std::string polyType = readID4();
properties.push_back(" Polygons Type: " + polyType);
while (offset < startOffset + chunkLen) {
uint16_t numVertFlags = readU2();
uint16_t numVert = numVertFlags & 0x03FF;
uint16_t flags = numVertFlags >> 10;
properties.push_back(" Polygon: Verts " + std::to_string(numVert) + ", Flags " + std::to_string(flags));
for (uint16_t j = 0; j < numVert; j++) {
uint32_t vert = readVX();
properties.push_back(" Vert " + std::to_string(j) + ": " + std::to_string(vert));
}
}
} else if (chunkID == "SURF") {
std::string name = readS0();
std::string source = readS0();
properties.push_back(" Surface: Name " + name + ", Source " + source);
while (offset < startOffset + chunkLen) {
std::string subID = readID4();
uint16_t subLen = readU2();
properties.push_back(" Subchunk: " + subID + ", Length " + std::to_string(subLen));
offset += subLen;
if (subLen % 2 != 0) offset++;
}
} else {
offset = startOffset + chunkLen;
}
if (chunkLen % 2 != 0) offset++;
}
}
void printProperties() {
for (const auto& prop : properties) {
std::cout << prop << std::endl;
}
}
void write(const std::string& filename) {
if (data.empty()) return;
std::ofstream file(filename, std::ios::binary);
file.write(data.data(), data.size());
}
};
// Example usage:
// int main() {
// LWOParser parser("example.lwo");
// parser.printProperties();
// parser.write("output.lwo");
// return 0;
// }