qmuntal/gltf

View on GitHub
gltf.go

Summary

Maintainability
A
0 mins
Test Coverage
package gltf

import (
    "encoding/base64"
    "errors"
    "strings"
    "sync"
)

// Index is an utility function that returns a pointer to a uint32.
func Index(i uint32) *uint32 {
    return &i
}

// Float is an utility function that returns a pointer to a float64.
func Float(val float64) *float64 {
    return &val
}

// An Asset is metadata about the glTF asset.
type Asset struct {
    Extensions Extensions `json:"extensions,omitempty"`
    Extras     any        `json:"extras,omitempty"`
    Copyright  string     `json:"copyright,omitempty"`         // A copyright message suitable for display to credit the content creator.
    Generator  string     `json:"generator,omitempty"`         // Tool that generated this glTF model. Useful for debugging.
    Version    string     `json:"version" validate:"required"` // The glTF version that this asset targets.
    MinVersion string     `json:"minVersion,omitempty"`        // The minimum glTF version that this asset targets.
}

// Document defines the root object for a glTF asset.
type Document struct {
    Extensions         Extensions    `json:"extensions,omitempty"`
    Extras             any           `json:"extras,omitempty"`
    ExtensionsUsed     []string      `json:"extensionsUsed,omitempty"`
    ExtensionsRequired []string      `json:"extensionsRequired,omitempty"`
    Accessors          []*Accessor   `json:"accessors,omitempty" validate:"dive"`
    Animations         []*Animation  `json:"animations,omitempty" validate:"dive"`
    Asset              Asset         `json:"asset"`
    Buffers            []*Buffer     `json:"buffers,omitempty" validate:"dive"`
    BufferViews        []*BufferView `json:"bufferViews,omitempty" validate:"dive"`
    Cameras            []*Camera     `json:"cameras,omitempty" validate:"dive"`
    Images             []*Image      `json:"images,omitempty" validate:"dive"`
    Materials          []*Material   `json:"materials,omitempty" validate:"dive"`
    Meshes             []*Mesh       `json:"meshes,omitempty" validate:"dive"`
    Nodes              []*Node       `json:"nodes,omitempty" validate:"dive"`
    Samplers           []*Sampler    `json:"samplers,omitempty" validate:"dive"`
    Scene              *uint32       `json:"scene,omitempty"`
    Scenes             []*Scene      `json:"scenes,omitempty" validate:"dive"`
    Skins              []*Skin       `json:"skins,omitempty" validate:"dive"`
    Textures           []*Texture    `json:"textures,omitempty" validate:"dive"`
}

// NewDocument returns a new Document with sane defaults.
func NewDocument() *Document {
    return &Document{
        Scene:  Index(0),
        Scenes: []*Scene{{Name: "Root Scene"}},
        Asset: Asset{
            Generator: "qmuntal/gltf",
            Version:   "2.0",
        },
    }
}

// An Accessor is a typed view into a bufferView.
// An accessor provides a typed view into a bufferView or a subset of a bufferView
// similar to how WebGL's vertexAttribPointer() defines an attribute in a buffer.
type Accessor struct {
    Extensions    Extensions    `json:"extensions,omitempty"`
    Extras        any           `json:"extras,omitempty"`
    Name          string        `json:"name,omitempty"`
    BufferView    *uint32       `json:"bufferView,omitempty"`
    ByteOffset    uint32        `json:"byteOffset,omitempty"`
    ComponentType ComponentType `json:"componentType" validate:"lte=5"`
    Normalized    bool          `json:"normalized,omitempty"`      // Specifies whether integer data values should be normalized.
    Count         uint32        `json:"count" validate:"required"` // The number of attributes referenced by this accessor.
    Type          AccessorType  `json:"type" validate:"lte=6"`
    Max           []float64     `json:"max,omitempty" validate:"omitempty,lte=16"` // Maximum value of each component in this attribute.
    Min           []float64     `json:"min,omitempty" validate:"omitempty,lte=16"` // Minimum value of each component in this attribute.
    Sparse        *Sparse       `json:"sparse,omitempty"`                          // Sparse storage of attributes that deviate from their initialization value.
}

