2025-08-06 16:09:32 +03:00

774 lines
25 KiB
Go

// 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
}