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.

  1. 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).
  1. Two direct download links for files of format .TER:
  1. Ghost blog embedded HTML JavaScript for drag and drop .TER file to dump properties to screen:
TER File Dumper
Drag and drop .TER file here
  1. 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')
  1. 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");
    }
}
  1. 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');
  1. 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;
}