// Sparse storage of attributes that deviate from their initialization value.
type Sparse struct {
    Extensions Extensions    `json:"extensions,omitempty"`
    Extras     any           `json:"extras,omitempty"`
    Count      uint32        `json:"count" validate:"gte=1"` // Number of entries stored in the sparse array.
    Indices    SparseIndices `json:"indices"`                // Index array of size count that points to those accessor attributes that deviate from their initialization value.
    Values     SparseValues  `json:"values"`                 // Array of size count times number of components, storing the displaced accessor attributes pointed by indices.
}

// SparseValues stores the displaced accessor attributes pointed by accessor.sparse.indices.
type SparseValues struct {
    Extensions Extensions `json:"extensions,omitempty"`
    Extras     any        `json:"extras,omitempty"`
    BufferView uint32     `json:"bufferView"`
    ByteOffset uint32     `json:"byteOffset,omitempty"`
}

// SparseIndices defines the indices of those attributes that deviate from their initialization value.
type SparseIndices struct {
    Extensions    Extensions    `json:"extensions,omitempty"`
    Extras        any           `json:"extras,omitempty"`
    BufferView    uint32        `json:"bufferView"`
    ByteOffset    uint32        `json:"byteOffset,omitempty"`
    ComponentType ComponentType `json:"componentType" validate:"oneof=2 4 5"`
}

// A Buffer points to binary geometry, animation, or skins.
// If Data length is 0 and the Buffer is an external resource the Data won't be flushed,
// which can be useful when there is no need to load data in memory.
type Buffer struct {
    Extensions Extensions `json:"extensions,omitempty"`
    Extras     any        `json:"extras,omitempty"`
    Name       string     `json:"name,omitempty"`
    URI        string     `json:"uri,omitempty" validate:"omitempty"`
    ByteLength uint32     `json:"byteLength" validate:"required"`
    Data       []byte     `json:"-"`
}

// IsEmbeddedResource returns true if the buffer points to an embedded resource.
func (b *Buffer) IsEmbeddedResource() bool {
    return strings.HasPrefix(b.URI, mimetypeApplicationOctet)
}

// EmbeddedResource defines the buffer as an embedded resource and encodes the URI so it points to the the resource.
func (b *Buffer) EmbeddedResource() {
    b.URI = mimetypeApplicationOctet + "," + base64.StdEncoding.EncodeToString(b.Data)
}

// marshalData decode the buffer from the URI. If the buffer is not en embedded resource the returned array will be empty.
func (b *Buffer) marshalData() ([]byte, error) {
    if !b.IsEmbeddedResource() {
        return nil, nil
    }
    startPos := len(mimetypeApplicationOctet) + 1
    if len(b.URI) < startPos {
        return nil, errors.New("gltf: Invalid base64 content")
    }
    sl, err := base64.StdEncoding.DecodeString(b.URI[startPos:])
    if len(sl) == 0 || err != nil {
        return nil, err
    }
    return sl, nil
}

// BufferView is a view into a buffer generally representing a subset of the buffer.
type BufferView struct {
    Extensions Extensions `json:"extensions,omitempty"`
    Extras     any        `json:"extras,omitempty"`
    Buffer     uint32     `json:"buffer"`
    ByteOffset uint32     `json:"byteOffset,omitempty"`
    ByteLength uint32     `json:"byteLength" validate:"required"`
    ByteStride uint32     `json:"byteStride,omitempty" validate:"omitempty,gte=4,lte=252"`
    Target     Target     `json:"target,omitempty" validate:"omitempty,oneof=34962 34963"`
    Name       string     `json:"name,omitempty"`
}

// The Scene contains a list of root nodes.
type Scene struct {
    Extensions Extensions `json:"extensions,omitempty"`
    Extras     any        `json:"extras,omitempty"`
    Name       string     `json:"name,omitempty"`
    Nodes      []uint32   `json:"nodes,omitempty" validate:"omitempty,unique"`
}

