modeler/write.go
// Package modeler implements helper methods to write common glTF entities
// (indices, positions, colors, ...) into buffers.
package modeler
import (
"bytes"
"errors"
"fmt"
"image/color"
"io"
"io/ioutil"
"math"
"reflect"
"github.com/qmuntal/gltf"
"github.com/qmuntal/gltf/binary"
)
// WriteIndices adds a new INDICES accessor to doc
// and fills the last buffer with data.
// If success it returns the index of the new accessor.
func WriteIndices(doc *gltf.Document, data any) uint32 {
switch data.(type) {
case []uint16, []uint32:
default:
panic(fmt.Sprintf("modeler.WriteIndices: invalid type %T", data))
}
return WriteAccessor(doc, gltf.TargetElementArrayBuffer, data)
}
// WriteNormal adds a new NORMAL accessor to doc
// and fills the last buffer with data.
// If success it returns the index of the new accessor.
func WriteNormal(doc *gltf.Document, data [][3]float32) uint32 {
return WriteAccessor(doc, gltf.TargetArrayBuffer, data)
}
// WriteTangent adds a new TANGENT accessor to doc
// and fills the last buffer with data.
// If success it returns the index of the new accessor.
func WriteTangent(doc *gltf.Document, data [][4]float32) uint32 {
return WriteAccessor(doc, gltf.TargetArrayBuffer, data)
}
// WriteTextureCoord adds a new TEXTURECOORD accessor to doc
// and fills the last buffer with data.
// If success it returns the index of the new accessor.
func WriteTextureCoord(doc *gltf.Document, data any) uint32 {
normalized := checkTextureCoord(data)
index := WriteAccessor(doc, gltf.TargetArrayBuffer, data)
doc.Accessors[index].Normalized = normalized
return index
}
func checkTextureCoord(data any) bool {
var normalized bool
switch data.(type) {
case [][2]uint8, [][2]uint16:
normalized = true
case [][2]float32:
default:
panic(fmt.Sprintf("modeler.WriteTextureCoord: invalid type %T", data))
}
return normalized
}
// WriteWeights adds a new WEIGHTS accessor to doc
// and fills the last buffer with data.
// If success it returns the index of the new accessor.
func WriteWeights(doc *gltf.Document, data any) uint32 {
normalized := checkWeights(data)
index := WriteAccessor(doc, gltf.TargetArrayBuffer, data)
doc.Accessors[index].Normalized = normalized
return index
}
func checkWeights(data any) bool {
var normalized bool
switch data.(type) {
case [][4]uint8, [][4]uint16:
normalized = true
case [][4]float32:
default:
panic(fmt.Sprintf("modeler.WriteWeights: invalid type %T", data))
}
return normalized
}
// WriteJoints adds a new JOINTS accessor to doc
// and fills the last buffer with data.
// If success it returns the index of the new accessor.
func WriteJoints(doc *gltf.Document, data any) uint32 {
checkJoints(data)
return WriteAccessor(doc, gltf.TargetArrayBuffer, data)
}
func checkJoints(data any) {
switch data.(type) {
case [][4]uint8, [][4]uint16:
default:
panic(fmt.Sprintf("modeler.WriteJoints: invalid type %T", data))
}
}
// WritePosition adds a new POSITION accessor to doc
// and fills the last buffer with data.
// If success it returns the index of the new accessor.
func WritePosition(doc *gltf.Document, data [][3]float32) uint32 {
index := WriteAccessor(doc, gltf.TargetArrayBuffer, data)
min, max := minMaxFloat32(data)
doc.Accessors[index].Min = min[:]
doc.Accessors[index].Max = max[:]
return index
}
func minMaxFloat32(data [][3]float32) ([3]float64, [3]float64) {
min := [3]float64{math.MaxFloat64, math.MaxFloat64, math.MaxFloat64}
max := [3]float64{-math.MaxFloat64, -math.MaxFloat64, -math.MaxFloat64}
for _, v := range data {
for i, x := range v {
min[i] = math.Min(float64(min[i]), float64(x))
max[i] = math.Max(float64(max[i]), float64(x))
}
}
return min, max
}
// WriteColor adds a new COLOR accessor to doc
// and fills the buffer with data.
// If success it returns the index of the new accessor.
func WriteColor(doc *gltf.Document, data any) uint32 {
normalized := checkColor(data)
index := WriteAccessor(doc, gltf.TargetArrayBuffer, data)
doc.Accessors[index].Normalized = normalized
return index
}
func checkColor(data any) bool {
var normalized bool
switch data.(type) {
case []color.RGBA, []color.RGBA64, [][4]uint8, [][3]uint8, [][4]uint16, [][3]uint16:
normalized = true
case [][3]float32, [][4]float32:
default:
panic(fmt.Sprintf("modeler.WriteColor: invalid type %T", data))
}
return normalized
}
// WriteImage adds a new image to doc
// and fills the buffer with the image data.
// If success it returns the index of the new image.
func WriteImage(doc *gltf.Document, name string, mimeType string, r io.Reader) (uint32, error) {
var data []byte
switch r := r.(type) {
case *bytes.Buffer:
data = r.Bytes()
default:
var err error
data, err = ioutil.ReadAll(r)
if err != nil {
return 0, err
}
}
index := WriteBufferView(doc, gltf.TargetNone, data)
doc.Images = append(doc.Images, &gltf.Image{
Name: name,
MimeType: mimeType,
BufferView: gltf.Index(index),
})
return uint32(len(doc.Images) - 1), nil
}
// WriteAccessor adds a new Accessor to doc
// and fills the buffer with the data.
// Returns the index of the new accessor.
func WriteAccessor(doc *gltf.Document, target gltf.Target, data any) uint32 {
ensurePadding(doc)
index := WriteBufferView(doc, target, data)
c, a, l := binary.Type(data)
doc.Accessors = append(doc.Accessors, &gltf.Accessor{
BufferView: gltf.Index(index),
ByteOffset: 0,
ComponentType: c,
Type: a,
Count: l,
})
return uint32(len(doc.Accessors) - 1)
}
// WriteAccessorsInterleaved adds as many accessors as
// elements in data all pointing to the same interleaved buffer view
// and fills the buffer with the data.
// Returns an slice with the indices of the newly created accessors,
// with the same order as data or an error if the data elements
// don´t have all the same length.
func WriteAccessorsInterleaved(doc *gltf.Document, data ...any) ([]uint32, error) {
ensurePadding(doc)
index, err := WriteBufferViewInterleaved(doc, data...)
if err != nil {
return nil, err
}
indices := make([]uint32, len(data))
var byteOffset uint32
for i, d := range data {
c, t, l := binary.Type(d)
doc.Accessors = append(doc.Accessors, &gltf.Accessor{
BufferView: gltf.Index(index),
ByteOffset: byteOffset,
ComponentType: c,
Type: t,
Count: l,
})
byteOffset += gltf.SizeOfElement(c, t)
indices[i] = uint32(len(doc.Accessors) - 1)
}
return indices, nil
}
// CustomAttribute defines an application-specific attribute
type CustomAttribute struct {
Name string
Data any
}
// Attributes defines all the vertex attributes that can
// be associated to a primitive.
type Attributes struct {
Position [][3]float32
Normal [][3]float32
Tangent [][4]float32
// [][2]uint8, [][2]uint16 or [][2]float32
TextureCoord_0, TextureCoord_1 any
// [][4]uint8, [][4]uint16 or [][4]float32
Weights any
// [][4]uint8 or [][4]uint16
Joints any
//[]color.RGBA, []color.RGBA64, [][4]uint8, [][3]uint8, [][4]uint16, [][3]uint16, [][3]float32 or [][4]float32
Color any
CustomAttributes []CustomAttribute
}
// WriteAttributesInterleaved write all the attributes in v
// which are not nil and have a non-zero length.
// Returns an attribute map that can be directly used
// as a primitive attributes.
func WriteAttributesInterleaved(doc *gltf.Document, v Attributes) (map[string]uint32, error) {
type attrProps struct {
Name string
Normalized bool
}
var (
props []attrProps
data []any
)
if len(v.Position) != 0 {
props = append(props, attrProps{Name: gltf.POSITION})
data = append(data, v.Position)
}
if len(v.Normal) != 0 {
props = append(props, attrProps{Name: gltf.NORMAL})
data = append(data, v.Normal)
}
if len(v.Tangent) != 0 {
props = append(props, attrProps{Name: gltf.TANGENT})
data = append(data, v.Tangent)
}
if sliceLength(v.TextureCoord_0) != 0 {
normalized := checkTextureCoord(v.TextureCoord_0)
props = append(props, attrProps{Name: gltf.TEXCOORD_0, Normalized: normalized})
data = append(data, v.TextureCoord_0)
}
if sliceLength(v.TextureCoord_1) != 0 {
normalized := checkTextureCoord(v.TextureCoord_1)
props = append(props, attrProps{Name: gltf.TEXCOORD_1, Normalized: normalized})
data = append(data, v.TextureCoord_1)
}
if sliceLength(v.Weights) != 0 {
normalized := checkWeights(v.Weights)
props = append(props, attrProps{Name: gltf.WEIGHTS_0, Normalized: normalized})
data = append(data, v.Weights)
}
if sliceLength(v.Joints) != 0 {
checkJoints(v.Joints)
props = append(props, attrProps{Name: gltf.JOINTS_0})
data = append(data, v.Joints)
}
if sliceLength(v.Color) != 0 {
normalized := checkColor(v.Color)
props = append(props, attrProps{Name: gltf.COLOR_0, Normalized: normalized})
data = append(data, v.Color)
}
for _, c := range v.CustomAttributes {
if sliceLength(c.Data) != 0 {
props = append(props, attrProps{Name: c.Name})
data = append(data, c.Data)
}
}
indices, err := WriteAccessorsInterleaved(doc, data...)
if err != nil {
return nil, err
}
attrs := make(map[string]uint32, len(props))
for i, index := range indices {
prop := props[i]
attrs[prop.Name] = index
doc.Accessors[index].Normalized = prop.Normalized
}
if pos, ok := attrs[gltf.POSITION]; ok {
min, max := minMaxFloat32(v.Position)
doc.Accessors[pos].Min = min[:]
doc.Accessors[pos].Max = max[:]
}
return attrs, nil
}
// WriteBufferViewInterleaved adds a new BufferView to doc
// and fills the buffer with one or more vertex attribute.
// If success it returns the index of the new buffer view.
// Returns the index of the new buffer view or an error if the data elements
// don´t have all the same length.
func WriteBufferViewInterleaved(doc *gltf.Document, data ...any) (uint32, error) {
return writeBufferViews(doc, gltf.TargetArrayBuffer, data...)
}
// WriteBufferView adds a new BufferView to doc
// and fills the buffer with the data.
// Returns the index of the new buffer view.
func WriteBufferView(doc *gltf.Document, target gltf.Target, data any) uint32 {
index, _ := writeBufferViews(doc, target, data)
return index
}
func writeBufferViews(doc *gltf.Document, target gltf.Target, data ...any) (uint32, error) {
var refLength, stride, size uint32
for i, d := range data {
c, a, l := binary.Type(d)
if i == 0 {
refLength = l
} else if refLength != l {
return 0, errors.New("go3mf: interleaved data shall have the same number of elements in all chunks")
}
sizeOfElement := gltf.SizeOfElement(c, a)
size += l * sizeOfElement
if len(data) > 1 {
stride += sizeOfElement
} else if target == gltf.TargetArrayBuffer && c.ByteSize()*a.Components() != sizeOfElement {
stride = sizeOfElement
}
}
buffer := lastBuffer(doc)
offset := uint32(len(buffer.Data))
buffer.ByteLength += size
buffer.Data = append(buffer.Data, make([]byte, size)...)
dataOffset := offset
for _, d := range data {
// Cannot return error as the buffer has enough size and the data type is controlled.
_ = binary.Write(buffer.Data[dataOffset:], stride, d)
c, a, _ := binary.Type(d)
dataOffset += gltf.SizeOfElement(c, a)
}
bufferView := &gltf.BufferView{
Buffer: uint32(len(doc.Buffers)) - 1,
ByteLength: size,
ByteOffset: offset,
ByteStride: stride,
Target: target,
}
doc.BufferViews = append(doc.BufferViews, bufferView)
return uint32(len(doc.BufferViews)) - 1, nil
}
func ensurePadding(doc *gltf.Document) {
buffer := lastBuffer(doc)
padding := getPadding(uint32(len(buffer.Data)))
buffer.Data = append(buffer.Data, make([]byte, padding)...)
buffer.ByteLength += padding
}
func lastBuffer(doc *gltf.Document) *gltf.Buffer {
if len(doc.Buffers) == 0 {
doc.Buffers = append(doc.Buffers, new(gltf.Buffer))
}
return doc.Buffers[len(doc.Buffers)-1]
}
func getPadding(offset uint32) uint32 {
padAlign := offset % 4
if padAlign == 0 {
return 0
}
return 4 - padAlign
}
func sliceLength(data any) int {
if data == nil {
return 0
}
v := reflect.ValueOf(data)
if v.IsNil() {
return 0
}
if v.Kind() != reflect.Slice {
panic(fmt.Sprintf("go3mf: expecting a slice but got %s", v.Kind()))
}
return v.Len()
}