Task 001: .3DS File Format
Task 001: .3DS File Format
The .3DS file format is a binary, chunk-based structure used for 3D models, originally developed for Autodesk 3D Studio. The specifications are documented in sources such as Paul Bourke's data formats reference, Wikipedia, and technical PDFs detailing the format's hierarchy.
The properties intrinsic to the .3DS file format include the following structural elements, derived from its chunk-based architecture (all multi-byte values are in little-endian order):
- File signature: The file starts with chunk ID 0x4D4D (MAIN3DS), which encompasses the entire file content.
- Byte order: Little-endian for all integers and floats.
- Chunk structure: Each chunk consists of a 2-byte unsigned integer ID, a 4-byte unsigned integer length (including the header), followed by data or subchunks. Chunks are nested hierarchically.
- Version: Chunk ID 0x0002, containing a 4-byte unsigned integer indicating the format version (typically 3 or higher for modern files).
- Editor configuration: Chunk ID 0x3D3D (EDIT3DS), containing scene data such as objects, materials, and viewports.
- Keyframer data: Chunk ID 0xB000 (KEYF3DS), containing animation information, including frame ranges (subchunk 0xB008 with 4-byte start and 4-byte end frames), object hierarchies (subchunk 0xB030), and tracks for position (0xB020), rotation (0xB021), and scale (0xB022).
- Material block: Chunk ID 0xAFFF, with subchunks for name (0xA000, null-terminated ASCII string), ambient color (0xA010, with color subchunks), diffuse color (0xA020), specular color (0xA030), texture map (0xA200, with filename subchunk 0xA300), bump map (0xA230), reflection map (0xA220), and mapping parameters (0xA351).
- Color properties: Subchunks 0x0010 (RGB float: three 4-byte floats for R, G, B) or 0x0011 (24-bit color: three 1-byte values for R, G, B).
- Object block: Chunk ID 0x4000, starting with a null-terminated ASCII object name, followed by type-specific subchunks.
- Triangular mesh object: Subchunk ID 0x4100, with vertices list (0x4110: 2-byte vertex count, followed by 3 4-byte floats per vertex for X, Y, Z), faces description (0x4120: 2-byte face count, followed by three 2-byte vertex indices and 2-byte flags per face), face material assignment (0x4130: null-terminated material name, 2-byte face count, followed by 2-byte face indices), UV mapping coordinates (0x4140: 2-byte vertex count, followed by two 4-byte floats per UV for U, V), smoothing groups (0x4150: 4-byte integer per face indicating group membership via bits), local transformation matrix (0x4160: twelve 4-byte floats for the 3x4 matrix rows, defining axes and origin), and visibility flag (0x4165: 4-byte integer).
- Light object: Subchunk ID 0x4600, with three 4-byte floats for position (X, Y, Z), color subchunks, spotlight parameters (0x4610: three 4-byte floats for target, two 4-byte floats for hotspot and falloff), and on/off state (0x4620: boolean).
- Camera object: Subchunk ID 0x4700, with three 4-byte floats for position, three 4-byte floats for target, 4-byte float for bank angle, and 4-byte float for lens focal length.
- Viewport configuration: Subchunks under 0x3D3D, including layout (0x7001, 0x7011, 0x7012, 0x7020 with viewport types like top=0x0001, camera=0xFFFF), background color (0x1200), and ambient light (0x2100).
- Additional keyframer properties: Object description (0xB002), pivot point (0xB013: three 4-byte floats), dummy name (0xB011), and unknown chunks (e.g., 0xB009, 0xB00A).
- Limitations as properties: Maximum 65,536 vertices/faces per mesh, triangular faces only, 8.3 DOS filename format for textures, 10-character limit for object names, 16-character limit for material names, no directional lights.
Two direct download links for .3DS files are:
- https://people.sc.fsu.edu/~jburkardt/data/3ds/cow.3ds
- https://people.sc.fsu.edu/~jburkardt/data/3ds/teapot.3ds
The following is an embeddable HTML with JavaScript for a Ghost blog (or similar platform), enabling drag-and-drop of a .3DS file to parse and display all properties on screen:
Drag and drop a .3DS file here
- The following is a Python class for handling .3DS files:
import struct
import os
class ThreeDSFile:
def __init__(self, filename=None):
self.chunks = None
if filename:
self.load(filename)
def load(self, filename):
with open(filename, 'rb') as f:
data = f.read()
self.chunks = self._parse_chunk(memoryview(data), 0)[0]
def _parse_chunk(self, view, pos):
if pos >= len(view):
return None, pos
id = struct.unpack_from('<H', view, pos)[0]
pos += 2
len_ = struct.unpack_from('<I', view, pos)[0]
pos += 4
end = pos + len_ - 6
chunk = {'id': hex(id), 'subchunks': [], 'data': {}}
if id == 0x4d4d:
chunk['data']['type'] = 'MAIN3DS'
elif id == 0x0002:
chunk['data']['version'] = struct.unpack_from('<I', view, pos)[0]
pos += 4
elif id == 0x3d3d:
chunk['data']['type'] = 'EDIT3DS'
elif id == 0xb000:
chunk['data']['type'] = 'KEYF3DS'
elif id == 0xafff:
chunk['data']['type'] = 'MATERIAL'
elif id == 0xa000:
str_ = b''
while True:
byte = struct.unpack_from('<B', view, pos)[0]
pos += 1
if byte == 0:
break
str_ += bytes([byte])
chunk['data']['name'] = str_.decode('ascii')
elif id in [0xa010, 0xa020, 0xa030]:
chunk['data']['colorType'] = 'Ambient' if id == 0xa010 else 'Diffuse' if id == 0xa020 else 'Specular'
elif id == 0x0010:
chunk['data']['rgbFloat'] = struct.unpack_from('<fff', view, pos)
pos += 12
elif id == 0x0011:
chunk['data']['rgbByte'] = struct.unpack_from('<BBB', view, pos)
pos += 3
elif id == 0x4000:
str_ = b''
while True:
byte = struct.unpack_from('<B', view, pos)[0]
pos += 1
if byte == 0:
break
str_ += bytes([byte])
chunk['data']['objectName'] = str_.decode('ascii')
elif id == 0x4100:
chunk['data']['type'] = 'TRIMESH'
elif id == 0x4110:
count = struct.unpack_from('<H', view, pos)[0]
pos += 2
chunk['data']['vertexCount'] = count
chunk['data']['vertices'] = []
for _ in range(count):
vertex = struct.unpack_from('<fff', view, pos)
chunk['data']['vertices'].append(vertex)
pos += 12
elif id == 0x4120:
count = struct.unpack_from('<H', view, pos)[0]
pos += 2
chunk['data']['faceCount'] = count
chunk['data']['faces'] = []
for _ in range(count):
face = struct.unpack_from('<HHHH', view, pos)
chunk['data']['faces'].append(face)
pos += 8
elif id == 0x4130:
str_ = b''
while True:
byte = struct.unpack_from('<B', view, pos)[0]
pos += 1
if byte == 0:
break
str_ += bytes([byte])
chunk['data']['materialName'] = str_.decode('ascii')
count = struct.unpack_from('<H', view, pos)[0]
pos += 2
chunk['data']['faceCount'] = count
chunk['data']['faceIndices'] = []
for _ in range(count):
idx = struct.unpack_from('<H', view, pos)[0]
chunk['data']['faceIndices'].append(idx)
pos += 2
elif id == 0x4140:
count = struct.unpack_from('<H', view, pos)[0]
pos += 2
chunk['data']['uvCount'] = count
chunk['data']['uvs'] = []
for _ in range(count):
uv = struct.unpack_from('<ff', view, pos)
chunk['data']['uvs'].append(uv)
pos += 8
elif id == 0x4150:
chunk['data']['smoothingGroups'] = []
num = (len_ - 6) // 4
for _ in range(num):
group = struct.unpack_from('<I', view, pos)[0]
chunk['data']['smoothingGroups'].append(group)
pos += 4
elif id == 0x4160:
chunk['data']['matrix'] = []
for _ in range(12):
val = struct.unpack_from('<f', view, pos)[0]
chunk['data']['matrix'].append(val)
pos += 4
elif id == 0x4600:
position = struct.unpack_from('<fff', view, pos)
chunk['data']['position'] = position
pos += 12
elif id == 0x4700:
position = struct.unpack_from('<fff', view, pos)
target = struct.unpack_from('<fff', view, pos + 12)
bank = struct.unpack_from('<f', view, pos + 24)[0]
lens = struct.unpack_from('<f', view, pos + 28)[0]
chunk['data']['position'] = position
chunk['data']['target'] = target
chunk['data']['bank'] = bank
chunk['data']['lens'] = lens
pos += 32
else:
chunk['data']['raw'] = view[pos:end].tobytes()
pos = end
while pos < end:
sub, pos = self._parse_chunk(view, pos)
if sub:
chunk['subchunks'].append(sub)
return chunk, pos
def print_properties(self):
def recurse(chunk, indent=''):
print(f"{indent}Chunk ID: {chunk['id']}")
for key, value in chunk['data'].items():
print(f"{indent}{key}: {value}")
for sub in chunk['subchunks']:
recurse(sub, indent + ' ')
if self.chunks:
recurse(self.chunks)
def save(self, filename):
def calc_len(chunk):
data_len = self._data_len(chunk['data'], chunk['id'])
sub_len = sum(calc_len(sub) for sub in chunk['subchunks'])
return 6 + data_len + sub_len
def write_chunk(f, chunk):
f.write(struct.pack('<H', int(chunk['id'], 16)))
total_len = calc_len(chunk)
f.write(struct.pack('<I', total_len))
self._write_data(f, chunk['data'], chunk['id'])
for sub in chunk['subchunks']:
write_chunk(f, sub)
with open(filename, 'wb') as f:
write_chunk(f, self.chunks)
def _data_len(self, data, id):
if id == 0x0002:
return 4
elif id == 0xa000 or id == 0x4000 or id == 0x4130:
return len(data.get('name', '')) + 1 + (2 if 'faceCount' in data else 0) + len(data.get('faceIndices', [])) * 2 if 'materialName' in data else len(data.get('objectName', '')) + 1
elif id == 0x0010:
return 12
elif id == 0x0011:
return 3
elif id == 0x4110:
return 2 + len(data.get('vertices', [])) * 12
elif id == 0x4120:
return 2 + len(data.get('faces', [])) * 8
elif id == 0x4140:
return 2 + len(data.get('uvs', [])) * 8
elif id == 0x4150:
return len(data.get('smoothingGroups', [])) * 4
elif id == 0x4160:
return 48
elif id == 0x4600:
return 12
elif id == 0x4700:
return 32
elif 'raw' in data:
return len(data['raw'])
return 0
def _write_data(self, f, data, id):
if id == 0x0002:
f.write(struct.pack('<I', data['version']))
elif id == 0xa000:
f.write(data['name'].encode('ascii') + b'\0')
elif id == 0x4000:
f.write(data['objectName'].encode('ascii') + b'\0')
elif id == 0x0010:
f.write(struct.pack('<fff', *data['rgbFloat']))
elif id == 0x0011:
f.write(struct.pack('<BBB', *data['rgbByte']))
elif id == 0x4110:
f.write(struct.pack('<H', data['vertexCount']))
for v in data['vertices']:
f.write(struct.pack('<fff', *v))
elif id == 0x4120:
f.write(struct.pack('<H', data['faceCount']))
for face in data['faces']:
f.write(struct.pack('<HHHH', *face))
elif id == 0x4130:
f.write(data['materialName'].encode('ascii') + b'\0')
f.write(struct.pack('<H', data['faceCount']))
for idx in data['faceIndices']:
f.write(struct.pack('<H', idx))
elif id == 0x4140:
f.write(struct.pack('<H', data['uvCount']))
for uv in data['uvs']:
f.write(struct.pack('<ff', *uv))
elif id == 0x4150:
for group in data['smoothingGroups']:
f.write(struct.pack('<I', group))
elif id == 0x4160:
for val in data['matrix']:
f.write(struct.pack('<f', val))
elif id == 0x4600:
f.write(struct.pack('<fff', *data['position']))
elif id == 0x4700:
f.write(struct.pack('<fff', *data['position']))
f.write(struct.pack('<fff', *data['target']))
f.write(struct.pack('<f', data['bank']))
f.write(struct.pack('<f', data['lens']))
elif 'raw' in data:
f.write(data['raw'])
- The following is a Java class for handling .3DS files:
import java.io.*;
import java.nio.*;
import java.nio.channels.FileChannel;
import java.util.*;
public class ThreeDSFile {
private Map<String, Object> chunks;
public ThreeDSFile(String filename) throws IOException {
if (filename != null) {
load(filename);
}
}
public void load(String filename) throws IOException {
RandomAccessFile raf = new RandomAccessFile(filename, "r");
FileChannel channel = raf.getChannel();
ByteBuffer buffer = channel.map(FileChannel.MapMode.READ_ONLY, 0, channel.size());
buffer.order(ByteOrder.LITTLE_ENDIAN);
chunks = parseChunk(buffer);
raf.close();
}
private Map<String, Object> parseChunk(ByteBuffer buffer) {
int pos = buffer.position();
if (pos >= buffer.limit()) return null;
short id = buffer.getShort();
int len = buffer.getInt();
int end = pos + len;
Map<String, Object> chunk = new HashMap<>();
chunk.put("id", String.format("0x%04X", id));
List<Map<String, Object>> subchunks = new ArrayList<>();
chunk.put("subchunks", subchunks);
Map<String, Object> data = new HashMap<>();
chunk.put("data", data);
switch (id) {
case 0x4D4D: data.put("type", "MAIN3DS"); break;
case 0x0002: data.put("version", buffer.getInt()); break;
case 0x3D3D: data.put("type", "EDIT3DS"); break;
case 0xB000: data.put("type", "KEYF3DS"); break;
case 0xAFFF: data.put("type", "MATERIAL"); break;
case 0xA000: data.put("name", readString(buffer)); break;
case 0xA010: data.put("colorType", "Ambient"); break;
case 0xA020: data.put("colorType", "Diffuse"); break;
case 0xA030: data.put("colorType", "Specular"); break;
case 0x0010: float[] rgbF = {buffer.getFloat(), buffer.getFloat(), buffer.getFloat()}; data.put("rgbFloat", rgbF); break;
case 0x0011: byte[] rgbB = new byte[3]; buffer.get(rgbB); data.put("rgbByte", rgbB); break;
case 0x4000: data.put("objectName", readString(buffer)); break;
case 0x4100: data.put("type", "TRIMESH"); break;
case 0x4110:
short countV = buffer.getShort();
data.put("vertexCount", (int) countV);
List<float[]> vertices = new ArrayList<>();
for (int i = 0; i < countV; i++) {
vertices.add(new float[]{buffer.getFloat(), buffer.getFloat(), buffer.getFloat()});
}
data.put("vertices", vertices);
break;
case 0x4120:
short countF = buffer.getShort();
data.put("faceCount", (int) countF);
List<short[]> faces = new ArrayList<>();
for (int i = 0; i < countF; i++) {
faces.add(new short[]{buffer.getShort(), buffer.getShort(), buffer.getShort(), buffer.getShort()});
}
data.put("faces", faces);
break;
case 0x4130:
data.put("materialName", readString(buffer));
short countFI = buffer.getShort();
data.put("faceCount", (int) countFI);
List<Short> indices = new ArrayList<>();
for (int i = 0; i < countFI; i++) {
indices.add(buffer.getShort());
}
data.put("faceIndices", indices);
break;
case 0x4140:
short countU = buffer.getShort();
data.put("uvCount", (int) countU);
List<float[]> uvs = new ArrayList<>();
for (int i = 0; i < countU; i++) {
uvs.add(new float[]{buffer.getFloat(), buffer.getFloat()});
}
data.put("uvs", uvs);
break;
case 0x4150:
List<Integer> groups = new ArrayList<>();
int numG = (len - 6) / 4;
for (int i = 0; i < numG; i++) {
groups.add(buffer.getInt());
}
data.put("smoothingGroups", groups);
break;
case 0x4160:
float[] matrix = new float[12];
for (int i = 0; i < 12; i++) {
matrix[i] = buffer.getFloat();
}
data.put("matrix", matrix);
break;
case 0x4600:
float[] posL = {buffer.getFloat(), buffer.getFloat(), buffer.getFloat()};
data.put("position", posL);
break;
case 0x4700:
float[] posC = {buffer.getFloat(), buffer.getFloat(), buffer.getFloat()};
float[] tar = {buffer.getFloat(), buffer.getFloat(), buffer.getFloat()};
float bank = buffer.getFloat();
float lens = buffer.getFloat();
data.put("position", posC);
data.put("target", tar);
data.put("bank", bank);
data.put("lens", lens);
break;
default:
byte[] raw = new byte[end - buffer.position()];
buffer.get(raw);
data.put("raw", raw);
break;
}
while (buffer.position() < end) {
Map<String, Object> sub = parseChunk(buffer);
if (sub != null) subchunks.add(sub);
}
return chunk;
}
private String readString(ByteBuffer buffer) {
StringBuilder sb = new StringBuilder();
byte b;
while ((b = buffer.get()) != 0) {
sb.append((char) b);
}
return sb.toString();
}
public void printProperties() {
printRecurse(chunks, "");
}
private void printRecurse(Map<String, Object> chunk, String indent) {
System.out.println(indent + "Chunk ID: " + chunk.get("id"));
Map<String, Object> data = (Map<String, Object>) chunk.get("data");
for (Map.Entry<String, Object> entry : data.entrySet()) {
System.out.println(indent + entry.getKey() + ": " + entry.getValue());
}
List<Map<String, Object>> subs = (List<Map<String, Object>>) chunk.get("subchunks");
for (Map<String, Object> sub : subs) {
printRecurse(sub, indent + " ");
}
}
public void save(String filename) throws IOException {
FileOutputStream fos = new FileOutputStream(filename);
writeChunk(fos, chunks);
fos.close();
}
private void writeChunk(FileOutputStream fos, Map<String, Object> chunk) throws IOException {
short id = Short.parseShort(((String) chunk.get("id")).substring(2), 16);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
writeData(baos, (Map<String, Object>) chunk.get("data"), id);
List<Map<String, Object>> subs = (List<Map<String, Object>>) chunk.get("subchunks");
for (Map<String, Object> sub : subs) {
ByteArrayOutputStream subBaos = new ByteArrayOutputStream();
writeChunk(new FileOutputStream(subBaos.toString()), sub); // Recursive, but use baos for subs
baos.write(subBaos.toByteArray());
}
byte[] dataBytes = baos.toByteArray();
int totalLen = 6 + dataBytes.length;
ByteBuffer header = ByteBuffer.allocate(6).order(ByteOrder.LITTLE_ENDIAN);
header.putShort(id);
header.putInt(totalLen);
fos.write(header.array());
fos.write(dataBytes);
}
private void writeData(ByteArrayOutputStream baos, Map<String, Object> data, short id) throws IOException {
ByteBuffer bb = ByteBuffer.allocate(1024).order(ByteOrder.LITTLE_ENDIAN); // Adjust size as needed
if (id == 0x0002) {
bb.putInt((Integer) data.get("version"));
} else if (id == 0xA000) {
bb.put((String) data.get("name").getBytes("ascii"));
bb.put((byte) 0);
} else if (id == 0x4000) {
bb.put((String) data.get("objectName").getBytes("ascii"));
bb.put((byte) 0);
} else if (id == 0x0010) {
float[] rgb = (float[]) data.get("rgbFloat");
bb.putFloat(rgb[0]);
bb.putFloat(rgb[1]);
bb.putFloat(rgb[2]);
} else if (id == 0x0011) {
byte[] rgb = (byte[]) data.get("rgbByte");
bb.put(rgb);
} else if (id == 0x4110) {
bb.putShort(((Integer) data.get("vertexCount")).shortValue());
List<float[]> verts = (List<float[]>) data.get("vertices");
for (float[] v : verts) {
bb.putFloat(v[0]);
bb.putFloat(v[1]);
bb.putFloat(v[2]);
}
} else if (id == 0x4120) {
bb.putShort(((Integer) data.get("faceCount")).shortValue());
List<short[]> faces = (List<short[]>) data.get("faces");
for (short[] f : faces) {
bb.putShort(f[0]);
bb.putShort(f[1]);
bb.putShort(f[2]);
bb.putShort(f[3]);
}
} else if (id == 0x4130) {
bb.put((String) data.get("materialName").getBytes("ascii"));
bb.put((byte) 0);
bb.putShort(((Integer) data.get("faceCount")).shortValue());
List<Short> indices = (List<Short>) data.get("faceIndices");
for (short idx : indices) {
bb.putShort(idx);
}
} else if (id == 0x4140) {
bb.putShort(((Integer) data.get("uvCount")).shortValue());
List<float[]> uvs = (List<float[]>) data.get("uvs");
for (float[] uv : uvs) {
bb.putFloat(uv[0]);
bb.putFloat(uv[1]);
}
} else if (id == 0x4150) {
List<Integer> groups = (List<Integer>) data.get("smoothingGroups");
for (int group : groups) {
bb.putInt(group);
}
} else if (id == 0x4160) {
float[] matrix = (float[]) data.get("matrix");
for (float val : matrix) {
bb.putFloat(val);
}
} else if (id == 0x4600) {
float[] pos = (float[]) data.get("position");
bb.putFloat(pos[0]);
bb.putFloat(pos[1]);
bb.putFloat(pos[2]);
} else if (id == 0x4700) {
float[] pos = (float[]) data.get("position");
float[] tar = (float[]) data.get("target");
bb.putFloat(pos[0]);
bb.putFloat(pos[1]);
bb.putFloat(pos[2]);
bb.putFloat(tar[0]);
bb.putFloat(tar[1]);
bb.putFloat(tar[2]);
bb.putFloat((Float) data.get("bank"));
bb.putFloat((Float) data.get("lens"));
} else if (data.containsKey("raw")) {
bb.put((byte[]) data.get("raw"));
}
baos.write(bb.array(), 0, bb.position());
}
}
- The following is a JavaScript class for handling .3DS files (suitable for Node.js with 'fs' module):
const fs = require('fs');
class ThreeDSFile {
constructor(filename) {
if (filename) {
this.load(filename);
}
}
load(filename) {
const buffer = fs.readFileSync(filename);
const view = new DataView(buffer.buffer);
this.chunks = this.parseChunk(view, 0);
}
parseChunk(view, pos) {
if (pos >= view.byteLength) return null;
const id = view.getUint16(pos, true); pos += 2;
const len = view.getUint32(pos, true); pos += 4;
const end = pos + len - 6;
const chunk = { id: `0x${id.toString(16).toUpperCase()}`, subchunks: [], data: {} };
switch (id) {
case 0x4D4D: chunk.data.type = 'MAIN3DS'; break;
case 0x0002: chunk.data.version = view.getUint32(pos, true); pos += 4; break;
case 0x3D3D: chunk.data.type = 'EDIT3DS'; break;
case 0xB000: chunk.data.type = 'KEYF3DS'; break;
case 0xAFFF: chunk.data.type = 'MATERIAL'; break;
case 0xA000: chunk.data.name = this.readString(view, pos); pos += chunk.data.name.length + 1; break;
case 0xA010: chunk.data.colorType = 'Ambient'; break;
case 0xA020: chunk.data.colorType = 'Diffuse'; break;
case 0xA030: chunk.data.colorType = 'Specular'; break;
case 0x0010: chunk.data.rgbFloat = [view.getFloat32(pos, true), view.getFloat32(pos+4, true), view.getFloat32(pos+8, true)]; pos += 12; break;
case 0x0011: chunk.data.rgbByte = [view.getUint8(pos), view.getUint8(pos+1), view.getUint8(pos+2)]; pos += 3; break;
case 0x4000: chunk.data.objectName = this.readString(view, pos); pos += chunk.data.objectName.length + 1; break;
case 0x4100: chunk.data.type = 'TRIMESH'; break;
case 0x4110:
chunk.data.vertexCount = view.getUint16(pos, true); pos += 2;
chunk.data.vertices = [];
for (let i = 0; i < chunk.data.vertexCount; i++) {
chunk.data.vertices.push([view.getFloat32(pos, true), view.getFloat32(pos+4, true), view.getFloat32(pos+8, true)]);
pos += 12;
}
break;
case 0x4120:
chunk.data.faceCount = view.getUint16(pos, true); pos += 2;
chunk.data.faces = [];
for (let i = 0; i < chunk.data.faceCount; i++) {
chunk.data.faces.push([view.getUint16(pos, true), view.getUint16(pos+2, true), view.getUint16(pos+4, true), view.getUint16(pos+6, true)]);
pos += 8;
}
break;
case 0x4130:
chunk.data.materialName = this.readString(view, pos); pos += chunk.data.materialName.length + 1;
chunk.data.faceCount = view.getUint16(pos, true); pos += 2;
chunk.data.faceIndices = [];
for (let i = 0; i < chunk.data.faceCount; i++) {
chunk.data.faceIndices.push(view.getUint16(pos, true)); pos += 2;
}
break;
case 0x4140:
chunk.data.uvCount = view.getUint16(pos, true); pos += 2;
chunk.data.uvs = [];
for (let i = 0; i < chunk.data.uvCount; i++) {
chunk.data.uvs.push([view.getFloat32(pos, true), view.getFloat32(pos+4, true)]);
pos += 8;
}
break;
case 0x4150:
chunk.data.smoothingGroups = [];
for (let i = 0; i < (len - 6) / 4; i++) {
chunk.data.smoothingGroups.push(view.getUint32(pos, true)); pos += 4;
}
break;
case 0x4160:
chunk.data.matrix = [];
for (let i = 0; i < 12; i++) {
chunk.data.matrix.push(view.getFloat32(pos, true)); pos += 4;
}
break;
case 0x4600:
chunk.data.position = [view.getFloat32(pos, true), view.getFloat32(pos+4, true), view.getFloat32(pos+8, true)]; pos += 12;
break;
case 0x4700:
chunk.data.position = [view.getFloat32(pos, true), view.getFloat32(pos+4, true), view.getFloat32(pos+8, true)];
chunk.data.target = [view.getFloat32(pos+12, true), view.getFloat32(pos+16, true), view.getFloat32(pos+20, true)];
chunk.data.bank = view.getFloat32(pos+24, true);
chunk.data.lens = view.getFloat32(pos+28, true);
pos += 32;
break;
default: chunk.data.raw = new Uint8Array(view.buffer.slice(pos, end)); pos = end; break;
}
while (pos < end) {
const sub = this.parseChunk(view, pos);
if (sub) chunk.subchunks.push(sub);
pos = view.byteOffset + view.byteLength; // Update pos from sub parse
}
return chunk;
}
readString(view, pos) {
let str = '';
let byte;
let start = pos;
while ((byte = view.getUint8(pos++)) !== 0) str += String.fromCharCode(byte);
return str;
}
printProperties() {
const recurse = (chunk, indent = '') => {
console.log(`${indent}Chunk ID: ${chunk.id}`);
for (let key in chunk.data) {
console.log(`${indent}${key}: ${JSON.stringify(chunk.data[key])}`);
}
chunk.subchunks.forEach(sub => recurse(sub, indent + ' '));
};
if (this.chunks) recurse(this.chunks);
}
save(filename) {
const buffer = this.writeChunk(this.chunks);
fs.writeFileSync(filename, buffer);
}
writeChunk(chunk) {
const id = parseInt(chunk.id, 16);
const dataBuffer = this.writeData(chunk.data, id);
let subBuffers = Buffer.alloc(0);
chunk.subchunks.forEach(sub => {
subBuffers = Buffer.concat([subBuffers, this.writeChunk(sub)]);
});
const totalLen = 6 + dataBuffer.length + subBuffers.length;
const header = Buffer.alloc(6);
header.writeUint16LE(id, 0);
header.writeUint32LE(totalLen, 2);
return Buffer.concat([header, dataBuffer, subBuffers]);
}
writeData(data, id) {
const buffer = Buffer.alloc(1024); // Adjust as needed
let pos = 0;
if (id === 0x0002) {
buffer.writeUint32LE(data.version, pos); pos += 4;
} else if (id === 0xA000) {
pos += buffer.write(data.name, pos);
buffer[pos++] = 0;
} else if (id === 0x4000) {
pos += buffer.write(data.objectName, pos);
buffer[pos++] = 0;
} else if (id === 0x0010) {
data.rgbFloat.forEach(f => { buffer.writeFloatLE(f, pos); pos += 4; });
} else if (id === 0x0011) {
data.rgbByte.forEach(b => { buffer[pos++] = b; });
} else if (id === 0x4110) {
buffer.writeUint16LE(data.vertexCount, pos); pos += 2;
data.vertices.forEach(v => {
v.forEach(f => { buffer.writeFloatLE(f, pos); pos += 4; });
});
} else if (id === 0x4120) {
buffer.writeUint16LE(data.faceCount, pos); pos += 2;
data.faces.forEach(f => {
f.forEach(s => { buffer.writeUint16LE(s, pos); pos += 2; });
});
} else if (id === 0x4130) {
pos += buffer.write(data.materialName, pos);
buffer[pos++] = 0;
buffer.writeUint16LE(data.faceCount, pos); pos += 2;
data.faceIndices.forEach(idx => { buffer.writeUint16LE(idx, pos); pos += 2; });
} else if (id === 0x4140) {
buffer.writeUint16LE(data.uvCount, pos); pos += 2;
data.uvs.forEach(uv => {
uv.forEach(f => { buffer.writeFloatLE(f, pos); pos += 4; });
});
} else if (id === 0x4150) {
data.smoothingGroups.forEach(g => { buffer.writeUint32LE(g, pos); pos += 4; });
} else if (id === 0x4160) {
data.matrix.forEach(f => { buffer.writeFloatLE(f, pos); pos += 4; });
} else if (id === 0x4600) {
data.position.forEach(f => { buffer.writeFloatLE(f, pos); pos += 4; });
} else if (id === 0x4700) {
data.position.forEach(f => { buffer.writeFloatLE(f, pos); pos += 4; });
data.target.forEach(f => { buffer.writeFloatLE(f, pos); pos += 4; });
buffer.writeFloatLE(data.bank, pos); pos += 4;
buffer.writeFloatLE(data.lens, pos); pos += 4;
} else if (data.raw) {
buffer = Buffer.from(data.raw);
pos = data.raw.length;
}
return buffer.slice(0, pos);
}
}
- The following is a C++ class for handling .3DS files:
#include <fstream>
#include <iostream>
#include <vector>
#include <string>
#include <cstring>
#include <iomanip>
struct ChunkData {
std::string id;
std::vector<ChunkData> subchunks;
std::map<std::string, std::any> data; // Use std::any for flexibility
};
class ThreeDSFile {
private:
ChunkData chunks;
public:
ThreeDSFile(const std::string& filename = "") {
if (!filename.empty()) {
load(filename);
}
}
void load(const std::string& filename) {
std::ifstream file(filename, std::ios::binary | std::ios::ate);
if (!file) return;
size_t size = file.tellg();
file.seekg(0, std::ios::beg);
std::vector<char> buffer(size);
file.read(buffer.data(), size);
chunks = parseChunk(buffer.data(), 0, size).first;
}
std::pair<ChunkData, size_t> parseChunk(const char* buffer, size_t pos, size_t limit) {
if (pos >= limit) return {ChunkData{}, pos};
unsigned short id;
memcpy(&id, buffer + pos, 2); pos += 2;
unsigned int len;
memcpy(&len, buffer + pos, 4); pos += 4;
size_t end = pos + len - 6;
ChunkData chunk;
chunk.id = "0x" + std::to_string(id);
switch (id) {
case 0x4D4D: chunk.data["type"] = std::string("MAIN3DS"); break;
case 0x0002: {
unsigned int ver;
memcpy(&ver, buffer + pos, 4); pos += 4;
chunk.data["version"] = ver;
break;
}
case 0x3D3D: chunk.data["type"] = std::string("EDIT3DS"); break;
case 0xB000: chunk.data["type"] = std::string("KEYF3DS"); break;
case 0xAFFF: chunk.data["type"] = std::string("MATERIAL"); break;
case 0xA000: {
std::string name = readString(buffer, pos);
pos += name.length() + 1;
chunk.data["name"] = name;
break;
}
case 0xA010: chunk.data["colorType"] = std::string("Ambient"); break;
case 0xA020: chunk.data["colorType"] = std::string("Diffuse"); break;
case 0xA030: chunk.data["colorType"] = std::string("Specular"); break;
case 0x0010: {
float rgb[3];
memcpy(rgb, buffer + pos, 12); pos += 12;
chunk.data["rgbFloat"] = std::vector<float>(rgb, rgb + 3);
break;
}
case 0x0011: {
unsigned char rgb[3];
memcpy(rgb, buffer + pos, 3); pos += 3;
chunk.data["rgbByte"] = std::vector<unsigned char>(rgb, rgb + 3);
break;
}
case 0x4000: {
std::string name = readString(buffer, pos);
pos += name.length() + 1;
chunk.data["objectName"] = name;
break;
}
case 0x4100: chunk.data["type"] = std::string("TRIMESH"); break;
case 0x4110: {
unsigned short count;
memcpy(&count, buffer + pos, 2); pos += 2;
chunk.data["vertexCount"] = (unsigned int)count;
std::vector<std::vector<float>> vertices;
for (unsigned short i = 0; i < count; i++) {
float v[3];
memcpy(v, buffer + pos, 12); pos += 12;
vertices.push_back({v[0], v[1], v[2]});
}
chunk.data["vertices"] = vertices;
break;
}
case 0x4120: {
unsigned short count;
memcpy(&count, buffer + pos, 2); pos += 2;
chunk.data["faceCount"] = (unsigned int)count;
std::vector<std::vector<unsigned short>> faces;
for (unsigned short i = 0; i < count; i++) {
unsigned short f[4];
memcpy(f, buffer + pos, 8); pos += 8;
faces.push_back({f[0], f[1], f[2], f[3]});
}
chunk.data["faces"] = faces;
break;
}
case 0x4130: {
std::string name = readString(buffer, pos);
pos += name.length() + 1;
chunk.data["materialName"] = name;
unsigned short count;
memcpy(&count, buffer + pos, 2); pos += 2;
chunk.data["faceCount"] = (unsigned int)count;
std::vector<unsigned short> indices;
for (unsigned short i = 0; i < count; i++) {
unsigned short idx;
memcpy(&idx, buffer + pos, 2); pos += 2;
indices.push_back(idx);
}
chunk.data["faceIndices"] = indices;
break;
}
case 0x4140: {
unsigned short count;
memcpy(&count, buffer + pos, 2); pos += 2;
chunk.data["uvCount"] = (unsigned int)count;
std::vector<std::vector<float>> uvs;
for (unsigned short i = 0; i < count; i++) {
float uv[2];
memcpy(uv, buffer + pos, 8); pos += 8;
uvs.push_back({uv[0], uv[1]});
}
chunk.data["uvs"] = uvs;
break;
}
case 0x4150: {
std::vector<unsigned int> groups;
size_t num = (len - 6) / 4;
for (size_t i = 0; i < num; i++) {
unsigned int group;
memcpy(&group, buffer + pos, 4); pos += 4;
groups.push_back(group);
}
chunk.data["smoothingGroups"] = groups;
break;
}
case 0x4160: {
float matrix[12];
memcpy(matrix, buffer + pos, 48); pos += 48;
chunk.data["matrix"] = std::vector<float>(matrix, matrix + 12);
break;
}
case 0x4600: {
float posL[3];
memcpy(posL, buffer + pos, 12); pos += 12;
chunk.data["position"] = std::vector<float>(posL, posL + 3);
break;
}
case 0x4700: {
float posC[3];
memcpy(posC, buffer + pos, 12); pos += 12;
float tar[3];
memcpy(tar, buffer + pos, 12); pos += 12;
float bank;
memcpy(&bank, buffer + pos, 4); pos += 4;
float lens;
memcpy(&lens, buffer + pos, 4); pos += 4;
chunk.data["position"] = std::vector<float>(posC, posC + 3);
chunk.data["target"] = std::vector<float>(tar, tar + 3);
chunk.data["bank"] = bank;
chunk.data["lens"] = lens;
break;
}
default: {
std::vector<char> raw(buffer + pos, buffer + end);
chunk.data["raw"] = raw;
pos = end;
break;
}
}
while (pos < end) {
auto [sub, newPos] = parseChunk(buffer, pos, limit);
if (!sub.id.empty()) chunk.subchunks.push_back(sub);
pos = newPos;
}
return {chunk, pos};
}
std::string readString(const char* buffer, size_t pos) {
std::string str;
char c;
while ((c = buffer[pos++]) != 0) str += c;
return str;
}
void printProperties() {
printRecurse(chunks, "");
}
void printRecurse(const ChunkData& chunk, const std::string& indent) {
std::cout << indent << "Chunk ID: " << chunk.id << std::endl;
for (const auto& entry : chunk.data) {
std::cout << indent << entry.first << ": ";
if (entry.second.type() == typeid(std::string)) {
std::cout << std::any_cast<std::string>(entry.second);
} else if (entry.second.type() == typeid(unsigned int)) {
std::cout << std::any_cast<unsigned int>(entry.second);
} else if (entry.second.type() == typeid(float)) {
std::cout << std::any_cast<float>(entry.second);
} else if (entry.second.type() == typeid(std::vector<float>)) {
auto vec = std::any_cast<std::vector<float>>(entry.second);
for (float v : vec) std::cout << v << " ";
} else if (entry.second.type() == typeid(std::vector<unsigned char>)) {
auto vec = std::any_cast<std::vector<unsigned char>>(entry.second);
for (unsigned char v : vec) std::cout << (int)v << " ";
} else if (entry.second.type() == typeid(std::vector<unsigned short>)) {
auto vec = std::any_cast<std::vector<unsigned short>>(entry.second);
for (unsigned short v : vec) std::cout << v << " ";
} else if (entry.second.type() == typeid(std::vector<unsigned int>)) {
auto vec = std::any_cast<std::vector<unsigned int>>(entry.second);
for (unsigned int v : vec) std::cout << v << " ";
} else if (entry.second.type() == typeid(std::vector<std::vector<float>>)) {
auto vecs = std::any_cast<std::vector<std::vector<float>>>(entry.second);
for (auto& vec : vecs) {
for (float v : vec) std::cout << v << " ";
std::cout << "; ";
}
} else if (entry.second.type() == typeid(std::vector<std::vector<unsigned short>>)) {
auto vecs = std::any_cast<std::vector<std::vector<unsigned short>>>(entry.second);
for (auto& vec : vecs) {
for (unsigned short v : vec) std::cout << v << " ";
std::cout << "; ";
}
} else if (entry.second.type() == typeid(std::vector<char>)) {
auto raw = std::any_cast<std::vector<char>>(entry.second);
std::cout << "Raw data (" << raw.size() << " bytes)";
}
std::cout << std::endl;
}
for (const auto& sub : chunk.subchunks) {
printRecurse(sub, indent + " ");
}
}
void save(const std::string& filename) {
std::ofstream file(filename, std::ios::binary);
if (!file) return;
writeChunk(file, chunks);
}
void writeChunk(std::ofstream& file, const ChunkData& chunk) {
unsigned short id = std::stoul(chunk.id.substr(2), nullptr, 16);
std::vector<char> dataBuffer = writeData(chunk.data, id);
std::vector<char> subBuffer;
for (const auto& sub : chunk.subchunks) {
std::ostringstream subOs;
writeChunk(subOs, sub);
std::string subStr = subOs.str();
subBuffer.insert(subBuffer.end(), subStr.begin(), subStr.end());
}
unsigned int totalLen = 6 + dataBuffer.size() + subBuffer.size();
file.write(reinterpret_cast<const char*>(&id), 2);
file.write(reinterpret_cast<const char*>(&totalLen), 4);
file.write(dataBuffer.data(), dataBuffer.size());
file.write(subBuffer.data(), subBuffer.size());
}
std::vector<char> writeData(const std::map<std::string, std::any>& data, unsigned short id) {
std::vector<char> buffer;
if (id == 0x0002) {
unsigned int ver = std::any_cast<unsigned int>(data.at("version"));
buffer.insert(buffer.end(), reinterpret_cast<char*>(&ver), reinterpret_cast<char*>(&ver) + 4);
} else if (id == 0xA000) {
std::string name = std::any_cast<std::string>(data.at("name"));
buffer.insert(buffer.end(), name.begin(), name.end());
buffer.push_back(0);
} else if (id == 0x4000) {
std::string name = std::any_cast<std::string>(data.at("objectName"));
buffer.insert(buffer.end(), name.begin(), name.end());
buffer.push_back(0);
} else if (id == 0x0010) {
auto rgb = std::any_cast<std::vector<float>>(data.at("rgbFloat"));
for (float f : rgb) {
buffer.insert(buffer.end(), reinterpret_cast<char*>(&f), reinterpret_cast<char*>(&f) + 4);
}
} else if (id == 0x0011) {
auto rgb = std::any_cast<std::vector<unsigned char>>(data.at("rgbByte"));
buffer.insert(buffer.end(), rgb.begin(), rgb.end());
} else if (id == 0x4110) {
unsigned short count = static_cast<unsigned short>(std::any_cast<unsigned int>(data.at("vertexCount")));
buffer.insert(buffer.end(), reinterpret_cast<char*>(&count), reinterpret_cast<char*>(&count) + 2);
auto verts = std::any_cast<std::vector<std::vector<float>>>(data.at("vertices"));
for (auto& v : verts) {
for (float f : v) {
buffer.insert(buffer.end(), reinterpret_cast<char*>(&f), reinterpret_cast<char*>(&f) + 4);
}
}
} else if (id == 0x4120) {
unsigned short count = static_cast<unsigned short>(std::any_cast<unsigned int>(data.at("faceCount")));
buffer.insert(buffer.end(), reinterpret_cast<char*>(&count), reinterpret_cast<char*>(&count) + 2);
auto faces = std::any_cast<std::vector<std::vector<unsigned short>>>(data.at("faces"));
for (auto& f : faces) {
for (unsigned short s : f) {
buffer.insert(buffer.end(), reinterpret_cast<char*>(&s), reinterpret_cast<char*>(&s) + 2);
}
}
} else if (id == 0x4130) {
std::string name = std::any_cast<std::string>(data.at("materialName"));
buffer.insert(buffer.end(), name.begin(), name.end());
buffer.push_back(0);
unsigned short count = static_cast<unsigned short>(std::any_cast<unsigned int>(data.at("faceCount")));
buffer.insert(buffer.end(), reinterpret_cast<char*>(&count), reinterpret_cast<char*>(&count) + 2);
auto indices = std::any_cast<std::vector<unsigned short>>(data.at("faceIndices"));
for (unsigned short idx : indices) {
buffer.insert(buffer.end(), reinterpret_cast<char*>(&idx), reinterpret_cast<char*>(&idx) + 2);
}
} else if (id == 0x4140) {
unsigned short count = static_cast<unsigned short>(std::any_cast<unsigned int>(data.at("uvCount")));
buffer.insert(buffer.end(), reinterpret_cast<char*>(&count), reinterpret_cast<char*>(&count) + 2);
auto uvs = std::any_cast<std::vector<std::vector<float>>>(data.at("uvs"));
for (auto& uv : uvs) {
for (float f : uv) {
buffer.insert(buffer.end(), reinterpret_cast<char*>(&f), reinterpret_cast<char*>(&f) + 4);
}
}
} else if (id == 0x4150) {
auto groups = std::any_cast<std::vector<unsigned int>>(data.at("smoothingGroups"));
for (unsigned int g : groups) {
buffer.insert(buffer.end(), reinterpret_cast<char*>(&g), reinterpret_cast<char*>(&g) + 4);
}
} else if (id == 0x4160) {
auto matrix = std::any_cast<std::vector<float>>(data.at("matrix"));
for (float f : matrix) {
buffer.insert(buffer.end(), reinterpret_cast<char*>(&f), reinterpret_cast<char*>(&f) + 4);
}
} else if (id == 0x4600) {
auto pos = std::any_cast<std::vector<float>>(data.at("position"));
for (float f : pos) {
buffer.insert(buffer.end(), reinterpret_cast<char*>(&f), reinterpret_cast<char*>(&f) + 4);
}
} else if (id == 0x4700) {
auto pos = std::any_cast<std::vector<float>>(data.at("position"));
for (float f : pos) {
buffer.insert(buffer.end(), reinterpret_cast<char*>(&f), reinterpret_cast<char*>(&f) + 4);
}
auto tar = std::any_cast<std::vector<float>>(data.at("target"));
for (float f : tar) {
buffer.insert(buffer.end(), reinterpret_cast<char*>(&f), reinterpret_cast<char*>(&f) + 4);
}
float bank = std::any_cast<float>(data.at("bank"));
buffer.insert(buffer.end(), reinterpret_cast<char*>(&bank), reinterpret_cast<char*>(&bank) + 4);
float lens = std::any_cast<float>(data.at("lens"));
buffer.insert(buffer.end(), reinterpret_cast<char*>(&lens), reinterpret_cast<char*>(&lens) + 4);
} else if (data.count("raw")) {
auto raw = std::any_cast<std::vector<char>>(data.at("raw"));
buffer = raw;
}
return buffer;
}
};