// A Node in the node hierarchy.
// It can have either a matrix or any combination of translation/rotation/scale (TRS) properties.
type Node struct {
    Extensions  Extensions  `json:"extensions,omitempty"`
    Extras      any         `json:"extras,omitempty"`
    Name        string      `json:"name,omitempty"`
    Camera      *uint32     `json:"camera,omitempty"`
    Children    []uint32    `json:"children,omitempty" validate:"omitempty,unique"`
    Skin        *uint32     `json:"skin,omitempty"`
    Matrix      [16]float64 `json:"matrix"` // A 4x4 transformation matrix stored in column-major order.
    Mesh        *uint32     `json:"mesh,omitempty"`
    Rotation    [4]float64  `json:"rotation" validate:"omitempty,dive,gte=-1,lte=1"` // The node's unit quaternion rotation in the order (x, y, z, w), where w is the scalar.
    Scale       [3]float64  `json:"scale"`
    Translation [3]float64  `json:"translation"`
    Weights     []float64   `json:"weights,omitempty"` // The weights of the instantiated Morph Target.
}

// MatrixOrDefault returns the node matrix if it represents a valid affine matrix, else return the default one.
func (n *Node) MatrixOrDefault() [16]float64 {
    if n.Matrix == emptyMatrix {
        return DefaultMatrix
    }
    return n.Matrix
}

// RotationOrDefault returns the node rotation if it represents a valid quaternion, else return the default one.
func (n *Node) RotationOrDefault() [4]float64 {
    if n.Rotation == emptyRotation {
        return DefaultRotation
    }
    return n.Rotation
}

// ScaleOrDefault returns the node scale if it represents a valid scale factor, else return the default one.
func (n *Node) ScaleOrDefault() [3]float64 {
    if n.Scale == emptyScale {
        return DefaultScale
    }
    return n.Scale
}

// TranslationOrDefault returns the node translation.
func (n *Node) TranslationOrDefault() [3]float64 {
    return n.Translation
}

// Skin defines joints and matrices.
type Skin struct {
    Extensions          Extensions `json:"extensions,omitempty"`
    Extras              any        `json:"extras,omitempty"`
    Name                string     `json:"name,omitempty"`
    InverseBindMatrices *uint32    `json:"inverseBindMatrices,omitempty"`      // The index of the accessor containing the floating-point 4x4 inverse-bind matrices.
    Skeleton            *uint32    `json:"skeleton,omitempty"`                 // The index of the node used as a skeleton root. When undefined, joints transforms resolve to scene root.
    Joints              []uint32   `json:"joints" validate:"omitempty,unique"` // Indices of skeleton nodes, used as joints in this skin.
}

// A Camera projection. A node can reference a camera to apply a transform to place the camera in the scene.
type Camera struct {
    Extensions   Extensions    `json:"extensions,omitempty"`
    Extras       any           `json:"extras,omitempty"`
    Name         string        `json:"name,omitempty"`
    Orthographic *Orthographic `json:"orthographic,omitempty"`
    Perspective  *Perspective  `json:"perspective,omitempty"`
}

// Orthographic camera containing properties to create an orthographic projection matrix.
type Orthographic struct {
    Extensions Extensions `json:"extensions,omitempty"`
    Extras     any        `json:"extras,omitempty"`
    Xmag       float64    `json:"xmag"`                               // The horizontal magnification of the view.
    Ymag       float64    `json:"ymag"`                               // The vertical magnification of the view.
    Zfar       float64    `json:"zfar" validate:"gt=0,gtfield=Znear"` // The distance to the far clipping plane.
    Znear      float64    `json:"znear" validate:"gte=0"`             // The distance to the near clipping plane.
}

// Perspective camera containing properties to create a perspective projection matrix.
type Perspective struct {
    Extensions  Extensions `json:"extensions,omitempty"`
    Extras      any        `json:"extras,omitempty"`
    AspectRatio *float64   `json:"aspectRatio,omitempty"`
    Yfov        float64    `json:"yfov"`           // The vertical field of view in radians.
    Zfar        *float64   `json:"zfar,omitempty"` // The distance to the far clipping plane.
    Znear       float64    `json:"znear"`          // The distance to the near clipping plane.
}

