Task 724: .TER File Format
Task 724: .TER File Format
File Format Specifications for .TER (Terragen Terrain File)
The .TER file format is used by Terragen, a landscape generation software, to store terrain heightmap data. It is a binary format with a fixed header, followed by variable chunks that describe the terrain's dimensions, scaling, curvature, and elevation data. All multi-byte values are in little-endian byte order. The format is documented in detail at the Planetside Software wiki.
- List of all the properties of this file format intrinsic to its file system:
- Header: A 16-byte ASCII string "TERRAGENTERRAIN " (8 bytes "TERRAGEN" followed by 8 bytes "TERRAIN " with a space).
- Size: A 2-byte signed integer representing (shortest side - 1), used to determine the terrain grid size (required).
- Xpts: A 2-byte unsigned integer for the number of points in the X direction (optional for square terrains).
- Ypts: A 2-byte unsigned integer for the number of points in the Y direction (optional for square terrains).
- Scale: Three 4-byte floating-point values for x_scale, y_scale, z_scale in meters per terrain unit (optional, default 30,30,30).
- Planet Radius: A 4-byte floating-point value for the planet radius in kilometers (optional, default 6370).
- Curve Mode: A 4-byte unsigned integer for curve mode (0 for flat, 1 for spherical; optional, default 0).
- Height Scale: A 2-byte signed integer used in altitude calculation (required, part of ALTW chunk).
- Base Height: A 2-byte signed integer for base altitude (required, part of ALTW chunk).
- Elevations: A grid of 2-byte signed integers for elevation values (row-major order, size xpts * ypts; required).
- EOF Marker: A 4-byte ASCII string "EOF " (with space; required for compatibility).
- Two direct download links for files of format .TER:
- https://www.dropbox.com/sh/y238tdbs6yosmtr/AAC_f8VCgj-ha1aaQ6ZpakRVa/dense_crater_field.ter?dl=1 (Dense crater field terrain from a KSP-related share)
- https://www.dropbox.com/sh/y238tdbs6yosmtr/AAC_f8VCgj-ha1aaQ6ZpakRVa/heightmap.ter?dl=1 (Additional heightmap from the same share)
- Ghost blog embedded HTML JavaScript for drag and drop .TER file to dump properties to screen:
Drag and drop .TER file here
- Python class for .TER file handling:
import struct
import sys
class TerFile:
def __init__(self, filepath):
self.filepath = filepath
self.header = ''
self.size = 0
self.xpts = 0
self.ypts = 0
self.scale = [30.0, 30.0, 30.0]
self.crad = 6370.0
self.crvm = 0
self.heightscale = 0
self.baseheight = 0
self.elevations = []
self.eof = ''
def read(self):
with open(self.filepath, 'rb') as f:
data = f.read()
offset = 0
# Header
self.header = data[offset:offset+16].decode('ascii')
offset += 16
while offset < len(data) - 4:
chunk_id = data[offset:offset+4].decode('ascii')
offset += 4
if chunk_id == 'SIZE':
self.size = struct.unpack_from('<h', data, offset)[0] + 1
offset += 4 # value + padding
elif chunk_id == 'XPTS':
self.xpts = struct.unpack_from('<H', data, offset)[0]
offset += 4
elif chunk_id == 'YPTS':
self.ypts = struct.unpack_from('<H', data, offset)[0]
offset += 4
elif chunk_id == 'SCAL':
self.scale = list(struct.unpack_from('<fff', data, offset))
offset += 12
elif chunk_id == 'CRAD':
self.crad = struct.unpack_from('<f', data, offset)[0]
offset += 4
elif chunk_id == 'CRVM':
self.crvm = struct.unpack_from('<I', data, offset)[0]
offset += 4
elif chunk_id == 'ALTW':
self.heightscale = struct.unpack_from('<h', data, offset)[0]
self.baseheight = struct.unpack_from('<h', data, offset + 2)[0]
offset += 4
if self.xpts == 0: self.xpts = self.size
if self.ypts == 0: self.ypts = self.size
num_points = self.xpts * self.ypts
self.elevations = list(struct.unpack_from('<' + 'h' * num_points, data, offset))
offset += 2 * num_points
elif chunk_id == 'EOF ':
self.eof = chunk_id
break
def print_properties(self):
print(f"Header: {self.header}")
print(f"Size: {self.size}")
print(f"Xpts: {self.xpts}")
print(f"Ypts: {self.ypts}")
print(f"Scale: {' '.join(map(str, self.scale))}")
print(f"Planet Radius: {self.crad}")
print(f"Curve Mode: {self.crvm}")
print(f"Height Scale: {self.heightscale}")
print(f"Base Height: {self.baseheight}")
print(f"Elevations: {self.elevations}")
print(f"EOF Marker: {self.eof}")
def write(self, new_filepath=None):
if not new_filepath:
new_filepath = self.filepath
with open(new_filepath, 'wb') as f:
f.write(self.header.encode('ascii'))
f.write(b'SIZE')
f.write(struct.pack('<h', self.size - 1))
f.write(b'\x00\x00') # padding
if self.xpts != self.size or self.ypts != self.size:
f.write(b'XPTS')
f.write(struct.pack('<H', self.xpts))
f.write(b'\x00\x00')
f.write(b'YPTS')
f.write(struct.pack('<H', self.ypts))
f.write(b'\x00\x00')
if self.scale != [30.0, 30.0, 30.0]:
f.write(b'SCAL')
f.write(struct.pack('<fff', *self.scale))
if self.crad != 6370.0:
f.write(b'CRAD')
f.write(struct.pack('<f', self.crad))
if self.crvm != 0:
f.write(b'CRVM')
f.write(struct.pack('<I', self.crvm))
f.write(b'ALTW')
f.write(struct.pack('<h', self.heightscale))
f.write(struct.pack('<h', self.baseheight))
for elev in self.elevations:
f.write(struct.pack('<h', elev))
f.write(b'EOF ')
# Example usage
if __name__ == '__main__':
if len(sys.argv) < 2:
print("Usage: python ter.py <filepath.ter>")
sys.exit(1)
ter = TerFile(sys.argv[1])
ter.read()
ter.print_properties()
# To write: ter.write('output.ter')
- Java class for .TER file handling:
import java.io.*;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Arrays;
public class TerFile {
private String filepath;
private String header;
private int size;
private int xpts;
private int ypts;
private float[] scale = new float[]{30f, 30f, 30f};
private float crad = 6370f;
private int crvm = 0;
private short heightscale;
private short baseheight;
private short[] elevations;
private String eof;
public TerFile(String filepath) {
this.filepath = filepath;
}
public void read() throws IOException {
byte[] data = Files.readAllBytes(Paths.get(filepath));
ByteBuffer bb = ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN);
int offset = 0;
// Header
byte[] headerBytes = new byte[16];
bb.position(offset);
bb.get(headerBytes);
header = new String(headerBytes);
offset += 16;
while (offset < data.length - 4) {
byte[] chunkBytes = new byte[4];
bb.position(offset);
bb.get(chunkBytes);
String chunkId = new String(chunkBytes);
offset += 4;
if (chunkId.equals("SIZE")) {
size = bb.getShort() + 1;
offset += 4;
} else if (chunkId.equals("XPTS")) {
xpts = bb.getShort() & 0xFFFF;
offset += 4;
} else if (chunkId.equals("YPTS")) {
ypts = bb.getShort() & 0xFFFF;
offset += 4;
} else if (chunkId.equals("SCAL")) {
scale[0] = bb.getFloat();
scale[1] = bb.getFloat();
scale[2] = bb.getFloat();
offset += 12;
} else if (chunkId.equals("CRAD")) {
crad = bb.getFloat();
offset += 4;
} else if (chunkId.equals("CRVM")) {
crvm = bb.getInt();
offset += 4;
} else if (chunkId.equals("ALTW")) {
heightscale = bb.getShort();
baseheight = bb.getShort();
offset += 4;
if (xpts == 0) xpts = size;
if (ypts == 0) ypts = size;
elevations = new short[xpts * ypts];
for (int i = 0; i < elevations.length; i++) {
elevations[i] = bb.getShort();
}
offset += 2 * elevations.length;
} else if (chunkId.equals("EOF ")) {
eof = chunkId;
break;
} else {
break;
}
bb.position(offset);
}
}
public void printProperties() {
System.out.println("Header: " + header);
System.out.println("Size: " + size);
System.out.println("Xpts: " + xpts);
System.out.println("Ypts: " + ypts);
System.out.println("Scale: " + Arrays.toString(scale));
System.out.println("Planet Radius: " + crad);
System.out.println("Curve Mode: " + crvm);
System.out.println("Height Scale: " + heightscale);
System.out.println("Base Height: " + baseheight);
System.out.println("Elevations: " + Arrays.toString(elevations));
System.out.println("EOF Marker: " + eof);
}
public void write(String newFilepath) throws IOException {
if (newFilepath == null) newFilepath = filepath;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ByteBuffer bb = ByteBuffer.allocate(1024 * 1024).order(ByteOrder.LITTLE_ENDIAN); // Buffer, adjust size if needed
bb.put(header.getBytes());
bb.put("SIZE".getBytes());
bb.putShort((short) (size - 1));
bb.putShort((short) 0); // padding
if (xpts != size || ypts != size) {
bb.put("XPTS".getBytes());
bb.putShort((short) xpts);
bb.putShort((short) 0);
bb.put("YPTS".getBytes());
bb.putShort((short) ypts);
bb.putShort((short) 0);
}
if (!Arrays.equals(scale, new float[]{30f, 30f, 30f})) {
bb.put("SCAL".getBytes());
bb.putFloat(scale[0]);
bb.putFloat(scale[1]);
bb.putFloat(scale[2]);
}
if (crad != 6370f) {
bb.put("CRAD".getBytes());
bb.putFloat(crad);
}
if (crvm != 0) {
bb.put("CRVM".getBytes());
bb.putInt(crvm);
}
bb.put("ALTW".getBytes());
bb.putShort(heightscale);
bb.putShort(baseheight);
for (short elev : elevations) {
bb.putShort(elev);
}
bb.put("EOF ".getBytes());
byte[] outputData = new byte[bb.position()];
bb.position(0);
bb.get(outputData);
Files.write(Paths.get(newFilepath), outputData);
}
public static void main(String[] args) throws IOException {
if (args.length < 1) {
System.out.println("Usage: java TerFile <filepath.ter>");
System.exit(1);
}
TerFile ter = new TerFile(args[0]);
ter.read();
ter.printProperties();
// To write: ter.write("output.ter");
}
}
- JavaScript class for .TER file handling (using Node.js for file I/O):
const fs = require('fs');
class TerFile {
constructor(filepath) {
this.filepath = filepath;
this.header = '';
this.size = 0;
this.xpts = 0;
this.ypts = 0;
this.scale = [30, 30, 30];
this.crad = 6370;
this.crvm = 0;
this.heightscale = 0;
this.baseheight = 0;
this.elevations = [];
this.eof = '';
}
read() {
const data = fs.readFileSync(this.filepath);
const dv = new DataView(data.buffer);
let offset = 0;
// Header
this.header = new TextDecoder().decode(data.slice(0, 16));
offset += 16;
while (offset < data.length - 4) {
const chunkId = new TextDecoder().decode(data.slice(offset, offset + 4));
offset += 4;
if (chunkId === 'SIZE') {
this.size = dv.getInt16(offset, true) + 1;
offset += 4;
} else if (chunkId === 'XPTS') {
this.xpts = dv.getUint16(offset, true);
offset += 4;
} else if (chunkId === 'YPTS') {
this.ypts = dv.getUint16(offset, true);
offset += 4;
} else if (chunkId === 'SCAL') {
this.scale[0] = dv.getFloat32(offset, true);
this.scale[1] = dv.getFloat32(offset + 4, true);
this.scale[2] = dv.getFloat32(offset + 8, true);
offset += 12;
} else if (chunkId === 'CRAD') {
this.crad = dv.getFloat32(offset, true);
offset += 4;
} else if (chunkId === 'CRVM') {
this.crvm = dv.getUint32(offset, true);
offset += 4;
} else if (chunkId === 'ALTW') {
this.heightscale = dv.getInt16(offset, true);
this.baseheight = dv.getInt16(offset + 2, true);
offset += 4;
if (this.xpts === 0) this.xpts = this.size;
if (this.ypts === 0) this.ypts = this.size;
this.elevations = [];
for (let i = 0; i < this.xpts * this.ypts; i++) {
this.elevations.push(dv.getInt16(offset, true));
offset += 2;
}
} else if (chunkId === 'EOF ') {
this.eof = chunkId;
break;
} else {
break;
}
}
}
printProperties() {
console.log(`Header: ${this.header}`);
console.log(`Size: ${this.size}`);
console.log(`Xpts: ${this.xpts}`);
console.log(`Ypts: ${this.ypts}`);
console.log(`Scale: ${this.scale.join(' ')}`);
console.log(`Planet Radius: ${this.crad}`);
console.log(`Curve Mode: ${this.crvm}`);
console.log(`Height Scale: ${this.heightscale}`);
console.log(`Base Height: ${this.baseheight}`);
console.log(`Elevations: [${this.elevations.join(', ')}]`);
console.log(`EOF Marker: ${this.eof}`);
}
write(newFilepath = this.filepath) {
let buffer = Buffer.alloc(16 + 1024 * 1024); // Large enough buffer
let offset = 0;
buffer.write(this.header, offset, 16, 'ascii');
offset += 16;
buffer.write('SIZE', offset, 4, 'ascii');
offset += 4;
buffer.writeInt16LE(this.size - 1, offset);
offset += 2;
offset += 2; // padding
if (this.xpts !== this.size || this.ypts !== this.size) {
buffer.write('XPTS', offset, 4, 'ascii');
offset += 4;
buffer.writeUint16LE(this.xpts, offset);
offset += 2;
offset += 2;
buffer.write('YPTS', offset, 4, 'ascii');
offset += 4;
buffer.writeUint16LE(this.ypts, offset);
offset += 2;
offset += 2;
}
if (this.scale[0] !== 30 || this.scale[1] !== 30 || this.scale[2] !== 30) {
buffer.write('SCAL', offset, 4, 'ascii');
offset += 4;
buffer.writeFloatLE(this.scale[0], offset);
offset += 4;
buffer.writeFloatLE(this.scale[1], offset);
offset += 4;
buffer.writeFloatLE(this.scale[2], offset);
offset += 4;
}
if (this.crad !== 6370) {
buffer.write('CRAD', offset, 4, 'ascii');
offset += 4;
buffer.writeFloatLE(this.crad, offset);
offset += 4;
}
if (this.crvm !== 0) {
buffer.write('CRVM', offset, 4, 'ascii');
offset += 4;
buffer.writeUint32LE(this.crvm, offset);
offset += 4;
}
buffer.write('ALTW', offset, 4, 'ascii');
offset += 4;
buffer.writeInt16LE(this.heightscale, offset);
offset += 2;
buffer.writeInt16LE(this.baseheight, offset);
offset += 2;
for (let elev of this.elevations) {
buffer.writeInt16LE(elev, offset);
offset += 2;
}
buffer.write('EOF ', offset, 4, 'ascii');
offset += 4;
fs.writeFileSync(newFilepath, buffer.slice(0, offset));
}
}
// Example usage
if (process.argv.length < 3) {
console.log('Usage: node ter.js <filepath.ter>');
process.exit(1);
}
const ter = new TerFile(process.argv[2]);
ter.read();
ter.printProperties();
// To write: ter.write('output.ter');
- C class for .TER file handling (using struct as class-like):
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
typedef struct {
char* filepath;
char header[17];
int16_t size;
uint16_t xpts;
uint16_t ypts;
float scale[3];
float crad;
uint32_t crvm;
int16_t heightscale;
int16_t baseheight;
int16_t* elevations;
int elevations_size;
char eof[5];
} TerFile;
TerFile* ter_create(const char* filepath) {
TerFile* ter = (TerFile*)malloc(sizeof(TerFile));
ter->filepath = strdup(filepath);
ter->size = 0;
ter->xpts = 0;
ter->ypts = 0;
ter->scale[0] = 30.0f; ter->scale[1] = 30.0f; ter->scale[2] = 30.0f;
ter->crad = 6370.0f;
ter->crvm = 0;
ter->heightscale = 0;
ter->baseheight = 0;
ter->elevations = NULL;
ter->elevations_size = 0;
memset(ter->eof, 0, 5);
return ter;
}
void ter_destroy(TerFile* ter) {
free(ter->filepath);
if (ter->elevations) free(ter->elevations);
free(ter);
}
int ter_read(TerFile* ter) {
FILE* f = fopen(ter->filepath, "rb");
if (!f) return -1;
fseek(f, 0, SEEK_END);
long file_size = ftell(f);
fseek(f, 0, SEEK_SET);
uint8_t* data = (uint8_t*)malloc(file_size);
fread(data, 1, file_size, f);
fclose(f);
int offset = 0;
// Header
memcpy(ter->header, data + offset, 16);
ter->header[16] = '\0';
offset += 16;
while (offset < file_size - 4) {
char chunk_id[5] = {0};
memcpy(chunk_id, data + offset, 4);
offset += 4;
if (strcmp(chunk_id, "SIZE") == 0) {
memcpy(&ter->size, data + offset, 2);
ter->size += 1;
offset += 4;
} else if (strcmp(chunk_id, "XPTS") == 0) {
memcpy(&ter->xpts, data + offset, 2);
offset += 4;
} else if (strcmp(chunk_id, "YPTS") == 0) {
memcpy(&ter->ypts, data + offset, 2);
offset += 4;
} else if (strcmp(chunk_id, "SCAL") == 0) {
memcpy(ter->scale, data + offset, 12);
offset += 12;
} else if (strcmp(chunk_id, "CRAD") == 0) {
memcpy(&ter->crad, data + offset, 4);
offset += 4;
} else if (strcmp(chunk_id, "CRVM") == 0) {
memcpy(&ter->crvm, data + offset, 4);
offset += 4;
} else if (strcmp(chunk_id, "ALTW") == 0) {
memcpy(&ter->heightscale, data + offset, 2);
memcpy(&ter->baseheight, data + offset + 2, 2);
offset += 4;
if (ter->xpts == 0) ter->xpts = ter->size;
if (ter->ypts == 0) ter->ypts = ter->size;
ter->elevations_size = ter->xpts * ter->ypts;
ter->elevations = (int16_t*)malloc(ter->elevations_size * 2);
memcpy(ter->elevations, data + offset, ter->elevations_size * 2);
offset += ter->elevations_size * 2;
} else if (strcmp(chunk_id, "EOF ") == 0) {
strcpy(ter->eof, chunk_id);
break;
} else {
break;
}
}
free(data);
return 0;
}
void ter_print_properties(TerFile* ter) {
printf("Header: %s\n", ter->header);
printf("Size: %d\n", ter->size);
printf("Xpts: %u\n", ter->xpts);
printf("Ypts: %u\n", ter->ypts);
printf("Scale: %f %f %f\n", ter->scale[0], ter->scale[1], ter->scale[2]);
printf("Planet Radius: %f\n", ter->crad);
printf("Curve Mode: %u\n", ter->crvm);
printf("Height Scale: %d\n", ter->heightscale);
printf("Base Height: %d\n", ter->baseheight);
printf("Elevations: [");
for (int i = 0; i < ter->elevations_size; i++) {
printf("%d", ter->elevations[i]);
if (i < ter->elevations_size - 1) printf(", ");
}
printf("]\n");
printf("EOF Marker: %s\n", ter->eof);
}
int ter_write(TerFile* ter, const char* new_filepath) {
if (!new_filepath) new_filepath = ter->filepath;
FILE* f = fopen(new_filepath, "wb");
if (!f) return -1;
fwrite(ter->header, 1, 16, f);
fwrite("SIZE", 1, 4, f);
int16_t size_val = ter->size - 1;
fwrite(&size_val, 2, 1, f);
int16_t padding = 0;
fwrite(&padding, 2, 1, f);
if (ter->xpts != ter->size || ter->ypts != ter->size) {
fwrite("XPTS", 1, 4, f);
fwrite(&ter->xpts, 2, 1, f);
fwrite(&padding, 2, 1, f);
fwrite("YPTS", 1, 4, f);
fwrite(&ter->ypts, 2, 1, f);
fwrite(&padding, 2, 1, f);
}
if (ter->scale[0] != 30.0f || ter->scale[1] != 30.0f || ter->scale[2] != 30.0f) {
fwrite("SCAL", 1, 4, f);
fwrite(ter->scale, 4, 3, f);
}
if (ter->crad != 6370.0f) {
fwrite("CRAD", 1, 4, f);
fwrite(&ter->crad, 4, 1, f);
}
if (ter->crvm != 0) {
fwrite("CRVM", 1, 4, f);
fwrite(&ter->crvm, 4, 1, f);
}
fwrite("ALTW", 1, 4, f);
fwrite(&ter->heightscale, 2, 1, f);
fwrite(&ter->baseheight, 2, 1, f);
fwrite(ter->elevations, 2, ter->elevations_size, f);
fwrite("EOF ", 1, 4, f);
fclose(f);
return 0;
}
int main(int argc, char** argv) {
if (argc < 2) {
printf("Usage: %s <filepath.ter>\n", argv[0]);
return 1;
}
TerFile* ter = ter_create(argv[1]);
if (ter_read(ter) != 0) {
printf("Error reading file\n");
ter_destroy(ter);
return 1;
}
ter_print_properties(ter);
// To write: ter_write(ter, "output.ter");
ter_destroy(ter);
return 0;
}