Skip to content
ProgSys edited this page Jun 1, 2024 · 48 revisions

The GEO file contains geometry and animations organized as a scene graph. Similar to the GLTF file format.

File structure

Overview

Name Description
Header geofileHeader
Transformations vector<geofileTransformation>
Alpha Keyframes vector<geofileAlphaKeyframe> - Animations
Scene Nodes vector<geofileTreeNode>
Empty Space Fill to next 16 bytes
Meshes vector<geofileMesh>
├── Mesh header geofileMeshHeader
├── Surfaces vector<geofileSurface> - Maps Triangles to textures
├── Vertices vector<vec3> - Triangles, size must be divisible by 3
├── Tangent vector<vec3> - Optional: If new is set, used for normal mapping
├── Bitangent vector<vec3> - Optional: If new is set, used for normal mapping
├── UV vector<vec2> - Texture coordinates
├── Empty Space Fill to next 16 bytes
└── Textures vector<geofileTexture>
$~~~~~~~~$├── Texture header vector<geofileTextureHeader>
$~~~~~~~~$├── Colortable vector<rgba> - Used deepening on texture format
$~~~~~~~~$└── Data vector<char> - Texture binary data

Header

The geo file starts with a header:

struct geofileHeader
{
    int unk1;
    int number_of_nodes; // Total number of scene nodes
    int unk3;
    int number_of_transformations; // Total number of transformations
    int number_of_keyframes; // Total number of keyframes
    int offset_to_first_mesh; // Offset to the very first mesh (geometry)
    int number_of_meshes; // Total number of meshes (geometry)
    int unk6; 
}

Transformations

A list of transformations, used for both the initial scene node transformation and keyframes.

struct geofileTransformation
{
    glm::vec3 position; // (xyz-float) All values are multiplied by 10.
    float unk7;
    glm::vec3 rotation;  // (xyz-float) Euler angles YXZ in degree. All values are multiplied by 10.
    float unk8;
    glm::vec3 scale;  // (xyz-float) All values are multiplied by 100.
    float unk9;
    float time; // When Transformation used as a keyframe then this declares its length 
    float unk11, unk12, unk13;
}

I use Open GL, to convert the values to the correct space I use the following functions:

glm::vec3 fromDisaPosition(const glm::vec3& position) { return glm::vec3(-position.x, -position.y, position.z) / 10.0f; }
glm::quat fromDisaRotation(const glm::vec3& rotation){
	const glm::vec3 eulerRotation = glm::radians(glm::vec3(
		-rotation.z / 10.0f,
		-rotation.y / 10.0f,
		rotation.x / 10.0f
	));
	return glm::quat(glm::eulerAngleYXZ(
		eulerRotation.y, eulerRotation.z, eulerRotation.x
	));
}
glm::vec3 fromDisaScale(const glm::vec3& scale) { return scale / 100.0f; }

Alpha Keyframes

Used for "sudo" particle systems to control the alpha values of each particle (node).

struct geofileAlphaKeyframe{
    unsigned short value;
    unsigned short time;
};

Scene Nodes

A Scene Tree is a hierarchical structure to organize scene meshes. Each node can have multiple child nodes, enabling transformations and animations between each other. So if you rotate the parent all children will be rotated too. (same with alpha)

struct geofileTreeNode
{
    short parent_index; // The index of the parent node. 0 is root, so 1 is first entry
    short transformations_start_index;
    short number_of_transformations; // Must be at least 1. If more then animated (position, rotation, scale)
    short mesh_index; // If -1, then node is a pure transformation node.
    short transformation_mode; // 2 or 3 is a Billboard
    short alpha_keyframe_start_index; // Is 0, if node then not alpha animated
    short alpha_number_of_keyframes; // Is 0, if node then not alpha animated
}

Meshes

The actual geometry data that can be attached to a scene node. The very first mesh starts at address 'offset_to_first_mesh'. Inside the mesh all addresses are relative to the origin address of mesh.

struct geofileMesh {
    geofileMeshHeader header;

    std::vector<geofileSurface> surfaces;
    //buffers
    std::vector<glm::vec3> vertices;
    std::vector<glm::vec3> tangent; //optinal, if new is set, used for normal mapping 
    std::vector<glm::vec3> bitangent; //optinal, if new is set, used for normal mapping 
    std::vector<glm::vec2> uv;
    std::vector<geofileTexture> textures;
};

Mesh header

struct geofileMeshHeader {
    unsigned short number_of_triangles, isNew;
    int unk9, number_of_surfaces, number_of_textures;
    int start_offset_texture, end_offset_mesh, size_of_triangles, unk12; //addresses are relative to the origin address of mesh
}

isNew marks if the mesh has tangent and bitangent. Usually this also means that there normal textures as well. This is only used for the HD mode.

Surfaces

Defines what texture to apply to the triangles. Plus some effects.

struct geofileSurface{
    unsigned char rMult, gMult, bMult, aMult; //RGBA color texture multiplication
    unsigned short index; // Index to texture starting from 1. Can be be 0, then this surface has no texture. (Default texture is Black?)
    unsigned short length; // Number of vertices inside surface (start is the sum of previous lenghts)
    unsigned char additive, animated, unk5, unk6; //texture effects, for particles or lava scroll
}

Textures

struct geofileTexture{
    geofileTextureHeader header;
    std::vector<rgba> colortable; //is filled depending on texture format
    std::vector<char> data; //binary texture data
}

Texture header

They already have a fine texture format, but no, they decided to invent a new one. However the tx2 format also creeps into it.

struct geofileTextureHeader {
    unsigned short totalSize; //total size if the texture, including the header and everything else
    unsigned char unk0;
    unsigned char type : 7;
    bool unkFlag : 1; //maybe if texture has normal map?!
    unsigned short width, height;
    unsigned short infoField; //usage depends on the type. Can be colortable size or additional type.
    unsigned char powerWidth, powerHeight;
    unsigned int unk4;
}

Right now its not 100% clear how it works, but here what I know. The type can be either 0, for old textures, or 16, for a variation of the tx2 texture format. Based on the values, the types can be determined as follows:

if(type == 0){
 // infoField is used as color table size
 return infoField <= 16? TX2::COLORTABLE_BGRA16: TX2::COLORTABLE_BGRA256;
}else if(type == 16){
 // infoField corresponds to a subset of TX2 types
 switch(infoField){
   case 0: return TX2::DXT1;
   case 1: return TX2::DXT4;
   case 2: return TX2::DXT5;
   case 3: return TX2::BGRA;
   case 4: return TX2::FONT;
   // Other types are likely not supported
}
throw "No idea what it is!?";
}

Note that totalSize is an unsigned short, which is used to jump to the next texture inside the list. This limits the size of any texture to 65,535 bytes, except for the last texture, which can be of any size :P