// A Mesh is a set of primitives to be rendered. A node can contain one mesh. A node's transform places the mesh in the scene.
type Mesh struct {
    Extensions Extensions   `json:"extensions,omitempty"`
    Extras     any          `json:"extras,omitempty"`
    Name       string       `json:"name,omitempty"`
    Primitives []*Primitive `json:"primitives" validate:"required,gt=0,dive"`
    Weights    []float64    `json:"weights,omitempty"`
}

// Primitive defines the geometry to be rendered with the given material.
type Primitive struct {
    Extensions Extensions    `json:"extensions,omitempty"`
    Extras     any           `json:"extras,omitempty"`
    Attributes Attribute     `json:"attributes"`
    Indices    *uint32       `json:"indices,omitempty"` // The index of the accessor that contains the indices.
    Material   *uint32       `json:"material,omitempty"`
    Mode       PrimitiveMode `json:"mode,omitempty" validate:"lte=6"`
    Targets    []Attribute   `json:"targets,omitempty" validate:"omitempty,dive,dive,keys,oneof=POSITION NORMAL TANGENT,endkeys"` // Only POSITION, NORMAL, and TANGENT supported.
}

// The Material appearance of a primitive.
type Material struct {
    Extensions           Extensions            `json:"extensions,omitempty"`
    Extras               any                   `json:"extras,omitempty"`
    Name                 string                `json:"name,omitempty"`
    PBRMetallicRoughness *PBRMetallicRoughness `json:"pbrMetallicRoughness,omitempty"`
    NormalTexture        *NormalTexture        `json:"normalTexture,omitempty"`
    OcclusionTexture     *OcclusionTexture     `json:"occlusionTexture,omitempty"`
    EmissiveTexture      *TextureInfo          `json:"emissiveTexture,omitempty"`
    EmissiveFactor       [3]float64            `json:"emissiveFactor,omitempty" validate:"dive,gte=0,lte=1"`
    AlphaMode            AlphaMode             `json:"alphaMode,omitempty" validate:"lte=2"`
    AlphaCutoff          *float64              `json:"alphaCutoff,omitempty" validate:"omitempty,gte=0"`
    DoubleSided          bool                  `json:"doubleSided,omitempty"`
}

// AlphaCutoffOrDefault returns the scale if it is not nil, else return the default one.
func (m *Material) AlphaCutoffOrDefault() float64 {
    if m.AlphaCutoff == nil {
        return 0.5
    }
    return *m.AlphaCutoff
}

// A NormalTexture references to a normal texture.
type NormalTexture struct {
    Extensions Extensions `json:"extensions,omitempty"`
    Extras     any        `json:"extras,omitempty"`
    Index      *uint32    `json:"index,omitempty"`
    TexCoord   uint32     `json:"texCoord,omitempty"` // The index of texture's TEXCOORD attribute used for texture coordinate mapping.
    Scale      *float64   `json:"scale,omitempty"`
}

// ScaleOrDefault returns the scale if it is not nil, else return the default one.
func (n *NormalTexture) ScaleOrDefault() float64 {
    if n.Scale == nil {
        return 1
    }
    return *n.Scale
}

// An OcclusionTexture references to an occlusion texture
type OcclusionTexture struct {
    Extensions Extensions `json:"extensions,omitempty"`
    Extras     any        `json:"extras,omitempty"`
    Index      *uint32    `json:"index,omitempty"`
    TexCoord   uint32     `json:"texCoord,omitempty"` // The index of texture's TEXCOORD attribute used for texture coordinate mapping.
    Strength   *float64   `json:"strength,omitempty" validate:"omitempty,gte=0,lte=1"`
}

// StrengthOrDefault returns the strength if it is not nil, else return the default one.
func (o *OcclusionTexture) StrengthOrDefault() float64 {
    if o.Strength == nil {
        return 1
    }
    return *o.Strength
}

