// Package threedb provides functionality for working with .3db file format. // This package contains the decoder for parsing 3D model data from the proprietary .3db format // used in the Diggles game. The format stores mesh data, materials, animations, and other // 3D model information in a binary structure. package threedb import ( "encoding/binary" "errors" "io" "git.influ.su/artmares/digglestool/internal/pkg/logger" ) // Decoder is responsible for reading and parsing .3db file format. // It uses an io.ReadSeeker to navigate through the binary file structure // and extract model data according to the .3db format specification. type Decoder struct { reader io.ReadSeeker // The source from which the 3D model data is read } // NewDecoder creates a new Decoder instance with the provided reader. // The reader must implement io.ReadSeeker interface to allow both reading // and seeking within the file. func NewDecoder(reader io.ReadSeeker) *Decoder { return &Decoder{reader: reader} } // Decode parses the entire .3db file and populates the provided Model structure. // The .3db file format has a specific structure with sections for different types of data: // 1. File header (version and model name) // 2. Materials list // 3. Meshes with their properties // 4. Object definitions // 5. Animation data // 6. Shadow maps // 7. Cube maps (environment textures) // 8. Geometry data (triangles, texture coordinates, vertices, etc.) // // This method reads each section in sequence, following the file format specification. // If any error occurs during reading, the decoding process is aborted. func (dec *Decoder) Decode(model *Model) error { if model == nil { return errors.New("model is nil") } var err error // Read file header information if model.DBVersion, err = dec.readString(); err != nil { return err } if model.Name, err = dec.readString(); err != nil { return err } // Read all model components in the order they appear in the file if err = dec.readMaterials(model); err != nil { return err } if err = dec.readMeshes(model); err != nil { return err } if err = dec.readObjects(model); err != nil { return err } if err = dec.readAnimations(model); err != nil { return err } if err = dec.readShadows(model); err != nil { return err } if err = dec.readCubeMaps(model); err != nil { return err } // Read the actual geometry data (triangles, texture coordinates, vertices, etc.) if err = dec.readData(model); err != nil { return err } return nil } // seek advances the reader position by the specified offset. // This is used to skip over sections of the file that are not needed // or not yet understood in the file format. func (dec *Decoder) seek(offset int64) (err error) { _, err = dec.reader.Seek(offset, io.SeekCurrent) return } // read is a helper method that reads binary data from the reader into the provided destination. // All data in the .3db format is stored in little-endian byte order. func (dec *Decoder) read(dst any) error { return binary.Read(dec.reader, binary.LittleEndian, dst) } // readUInt8 reads an unsigned 8-bit integer from the binary stream. func (dec *Decoder) readUInt8() (result uint8, err error) { err = dec.read(&result) return } // readUInt16 reads an unsigned 16-bit integer from the binary stream. // These are commonly used for indices, counts, and references in the .3db format. func (dec *Decoder) readUInt16() (result uint16, err error) { err = dec.read(&result) return } // readUInt32 reads an unsigned 32-bit integer from the binary stream. // These are used for larger indices and counts in the .3db format. func (dec *Decoder) readUInt32() (result uint32, err error) { err = dec.read(&result) return } // readFloat32 reads a 32-bit floating point number from the binary stream. // These are used for coordinates, vectors, and other numeric values in the .3db format. func (dec *Decoder) readFloat32() (result float32, err error) { err = dec.read(&result) return } // readString reads a string from the binary stream. // In the .3db format, strings are stored as a 32-bit length followed by the string data. // This is used for names, paths, and other textual information in the model. func (dec *Decoder) readString() (string, error) { // First read the length of the string length, err := dec.readUInt32() if err != nil { return "", err } // Then read the string data buf := make([]byte, length) if err = dec.read(&buf); err != nil { return "", err } return string(buf), nil } // readVector reads a 3D vector (x, y, z coordinates) from the binary stream. // Vectors are used for positions, directions, and other spatial information in the model. // Each component of the vector is stored as a 32-bit floating point number. func (dec *Decoder) readVector() (*Vector, error) { var vec Vector var err error // Read the x, y, and z components of the vector if vec[0], err = dec.readFloat32(); err != nil { return nil, err } if vec[1], err = dec.readFloat32(); err != nil { return nil, err } if vec[2], err = dec.readFloat32(); err != nil { return nil, err } return &vec, nil } // readMaterials reads the materials section of the .3db file. // Materials define the visual appearance of the model's surfaces and include // information about textures, colors, and other surface properties. // Each material has a name, a path to its texture file, and some additional properties. func (dec *Decoder) readMaterials(model *Model) error { // Read the number of materials in the file materialCount, err := dec.readUInt16() if err != nil { return err } count := int(materialCount) // Read each material's data for i := 0; i < count; i++ { material := Material{} // Read the material name (usually corresponds to a texture name) if material.Name, err = dec.readString(); err != nil { return err } // Read the path to the material's texture file if material.Path, err = dec.readString(); err != nil { return err } // Read an unknown property (possibly related to material properties or flags) if material.Unknown, err = dec.readUInt32(); err != nil { return err } // Add the material to the model's materials list model.Materials = append(model.Materials, material) } return nil } // readMeshes reads the meshes section of the .3db file. // Meshes are the 3D objects that make up the model. Each mesh consists of // a collection of triangles, texture coordinates, and vertices, organized into "links". // Meshes also contain transformation data and references to shadows and cube maps. func (dec *Decoder) readMeshes(model *Model) error { // Read the number of meshes in the file meshCount, err := dec.readUInt32() if err != nil { return err } count := int(meshCount) // Read each mesh's data for i := 0; i < count; i++ { mesh := Mesh{} // Read the mesh links (connections to materials, triangles, etc.) if err = dec.readMeshLink(&mesh); err != nil { return err } // Read two vectors that might represent position, scale, or other transformation data if mesh.Vector1, err = dec.readVector(); err != nil { return err } if mesh.Vector2, err = dec.readVector(); err != nil { return err } // Skip 128 bytes of unknown data _ = dec.seek(0x80) // Read the shadow index (reference to a shadow in the shadows section) if mesh.Shadow, err = dec.readUInt16(); err != nil { return err } // Skip 48 bytes of unknown data _ = dec.seek(0x30) // Read the cube map index (reference to a cube map in the cube maps section) if mesh.CMap, err = dec.readUInt16(); err != nil { return err } // Add the mesh to the model's meshes list model.Meshes = append(model.Meshes, mesh) } return nil } // readMeshLink reads the links section of a mesh. // Links connect a mesh to its materials, triangles, texture coordinates, vertices, and brightness values. // Each mesh can have multiple links, typically one for each material used in the mesh. func (dec *Decoder) readMeshLink(mesh *Mesh) error { // Read the number of links in the mesh meshLinkCount, err := dec.readUInt16() if err != nil { return err } count := int(meshLinkCount) // Read each link's data for i := 0; i < count; i++ { meshLink := MeshLink{} // Read the material index (reference to a material in the materials section) if meshLink.Material, err = dec.readUInt16(); err != nil { return err } // Read an unknown property if meshLink.Unknown, err = dec.readUInt16(); err != nil { return err } // Read indices to geometry data that will be read later in readData // These are indices into arrays of triangles, texture coordinates, points, and brightness values if meshLink.Triangles, err = dec.readUInt16(); err != nil { return err } if meshLink.TextureCoordinates, err = dec.readUInt16(); err != nil { return err } if meshLink.Points, err = dec.readUInt16(); err != nil { return err } if meshLink.Brightness, err = dec.readUInt16(); err != nil { return err } // Add the link to the mesh's links list mesh.Links = append(mesh.Links, meshLink) } return nil } // readObjects reads the objects section of the .3db file. // Objects are named collections of mesh indices that group meshes together // for logical organization or animation purposes. For example, all meshes // that make up a character's arm might be grouped under an "arm" object. func (dec *Decoder) readObjects(model *Model) error { // Read the number of key-value pairs (object definitions) keyValuePairCount, err := dec.readUInt16() if err != nil { return err } count := int(keyValuePairCount) // Initialize the objects map if there are objects to read if count > 0 && model.Objects == nil { model.Objects = make(map[string][]uint32) } // Read each object definition for i := 0; i < count; i++ { // Read the object name (key) var key string if key, err = dec.readString(); err != nil { return err } // Read the number of mesh indices in this object var objectCount uint16 if objectCount, err = dec.readUInt16(); err != nil { return err } // Initialize the array for this object's mesh indices model.Objects[key] = make([]uint32, objectCount) // Read each mesh index for j := 0; j < int(objectCount); j++ { var n uint32 if n, err = dec.readUInt32(); err != nil { return err } model.Objects[key] = append(model.Objects[key], n) } } return nil } // readAnimations reads the animations section of the .3db file. // Animations define how meshes move and rotate over time. Each animation // has a name, a list of mesh indices it affects, and transformation data. func (dec *Decoder) readAnimations(model *Model) error { // Read the number of animations animationCount, err := dec.readUInt16() if err != nil { return err } count := int(animationCount) // Read each animation for i := 0; i < count; i++ { animation := Animation{} // Read the animation name if animation.Name, err = dec.readString(); err != nil { return err } // Read the number of mesh indices this animation affects var meshIndexesCount uint16 if meshIndexesCount, err = dec.readUInt16(); err != nil { return err } // Initialize the array for mesh indices animation.MeshIndexes = make([]uint32, meshIndexesCount) // Read each mesh index for j := 0; j < int(meshIndexesCount); j++ { var n uint32 if n, err = dec.readUInt32(); err != nil { return err } animation.MeshIndexes[j] = n } // Read animation properties (some are not fully understood) // These might control timing, interpolation, or other animation parameters if animation.Unknown, err = dec.readUInt16(); err != nil { return err } if animation.Unknown1, err = dec.readUInt16(); err != nil { return err } if animation.Unknown2, err = dec.readUInt16(); err != nil { return err } // Read additional animation data if animation.Unknown3, err = dec.readString(); err != nil { return err } // Read movement and rotation vectors that define the animation transformation if animation.MoveVector, err = dec.readVector(); err != nil { return err } if animation.RotationVector, err = dec.readVector(); err != nil { return err } // Add the animation to the model's animations list model.Animations = append(model.Animations, animation) } return nil } // readShadows reads the shadows section of the .3db file. // Shadows appear to be stored as 32x32 pixel images, but this implementation // currently just skips over them without processing the data. func (dec *Decoder) readShadows(_ *Model) error { // Read the number of shadows shadowCount, err := dec.readUInt16() if err != nil { return err } count := int(shadowCount) // Skip over each shadow's data // Each shadow appears to be a 32x32 pixel image (1024 bytes) for i := 0; i < count; i++ { // Skip the shadow data _ = dec.seek(32 * 32) } return nil } // readCubeMaps reads the cube maps section of the .3db file. // Cube maps are used for environment reflections and are stored as // rectangular images with dimensions specified in the file. func (dec *Decoder) readCubeMaps(_ *Model) error { // Read the number of cube maps cubeMapCount, err := dec.readUInt16() if err != nil { return err } count := int(cubeMapCount) // Read each cube map for i := 0; i < count; i++ { // Read the dimensions of the cube map image var width, height uint16 if width, err = dec.readUInt16(); err != nil { return err } if height, err = dec.readUInt16(); err != nil { return err } // Read two unknown values (possibly format or flags) _, _ = dec.readUInt16() _, _ = dec.readUInt16() // Skip the pixel data (width * height bytes) _ = dec.seek(int64(width * height)) } return nil } // readData reads the geometry data section of the .3db file. // This is the most complex part of the file format, containing all the actual 3D geometry // information that defines the model's shape and appearance. The data is organized into // several arrays: // // 1. Triangles: Define the faces of the 3D model by referencing vertices // 2. Texture Coordinates: Define how textures are mapped onto the model's surface // 3. Points: The actual 3D vertices that make up the model's shape // 4. Brightness: Per-vertex lighting information // // The function first reads counts for each type of data, then reads arrays of counts // that indicate how many elements are in each sub-array. Finally, it reads the actual // geometry data by calling specialized functions for each data type. func (dec *Decoder) readData(model *Model) error { var ( // These variables store the number of sub-arrays for each data type triangleCount uint16 // Number of triangle arrays trianglesCounts []uint16 // Number of triangles in each array textureCoordCount uint16 // Number of texture coordinate arrays textureCoordCounts []uint16 // Number of texture coordinates in each array pointCount uint16 // Number of point (vertex) arrays pointCounts []uint16 // Number of points in each array brightnessCount uint16 // Number of brightness arrays brightnessCounts []uint16 // Number of brightness values in each array unknownCount uint32 // Number of unknown data blocks to skip cnt uint16 // Temporary variable for reading counts err error ) // Read the number of arrays for each data type if triangleCount, err = dec.readUInt16(); err != nil { return err } if textureCoordCount, err = dec.readUInt16(); err != nil { return err } if pointCount, err = dec.readUInt16(); err != nil { return err } if brightnessCount, err = dec.readUInt16(); err != nil { return err } // Read the count of unknown data blocks (possibly animation or physics related) if unknownCount, err = dec.readUInt32(); err != nil { return err } logger.Debug("unknownCount:", unknownCount) // Read the number of elements in each triangle array // Each value tells us how many triangles are in the corresponding array for i := 0; i < int(triangleCount); i++ { if cnt, err = dec.readUInt16(); err != nil { return err } trianglesCounts = append(trianglesCounts, cnt) } // Read the number of elements in each texture coordinate array // Each value tells us how many texture coordinates are in the corresponding array for i := 0; i < int(textureCoordCount); i++ { if cnt, err = dec.readUInt16(); err != nil { return err } textureCoordCounts = append(textureCoordCounts, cnt) } // Read the number of elements in each point (vertex) array // Each value tells us how many vertices are in the corresponding array for i := 0; i < int(pointCount); i++ { if cnt, err = dec.readUInt16(); err != nil { return err } pointCounts = append(pointCounts, cnt) } // Read the number of elements in each brightness array // Each value tells us how many brightness values are in the corresponding array for i := 0; i < int(brightnessCount); i++ { if cnt, err = dec.readUInt16(); err != nil { return err } brightnessCounts = append(brightnessCounts, cnt) } // Skip over blocks of unknown data // Each block is 20 bytes long and may contain additional model information // that is not currently used by this decoder for i := 0; i < int(unknownCount); i++ { // Note: There was experimental code here to try to decode this data, // but it's been commented out as the format is not fully understood. // Each block appears to be 20 bytes in size. _ = dec.seek(20) } // Now read the actual geometry data using the counts we've collected // Read triangle indices that define the model's faces if err = dec.readTriangles(model, int(triangleCount), trianglesCounts); err != nil { return err } // Read texture coordinates that define how textures map onto the model if err = dec.readTextureCoordinates(model, int(textureCoordCount), textureCoordCounts); err != nil { return err } // Read 3D points (vertices) that define the model's shape if err = dec.readPoint(model, int(pointCount), pointCounts); err != nil { return err } // Read brightness values that define per-vertex lighting if err = dec.readBrightness(model, int(brightnessCount), brightnessCounts); err != nil { return err } return nil } // readTriangles reads the triangle data from the .3db file. // Triangles are the fundamental building blocks of 3D models, defining the faces // that make up the model's surface. Each triangle is defined by three indices that // reference vertices in the corresponding point array. // // The function reads 'count' arrays of triangles, where each array contains a variable // number of triangle indices as specified in the 'counts' slice. Each triangle index // is stored as a 16-bit unsigned integer. // // In 3D graphics, triangles are used because they are always planar (flat) and can // approximate any 3D surface when used in sufficient numbers. The triangles in this // format appear to reference vertices in the corresponding point arrays, allowing // the renderer to construct the 3D mesh. func (dec *Decoder) readTriangles(model *Model, count int, counts []uint16) error { // Process each array of triangles for i := 0; i < count; i++ { // Get the number of triangle indices in this array cnt := int(counts[i]) var triangles []uint16 // Read each triangle index for j := 0; j < cnt; j++ { n, err := dec.readUInt16() if err != nil { return err } triangles = append(triangles, n) } // Add the array of triangles to the model model.Triangles = append(model.Triangles, triangles) } return nil } // readTextureCoordinates reads the texture coordinate data from the .3db file. // Texture coordinates (also known as UV coordinates) define how 2D textures are mapped // onto the 3D model's surface. Each texture coordinate is a 2D point (u,v) that maps // a vertex of the 3D model to a specific point on the texture image. // // The function reads 'count' arrays of texture coordinates, where each array contains // a variable number of coordinates as specified in the 'counts' slice. Each coordinate // consists of two 32-bit floating point values (u,v) that range from 0.0 to 1.0, // representing relative positions on the texture image: // - u: Horizontal position (0.0 = left edge, 1.0 = right edge) // - v: Vertical position (0.0 = top edge, 1.0 = bottom edge) // // Texture mapping is a critical part of 3D rendering as it allows detailed 2D images // to be applied to 3D surfaces, giving models realistic appearance without requiring // extremely complex geometry. func (dec *Decoder) readTextureCoordinates(model *Model, count int, counts []uint16) error { // Process each array of texture coordinates for i := 0; i < count; i++ { // Get the number of texture coordinates in this array cnt := int(counts[i]) var cords []Coordinate // Read each texture coordinate (u,v pair) for j := 0; j < cnt; j++ { cord := Coordinate{} // Read the u coordinate (horizontal position on texture) u, err := dec.readFloat32() if err != nil { return err } // Read the v coordinate (vertical position on texture) v, err := dec.readFloat32() if err != nil { return err } // Set the coordinate values and add to the array cord.Set(u, v) cords = append(cords, cord) } // Add the array of texture coordinates to the model model.TextureCoordinates = append(model.TextureCoordinates, cords) } return nil } // readPoint reads the vertex data from the .3db file. // Points (or vertices) are the fundamental 3D coordinates that define the shape of the model. // Each point is a 3D vector with x, y, and z coordinates that specify its position in 3D space. // // The function reads 'count' arrays of points, where each array contains a variable // number of vertices as specified in the 'counts' slice. Interestingly, in the .3db format, // each coordinate is stored as a 16-bit unsigned integer (0-65535) rather than a floating point, // which is then normalized to a floating point value between 0.0 and 1.0 by dividing by 0xFFFF (65535). // // This compression technique reduces file size while maintaining reasonable precision. // The actual world-space coordinates are likely calculated by applying transformations // (scaling, rotation, translation) to these normalized coordinates during rendering. // // The normalization formula is: // // float_value = uint16_value / 65535.0 // // This gives a value in the range [0.0, 1.0] which can later be transformed to the // appropriate scale and position in the 3D world. func (dec *Decoder) readPoint(model *Model, count int, counts []uint16) error { // Process each array of points (vertices) for i := 0; i < count; i++ { // Get the number of points in this array cnt := int(counts[i]) var vectors []Vector // Read each point (3D vertex) for j := 0; j < cnt; j++ { vec := Vector{} // Read the x coordinate as a 16-bit unsigned integer ux, err := dec.readUInt16() if err != nil { return err } // Read the y coordinate as a 16-bit unsigned integer uy, err := dec.readUInt16() if err != nil { return err } // Read the z coordinate as a 16-bit unsigned integer uz, err := dec.readUInt16() if err != nil { return err } // Convert the integer coordinates to normalized floating point values [0.0, 1.0] // by dividing by the maximum 16-bit value (0xFFFF = 65535) vec.Set(float32(ux)/float32(0xffff), float32(uy)/float32(0xffff), float32(uz)/float32(0xffff)) vectors = append(vectors, vec) } // Add the array of points to the model model.Points = append(model.Points, vectors) } return nil } // readBrightness reads the brightness (lighting) data from the .3db file. // Brightness values represent the lighting or shading information for each vertex // in the model. This allows for pre-calculated lighting effects that don't need // to be computed at runtime, which was important for older 3D engines with limited // processing power. // // The function reads 'count' arrays of brightness values, where each array contains // a variable number of values as specified in the 'counts' slice. Each brightness // value is stored as an 8-bit unsigned integer (0-255), where: // - 0 represents complete darkness (black) // - 255 represents maximum brightness (white) // - Values in between represent varying levels of gray // // These brightness values are typically used during rendering to modulate the color // of each vertex, creating lighting effects like shadows, highlights, and ambient // occlusion. The values might be applied directly to vertex colors or used as // multipliers for texture colors. // // This approach to lighting was common in older 3D games where dynamic lighting // was computationally expensive, so pre-calculated lighting was stored in the model. func (dec *Decoder) readBrightness(model *Model, count int, counts []uint16) error { // Process each array of brightness values for i := 0; i < count; i++ { // Get the number of brightness values in this array cnt := int(counts[i]) var brightness []byte // Read each brightness value for j := 0; j < cnt; j++ { // Read the brightness as an 8-bit unsigned integer (0-255) b, err := dec.readUInt8() if err != nil { return err } brightness = append(brightness, b) } // Add the array of brightness values to the model model.Brightness = append(model.Brightness, brightness) } return nil }