// PBRMetallicRoughness defines a set of parameter values that are used to define the metallic-roughness material model from Physically-Based Rendering (PBR) methodology.
type PBRMetallicRoughness struct {
    Extensions               Extensions   `json:"extensions,omitempty"`
    Extras                   any          `json:"extras,omitempty"`
    BaseColorFactor          *[4]float64  `json:"baseColorFactor,omitempty" validate:"omitempty,dive,gte=0,lte=1"`
    BaseColorTexture         *TextureInfo `json:"baseColorTexture,omitempty"`
    MetallicFactor           *float64     `json:"metallicFactor,omitempty" validate:"omitempty,gte=0,lte=1"`
    RoughnessFactor          *float64     `json:"roughnessFactor,omitempty" validate:"omitempty,gte=0,lte=1"`
    MetallicRoughnessTexture *TextureInfo `json:"metallicRoughnessTexture,omitempty"`
}

// MetallicFactorOrDefault returns the metallic factor if it is not nil, else return the default one.
func (p *PBRMetallicRoughness) MetallicFactorOrDefault() float64 {
    if p.MetallicFactor == nil {
        return 1
    }
    return *p.MetallicFactor
}

// RoughnessFactorOrDefault returns the roughness factor if it is not nil, else return the default one.
func (p *PBRMetallicRoughness) RoughnessFactorOrDefault() float64 {
    if p.RoughnessFactor == nil {
        return 1
    }
    return *p.RoughnessFactor
}

// BaseColorFactorOrDefault returns the base color factor if it is not nil, else return the default one.
func (p *PBRMetallicRoughness) BaseColorFactorOrDefault() [4]float64 {
    if p.BaseColorFactor == nil {
        return [4]float64{1, 1, 1, 1}
    }
    return *p.BaseColorFactor
}

// TextureInfo references to a texture.
type TextureInfo struct {
    Extensions Extensions `json:"extensions,omitempty"`
    Extras     any        `json:"extras,omitempty"`
    Index      uint32     `json:"index"`
    TexCoord   uint32     `json:"texCoord,omitempty"` // The index of texture's TEXCOORD attribute used for texture coordinate mapping.
}

// A Texture and its sampler.
type Texture struct {
    Extensions Extensions `json:"extensions,omitempty"`
    Extras     any        `json:"extras,omitempty"`
    Name       string     `json:"name,omitempty"`
    Sampler    *uint32    `json:"sampler,omitempty"`
    Source     *uint32    `json:"source,omitempty"`
}

// Sampler of a texture for filtering and wrapping modes.
type Sampler struct {
    Extensions Extensions   `json:"extensions,omitempty"`
    Extras     any          `json:"extras,omitempty"`
    Name       string       `json:"name,omitempty"`
    MagFilter  MagFilter    `json:"magFilter,omitempty" validate:"lte=1"`
    MinFilter  MinFilter    `json:"minFilter,omitempty" validate:"lte=5"`
    WrapS      WrappingMode `json:"wrapS,omitempty" validate:"lte=2"`
    WrapT      WrappingMode `json:"wrapT,omitempty" validate:"lte=2"`
}

// Image data used to create a texture. Image can be referenced by URI or bufferView index.
// mimeType is required in the latter case.
type Image struct {
    Extensions Extensions `json:"extensions,omitempty"`
    Extras     any        `json:"extras,omitempty"`
    Name       string     `json:"name,omitempty"`
    URI        string     `json:"uri,omitempty" validate:"omitempty"`
    MimeType   string     `json:"mimeType,omitempty" validate:"omitempty,oneof=image/jpeg image/png"` // Manadatory if BufferView is defined.
    BufferView *uint32    `json:"bufferView,omitempty"`                                               // Use this instead of the image's uri property.
}

// IsEmbeddedResource returns true if the buffer points to an embedded resource.
func (im *Image) IsEmbeddedResource() bool {
    return strings.HasPrefix(im.URI, mimetypeImagePNG) || strings.HasPrefix(im.URI, mimetypeImageJPG)
}

// MarshalData decode the image from the URI. If the image is not en embedded resource the returned array will be empty.
func (im *Image) MarshalData() ([]byte, error) {
    if !im.IsEmbeddedResource() {
        return []byte{}, nil
    }
    mimetype := mimetypeImagePNG
    if strings.HasPrefix(im.URI, mimetypeImageJPG) {
        mimetype = mimetypeImageJPG
    }
    startPos := len(mimetype) + 1
    if len(im.URI) < startPos {
        return []byte{}, errors.New("gltf: Invalid base64 content")
    }
    return base64.StdEncoding.DecodeString(im.URI[startPos:])
}

// An Animation keyframe.
type Animation struct {
    Extensions Extensions          `json:"extensions,omitempty"`
    Extras     any                 `json:"extras,omitempty"`
    Name       string              `json:"name,omitempty"`
    Channels   []*Channel          `json:"channels" validate:"required,gt=0,dive"`
    Samplers   []*AnimationSampler `json:"samplers" validate:"required,gt=0,dive"`
}

// AnimationSampler combines input and output accessors with an interpolation algorithm to define a keyframe graph (but not its target).
type AnimationSampler struct {
    Extensions    Extensions    `json:"extensions,omitempty"`
    Extras        any           `json:"extras,omitempty"`
    Input         uint32        `json:"input"` // The index of an accessor containing keyframe input values.
    Interpolation Interpolation `json:"interpolation,omitempty" validate:"lte=2"`
    Output        uint32        `json:"output"` // The index of an accessor containing keyframe output values.
}

// The Channel targets an animation's sampler at a node's property.
type Channel struct {
    Extensions Extensions    `json:"extensions,omitempty"`
    Extras     any           `json:"extras,omitempty"`
    Sampler    *uint32       `json:"sampler,omitempty"`
    Target     ChannelTarget `json:"target"`
}

// ChannelTarget describes the index of the node and TRS property that an animation channel targets.
// The Path represents the name of the node's TRS property to modify, or the "weights" of the Morph Targets it instantiates.
// For the "translation" property, the values that are provided by the sampler are the translation along the x, y, and z axes.
// For the "rotation" property, the values are a quaternion in the order (x, y, z, w), where w is the scalar.
// For the "scale" property, the values are the scaling factors along the x, y, and z axes.
type ChannelTarget struct {
    Extensions Extensions  `json:"extensions,omitempty"`
    Extras     any         `json:"extras,omitempty"`
    Node       *uint32     `json:"node,omitempty"`
    Path       TRSProperty `json:"path" validate:"lte=4"`
}

// Extensions is map where the keys are the extension identifiers and the values are the extensions payloads.
// If a key matches with one of the supported extensions the value will be marshalled as a pointer to the extension struct.
// If a key does not match with any of the supported extensions the value will be a json.RawMessage so its decoding can be delayed.
type Extensions map[string]any

var (
    extMu      sync.RWMutex
    extensions = make(map[string]func([]byte) (any, error))
)

// RegisterExtension registers a function that returns a new extension of the given
// byte array. This is intended to be called from the init function in
// packages that implement extensions.
func RegisterExtension(key string, f func([]byte) (any, error)) {
    extMu.Lock()
    defer extMu.Unlock()
    extensions[key] = f
}

func queryExtension(key string) (func([]byte) (any, error), bool) {
    extMu.RLock()
    ext, ok := extensions[key]
    extMu.RUnlock()
    return ext, ok
}

// SizeOfElement returns the size, in bytes, of an element.
// The element size may not be (component size) * (number of components),
// as some of the elements are tightly packed in order to ensure
// that they are aligned to 4-byte boundaries.
func SizeOfElement(c ComponentType, t AccessorType) uint32 {
    // special cases
    switch {
    case (t == AccessorVec3 || t == AccessorVec2) && (c == ComponentByte || c == ComponentUbyte):
        return 4
    case t == AccessorVec3 && (c == ComponentShort || c == ComponentUshort):
        return 8
    case t == AccessorMat2 && (c == ComponentByte || c == ComponentUbyte):
        return 8
    case t == AccessorMat3 && (c == ComponentByte || c == ComponentUbyte):
        return 12
    case t == AccessorMat3 && (c == ComponentShort || c == ComponentUshort):
        return 24
    }
    return c.ByteSize() * t.Components()
}