Repository: qmuntal/gltf
Pull request #20 (2019-11-26 17:52:50)

Add gltf/modeler package


Patch:
diff --git a/.sonarcloud.properties b/.sonarcloud.properties
deleted file mode 100644
index 8b13789..0000000
--- a/.sonarcloud.properties
+++ /dev/null
@@ -1,1 +0,0 @@
-
diff --git a/README.md b/README.md
index ab16d10..c1ad72b 100644
--- a/README.md
+++ b/README.md
@@ -17,6 +17,7 @@ A Go package for simple, efficient, and robust serialization/deserialization of
   * [x] ASCII glTF
   * [x] Binary glTF(GLB)
   * [x] PBR material description
+  * [x] Modeler package
 * glTF validaton
   * [ ] Validate against schemas
   * [ ] Validate coherence
@@ -82,7 +83,42 @@ if err != nil {
 fmt.Print(doc.Asset)
 ```
 
-### Save
+### Create a glb using gltf/modeler
+
+```go
+package main
+
+import (
+ "github.com/qmuntal/gltf"
+ "github.com/qmuntal/gltf/modeler"
+)
+
+func main() {
+  m := modeler.NewModeler()
+  positionAccessor := m.AddPosition(0, [][3]float32{{43, 43, 0}, {83, 43, 0}, {63, 63, 40}, {43, 83, 0}, {83, 83, 0}})
+  indicesAccessor := m.AddIndices(0, []uint8{0, 1, 2, 3, 1, 0, 0, 2, 3, 1, 4, 2, 4, 3, 2, 4, 1, 3})
+  colorIndices := m.AddColor(0, [][3]uint8{{50, 155, 255}, {0, 100, 200}, {255, 155, 50}, {155, 155, 155}, {25, 25, 25}})
+  m.Document.Meshes = []gltf.Mesh{{
+    Name: "Pyramid",
+    Primitives: []gltf.Primitive{
+      {
+        Indices: gltf.Index(indicesAccessor),
+        Attributes: map[string]uint32{
+          "POSITION": positionAccessor,
+          "COLOR_0":  colorIndices,
+        },
+      },
+    },
+  }}
+  m.Nodes = []gltf.Node{{Name: "Root", Mesh: gltf.Index(0)}}
+  m.Scenes[0].Nodes = append(m.Scenes[0].Nodes, 0)
+  if err := gltf.SaveBinary(m.Document, "./example.glb"); err != nil {
+    panic(err)
+  }
+}
+```
+
+### Create a glb using raw data
 
 The following example generates a 3D box with colors per vertex.
 
diff --git a/binary/encode.go b/binary/encode.go
index fdc1459..5330683 100644
--- a/binary/encode.go
+++ b/binary/encode.go
@@ -3,7 +3,10 @@ package binary
 import (
 	"bytes"
 	"encoding/binary"
+	"image/color"
 	"io"
+
+	"github.com/qmuntal/gltf"
 )
 
 // Read reads structured binary data from r into data.
@@ -12,14 +15,35 @@ import (
 //
 // If data length is greater than the length of b, Read returns io.ErrShortBuffer.
 func Read(b []byte, data interface{}) error {
-	e, n := intDataSize(data)
-	if e == 0 {
+	c, t, n := Type(data)
+	if n <= 0 {
 		return binary.Read(bytes.NewReader(b), binary.LittleEndian, data)
 	}
+	e := SizeOfElement(c, t)
 	if len(b) < n*e {
 		return io.ErrShortBuffer
 	}
 	switch data := data.(type) {
+	case []color.RGBA:
+		for i := range data {
+			c := UnsignedByte.Vec4(b[e*i:])
+			data[i] = color.RGBA{R: c[0], G: c[1], B: c[2], A: c[3]}
+		}
+	case []color.RGBA64:
+		for i := range data {
+			c := UnsignedShort.Vec4(b[e*i:])
+			data[i] = color.RGBA64{R: c[0], G: c[1], B: c[2], A: c[3]}
+		}
+	case []gltf.RGBA:
+		for i := range data {
+			c := Float.Vec4(b[e*i:])
+			data[i] = gltf.RGBA{R: float64(c[0]), G: float64(c[1]), B: float64(c[2]), A: float64(c[3])}
+		}
+	case []gltf.RGB:
+		for i := range data {
+			c := Float.Vec3(b[e*i:])
+			data[i] = gltf.RGB{R: float64(c[0]), G: float64(c[1]), B: float64(c[2])}
+		}
 	case []int8:
 		for i, x := range b {
 			data[i] = int8(x)
@@ -194,14 +218,31 @@ func Read(b []byte, data interface{}) error {
 // Data must be a slice of glTF predefined fixed-size types,
 // else it fallbacks to `encoding/binary.Write`.
 func Write(b []byte, data interface{}) error {
-	e, n := intDataSize(data)
-	if e == 0 {
+	c, t, n := Type(data)
+	if n <= 0 {
 		return binary.Write(bytes.NewBuffer(b), binary.LittleEndian, data)
 	}
+	e := SizeOfElement(c, t)
 	if len(b) < e*n {
 		return io.ErrShortBuffer
 	}
 	switch data := data.(type) {
+	case []color.RGBA:
+		for i, x := range data {
+			UnsignedByte.PutVec4(b[e*i:], [4]uint8{x.R, x.G, x.B, x.A})
+		}
+	case []color.RGBA64:
+		for i, x := range data {
+			UnsignedShort.PutVec4(b[e*i:], [4]uint16{x.R, x.G, x.B, x.A})
+		}
+	case []gltf.RGBA:
+		for i, x := range data {
+			Float.PutVec4(b[e*i:], [4]float32{float32(x.R), float32(x.G), float32(x.B), float32(x.A)})
+		}
+	case []gltf.RGB:
+		for i, x := range data {
+			Float.PutVec3(b[e*i:], [3]float32{float32(x.R), float32(x.G), float32(x.B)})
+		}
 	case []int8:
 		for i, x := range data {
 			b[i] = byte(x)
diff --git a/binary/size.go b/binary/size.go
index 1359eb3..72d3364 100644
--- a/binary/size.go
+++ b/binary/size.go
@@ -1,6 +1,11 @@
 package binary
 
-import "github.com/qmuntal/gltf"
+import (
+	"image/color"
+	"reflect"
+
+	"github.com/qmuntal/gltf"
+)
 
 // SizeOf returns the size, in bytes, of a component type.
 func SizeOf(c gltf.ComponentType) int {
@@ -28,9 +33,16 @@ func ComponentsOf(t gltf.AccessorType) int {
 }
 
 // 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 gltf.ComponentType, t gltf.AccessorType) int {
 	// special cases
 	switch {
+	case (t == gltf.Vec3 || t == gltf.Vec2) && (c == gltf.Byte || c == gltf.UnsignedByte):
+		return 4
+	case t == gltf.Vec3 && (c == gltf.Short || c == gltf.UnsignedShort):
+		return 8
 	case t == gltf.Mat2 && (c == gltf.Byte || c == gltf.UnsignedByte):
 		return 8
 	case t == gltf.Mat3 && (c == gltf.Byte || c == gltf.UnsignedByte):
@@ -41,137 +53,102 @@ func SizeOfElement(c gltf.ComponentType, t gltf.AccessorType) int {
 	return SizeOf(c) * ComponentsOf(t)
 }
 
-// intDataSize returns the size of the data required to represent the data when encoded.
-// It returns zero if the type cannot be implemented by the fast path in Read or Write.
-func intDataSize(data interface{}) (element int, total int) {
-	var length int
-	switch data := data.(type) {
-	case []int8:
-		element = SizeOfElement(gltf.Byte, gltf.Scalar)
-		length = len(data)
-	case [][2]int8:
-		element = SizeOfElement(gltf.Byte, gltf.Vec2)
-		length = len(data)
-	case [][3]int8:
-		element = SizeOfElement(gltf.Byte, gltf.Vec3)
-		length = len(data)
-	case [][4]int8:
-		element = SizeOfElement(gltf.Byte, gltf.Vec4)
-		length = len(data)
-	case [][2][2]int8:
-		element = SizeOfElement(gltf.Byte, gltf.Mat2)
-		length = len(data)
-	case [][3][3]int8:
-		element = SizeOfElement(gltf.Byte, gltf.Mat3)
-		length = len(data)
-	case [][4][4]int8:
-		element = SizeOfElement(gltf.Byte, gltf.Mat4)
-		length = len(data)
-	case []uint8:
-		element = SizeOfElement(gltf.UnsignedByte, gltf.Scalar)
-		length = len(data)
-	case [][2]uint8:
-		element = SizeOfElement(gltf.UnsignedByte, gltf.Vec2)
-		length = len(data)
-	case [][3]uint8:
-		element = SizeOfElement(gltf.UnsignedByte, gltf.Vec3)
-		length = len(data)
-	case [][4]uint8:
-		element = SizeOfElement(gltf.UnsignedByte, gltf.Vec4)
-		length = len(data)
-	case [][2][2]uint8:
-		element = SizeOfElement(gltf.UnsignedByte, gltf.Mat2)
-		length = len(data)
-	case [][3][3]uint8:
-		element = SizeOfElement(gltf.UnsignedByte, gltf.Mat3)
-		length = len(data)
-	case [][4][4]uint8:
-		element = SizeOfElement(gltf.UnsignedByte, gltf.Mat4)
-		length = len(data)
-	case []int16:
-		element = SizeOfElement(gltf.Short, gltf.Scalar)
-		length = len(data)
-	case [][2]int16:
-		element = SizeOfElement(gltf.Short, gltf.Vec2)
-		length = len(data)
-	case [][3]int16:
-		element = SizeOfElement(gltf.Short, gltf.Vec3)
-		length = len(data)
-	case [][4]int16:
-		element = SizeOfElement(gltf.Short, gltf.Vec4)
-		length = len(data)
-	case [][2][2]int16:
-		element = SizeOfElement(gltf.Short, gltf.Mat2)
-		length = len(data)
-	case [][3][3]int16:
-		element = SizeOfElement(gltf.Short, gltf.Mat3)
-		length = len(data)
-	case [][4][4]int16:
-		element = SizeOfElement(gltf.Short, gltf.Mat4)
-		length = len(data)
-	case []uint16:
-		element = SizeOfElement(gltf.UnsignedShort, gltf.Scalar)
-		length = len(data)
-	case [][2]uint16:
-		element = SizeOfElement(gltf.UnsignedShort, gltf.Vec2)
-		length = len(data)
-	case [][3]uint16:
-		element = SizeOfElement(gltf.UnsignedShort, gltf.Vec3)
-		length = len(data)
-	case [][4]uint16:
-		element = SizeOfElement(gltf.UnsignedShort, gltf.Vec4)
-		length = len(data)
-	case [][2][2]uint16:
-		element = SizeOfElement(gltf.UnsignedShort, gltf.Mat2)
-		length = len(data)
-	case [][3][3]uint16:
-		element = SizeOfElement(gltf.UnsignedShort, gltf.Mat3)
-		length = len(data)
-	case [][4][4]uint16:
-		element = SizeOfElement(gltf.UnsignedShort, gltf.Mat4)
-		length = len(data)
-	case []uint32:
-		element = SizeOfElement(gltf.UnsignedInt, gltf.Scalar)
-		length = len(data)
-	case [][2]uint32:
-		element = SizeOfElement(gltf.UnsignedInt, gltf.Vec2)
-		length = len(data)
-	case [][3]uint32:
-		element = SizeOfElement(gltf.UnsignedInt, gltf.Vec3)
-		length = len(data)
-	case [][4]uint32:
-		element = SizeOfElement(gltf.UnsignedInt, gltf.Vec4)
-		length = len(data)
-	case [][2][2]uint32:
-		element = SizeOfElement(gltf.UnsignedInt, gltf.Mat2)
-		length = len(data)
-	case [][3][3]uint32:
-		element = SizeOfElement(gltf.UnsignedInt, gltf.Mat3)
-		length = len(data)
-	case [][4][4]uint32:
-		element = SizeOfElement(gltf.UnsignedInt, gltf.Mat4)
-		length = len(data)
-	case []float32:
-		element = SizeOfElement(gltf.Float, gltf.Scalar)
-		length = len(data)
-	case [][2]float32:
-		element = SizeOfElement(gltf.Float, gltf.Vec2)
-		length = len(data)
-	case [][3]float32:
-		element = SizeOfElement(gltf.Float, gltf.Vec3)
-		length = len(data)
-	case [][4]float32:
-		element = SizeOfElement(gltf.Float, gltf.Vec4)
-		length = len(data)
-	case [][2][2]float32:
-		element = SizeOfElement(gltf.Float, gltf.Mat2)
-		length = len(data)
-	case [][3][3]float32:
-		element = SizeOfElement(gltf.Float, gltf.Mat3)
-		length = len(data)
-	case [][4][4]float32:
-		element = SizeOfElement(gltf.Float, gltf.Mat4)
-		length = len(data)
+// Type returns the associated glTF type data.
+// If data is an slice, it also returns the length of the slice.
+// If data does not have an associated glTF type, length will be -1.
+func Type(data interface{}) (c gltf.ComponentType, t gltf.AccessorType, length int) {
+	v := reflect.ValueOf(data)
+	switch v.Kind() {
+	case reflect.Slice:
+		length = v.Len()
+	}
+	switch data.(type) {
+	case []int8, int8:
+		c, t = gltf.Byte, gltf.Scalar
+	case [][2]int8, [2]int8:
+		c, t = gltf.Byte, gltf.Vec2
+	case [][3]int8, [3]int8:
+		c, t = gltf.Byte, gltf.Vec3
+	case [][4]int8, [4]int8:
+		c, t = gltf.Byte, gltf.Vec4
+	case [][2][2]int8, [2][2]int8:
+		c, t = gltf.Byte, gltf.Mat2
+	case [][3][3]int8, [3][3]int8:
+		c, t = gltf.Byte, gltf.Mat3
+	case [][4][4]int8, [4][4]int8:
+		c, t = gltf.Byte, gltf.Mat4
+	case []uint8, uint8:
+		c, t = gltf.UnsignedByte, gltf.Scalar
+	case [][2]uint8, [2]uint8:
+		c, t = gltf.UnsignedByte, gltf.Vec2
+	case [][3]uint8, [3]uint8:
+		c, t = gltf.UnsignedByte, gltf.Vec3
+	case []color.RGBA, color.RGBA, [][4]uint8, [4]uint8:
+		c, t = gltf.UnsignedByte, gltf.Vec4
+	case [][2][2]uint8, [2][2]uint8:
+		c, t = gltf.UnsignedByte, gltf.Mat2
+	case [][3][3]uint8, [3][3]uint8:
+		c, t = gltf.UnsignedByte, gltf.Mat3
+	case [][4][4]uint8, [4][4]uint8:
+		c, t = gltf.UnsignedByte, gltf.Mat4
+	case []int16, int16:
+		c, t = gltf.Short, gltf.Scalar
+	case [][2]int16, [2]int16:
+		c, t = gltf.Short, gltf.Vec2
+	case [][3]int16, [3]int16:
+		c, t = gltf.Short, gltf.Vec3
+	case [][4]int16, [4]int16:
+		c, t = gltf.Short, gltf.Vec4
+	case [][2][2]int16, [2][2]int16:
+		c, t = gltf.Short, gltf.Mat2
+	case [][3][3]int16, [3][3]int16:
+		c, t = gltf.Short, gltf.Mat3
+	case [][4][4]int16, [4][4]int16:
+		c, t = gltf.Short, gltf.Mat4
+	case []uint16, uint16:
+		c, t = gltf.UnsignedShort, gltf.Scalar
+	case [][2]uint16, [2]uint16:
+		c, t = gltf.UnsignedShort, gltf.Vec2
+	case [][3]uint16, [3]uint16:
+		c, t = gltf.UnsignedShort, gltf.Vec3
+	case []color.RGBA64, color.RGBA64, [][4]uint16, [4]uint16:
+		c, t = gltf.UnsignedShort, gltf.Vec4
+	case [][2][2]uint16, [2][2]uint16:
+		c, t = gltf.UnsignedShort, gltf.Mat2
+	case [][3][3]uint16, [3][3]uint16:
+		c, t = gltf.UnsignedShort, gltf.Mat3
+	case [][4][4]uint16, [4][4]uint16:
+		c, t = gltf.UnsignedShort, gltf.Mat4
+	case []uint32, uint32:
+		c, t = gltf.UnsignedInt, gltf.Scalar
+	case [][2]uint32, [2]uint32:
+		c, t = gltf.UnsignedInt, gltf.Vec2
+	case [][3]uint32, [3]uint32:
+		c, t = gltf.UnsignedInt, gltf.Vec3
+	case [][4]uint32, [4]uint32:
+		c, t = gltf.UnsignedInt, gltf.Vec4
+	case [][2][2]uint32, [2][2]uint32:
+		c, t = gltf.UnsignedInt, gltf.Mat2
+	case [][3][3]uint32, [3][3]uint32:
+		c, t = gltf.UnsignedInt, gltf.Mat3
+	case [][4][4]uint32, [4][4]uint32:
+		c, t = gltf.UnsignedInt, gltf.Mat4
+	case []float32, float32:
+		c, t = gltf.Float, gltf.Scalar
+	case [][2]float32, [2]float32:
+		c, t = gltf.Float, gltf.Vec2
+	case []gltf.RGB, gltf.RGB, [][3]float32, [3]float32:
+		c, t = gltf.Float, gltf.Vec3
+	case []gltf.RGBA, gltf.RGBA, [][4]float32, [4]float32:
+		c, t = gltf.Float, gltf.Vec4
+	case [][2][2]float32, [2][2]float32:
+		c, t = gltf.Float, gltf.Mat2
+	case [][3][3]float32, [3][3]float32:
+		c, t = gltf.Float, gltf.Mat3
+	case [][4][4]float32, [4][4]float32:
+		c, t = gltf.Float, gltf.Mat4
+	default:
+		length = -1
 	}
-	return element, length
+	return
 }
diff --git a/ext/draco/draco.go b/ext/draco/draco.go
index 38f3cc4..2483e85 100644
--- a/ext/draco/draco.go
+++ b/ext/draco/draco.go
@@ -1,31 +1,31 @@
-package draco
-
-import (
-	"encoding/json"
-
-	"github.com/qmuntal/gltf"
-)
-
-const (
-	// ExtensionName defines the KHR_draco_mesh_compression unique key.
-	ExtensionName = "KHR_draco_mesh_compression"
-)
-
-func init() {
-	gltf.RegisterExtension(ExtensionName, Unmarshal)
-}
-
-// Unmarshal decodes the json data into the correct type.
-func Unmarshal(data []byte) (interface{}, error) {
-	drc := new(Primitive)
-	err := json.Unmarshal(data, drc)
-	return drc, err
-}
-
-// Primitive extension points to the bufferView that contains the compressed data.
-type Primitive struct {
-	Extensions gltf.Extensions `json:"extensions,omitempty"`
-	Extras     interface{}     `json:"extras,omitempty"`
-	BufferView uint32          `json:"bufferView"`
-	Attributes gltf.Attribute  `json:"attributes"`
-}
+package draco
+
+import (
+	"encoding/json"
+
+	"github.com/qmuntal/gltf"
+)
+
+const (
+	// ExtensionName defines the KHR_draco_mesh_compression unique key.
+	ExtensionName = "KHR_draco_mesh_compression"
+)
+
+func init() {
+	gltf.RegisterExtension(ExtensionName, Unmarshal)
+}
+
+// Unmarshal decodes the json data into the correct type.
+func Unmarshal(data []byte) (interface{}, error) {
+	drc := new(Primitive)
+	err := json.Unmarshal(data, drc)
+	return drc, err
+}
+
+// Primitive extension points to the bufferView that contains the compressed data.
+type Primitive struct {
+	Extensions gltf.Extensions `json:"extensions,omitempty"`
+	Extras     interface{}     `json:"extras,omitempty"`
+	BufferView uint32          `json:"bufferView"`
+	Attributes gltf.Attribute  `json:"attributes"`
+}
diff --git a/marshal.go b/marshal.go
index 4b577f3..f667f3e 100644
--- a/marshal.go
+++ b/marshal.go
@@ -1,250 +1,250 @@
-package gltf
-
-import (
-	"bytes"
-	"encoding/json"
-	"errors"
-)
-
-// UnmarshalJSON unmarshal the node with the correct default values.
-func (n *Node) UnmarshalJSON(data []byte) error {
-	type alias Node
-	tmp := alias(Node{
-		Matrix:   DefaultMatrix,
-		Rotation: DefaultRotation,
-		Scale:    DefaultScale,
-	})
-	err := json.Unmarshal(data, &tmp)
-	if err == nil {
-		*n = Node(tmp)
-	}
-	return err
-}
-
-// MarshalJSON marshal the node with the correct default values.
-func (n *Node) MarshalJSON() ([]byte, error) {
-	type alias Node
-	out, err := json.Marshal(&struct{ *alias }{alias: (*alias)(n)})
-	if err == nil {
-		if n.Matrix == DefaultMatrix {
-			out = removeProperty([]byte(`"matrix":[1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1]`), out)
-		} else if n.Matrix == emptyMatrix {
-			out = removeProperty([]byte(`"matrix":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]`), out)
-		}
-
-		if n.Rotation == DefaultRotation {
-			out = removeProperty([]byte(`"rotation":[0,0,0,1]`), out)
-		} else if n.Rotation == emptyRotation {
-			out = removeProperty([]byte(`"rotation":[0,0,0,0]`), out)
-		}
-
-		if n.Scale == DefaultScale {
-			out = removeProperty([]byte(`"scale":[1,1,1]`), out)
-		} else if n.Scale == emptyScale {
-			out = removeProperty([]byte(`"scale":[0,0,0]`), out)
-		}
-
-		if n.Translation == DefaultTranslation {
-			out = removeProperty([]byte(`"translation":[0,0,0]`), out)
-		}
-		out = sanitizeJSON(out)
-	}
-	return out, err
-}
-
-// MarshalJSON marshal the camera with the correct default values.
-func (c *Camera) MarshalJSON() ([]byte, error) {
-	type alias Camera
-	if c.Perspective != nil {
-		return json.Marshal(&struct {
-			Type string `json:"type"`
-			*alias
-		}{
-			Type:  "perspective",
-			alias: (*alias)(c),
-		})
-	} else if c.Orthographic != nil {
-		return json.Marshal(&struct {
-			Type string `json:"type"`
-			*alias
-		}{
-			Type:  "orthographic",
-			alias: (*alias)(c),
-		})
-	}
-	return nil, errors.New("gltf: camera must defined either the perspective or orthographic property")
-}
-
-// UnmarshalJSON unmarshal the material with the correct default values.
-func (m *Material) UnmarshalJSON(data []byte) error {
-	type alias Material
-	tmp := alias(Material{AlphaCutoff: Float64(0.5)})
-	err := json.Unmarshal(data, &tmp)
-	if err == nil {
-		*m = Material(tmp)
-	}
-	return err
-}
-
-// MarshalJSON marshal the material with the correct default values.
-func (m *Material) MarshalJSON() ([]byte, error) {
-	type alias Material
-	out, err := json.Marshal(&struct{ *alias }{alias: (*alias)(m)})
-	if err == nil {
-		if m.AlphaCutoff != nil && *m.AlphaCutoff == 0.5 {
-			out = removeProperty([]byte(`"alphaCutoff":0.5`), out)
-		}
-		if m.EmissiveFactor == [3]float64{0, 0, 0} {
-			out = removeProperty([]byte(`"emissiveFactor":[0,0,0]`), out)
-		}
-		out = sanitizeJSON(out)
-	}
-	return out, err
-}
-
-// UnmarshalJSON unmarshal the texture info with the correct default values.
-func (n *NormalTexture) UnmarshalJSON(data []byte) error {
-	type alias NormalTexture
-	tmp := alias(NormalTexture{Scale: Float64(1)})
-	err := json.Unmarshal(data, &tmp)
-	if err == nil {
-		*n = NormalTexture(tmp)
-	}
-	return err
-}
-
-// MarshalJSON marshal the texture info with the correct default values.
-func (n *NormalTexture) MarshalJSON() ([]byte, error) {
-	type alias NormalTexture
-	if n.Scale != nil && *n.Scale == 1 {
-		return json.Marshal(&struct {
-			Scale float64 `json:"scale,omitempty"`
-			*alias
-		}{
-			Scale: 0,
-			alias: (*alias)(n),
-		})
-	}
-	return json.Marshal(&struct{ *alias }{alias: (*alias)(n)})
-}
-
-// UnmarshalJSON unmarshal the texture info with the correct default values.
-func (o *OcclusionTexture) UnmarshalJSON(data []byte) error {
-	type alias OcclusionTexture
-	tmp := alias(OcclusionTexture{Strength: Float64(1)})
-	err := json.Unmarshal(data, &tmp)
-	if err == nil {
-		*o = OcclusionTexture(tmp)
-	}
-	return err
-}
-
-// MarshalJSON marshal the texture info with the correct default values.
-func (o *OcclusionTexture) MarshalJSON() ([]byte, error) {
-	type alias OcclusionTexture
-	if o.Strength != nil && *o.Strength == 1 {
-		return json.Marshal(&struct {
-			Strength float64 `json:"strength,omitempty"`
-			*alias
-		}{
-			Strength: 0,
-			alias:    (*alias)(o),
-		})
-	}
-	return json.Marshal(&struct{ *alias }{alias: (*alias)(o)})
-}
-
-// UnmarshalJSON unmarshal the color with the correct default values.
-func (c *RGBA) UnmarshalJSON(data []byte) error {
-	tmp := [4]float64{1, 1, 1, 1}
-	err := json.Unmarshal(data, &tmp)
-	if err == nil {
-		c.R, c.G, c.B, c.A = tmp[0], tmp[1], tmp[2], tmp[3]
-	}
-	return err
-}
-
-// MarshalJSON marshal the color with the correct default values.
-func (c *RGBA) MarshalJSON() ([]byte, error) {
-	return json.Marshal([4]float64{c.R, c.G, c.B, c.A})
-}
-
-// UnmarshalJSON unmarshal the color with the correct default values.
-func (c *RGB) UnmarshalJSON(data []byte) error {
-	tmp := [3]float64{1, 1, 1}
-	err := json.Unmarshal(data, &tmp)
-	if err == nil {
-		c.R, c.G, c.B = tmp[0], tmp[1], tmp[2]
-	}
-	return err
-}
-
-// MarshalJSON marshal the color with the correct default values.
-func (c *RGB) MarshalJSON() ([]byte, error) {
-	return json.Marshal([3]float64{c.R, c.G, c.B})
-}
-
-// UnmarshalJSON unmarshal the pbr with the correct default values.
-func (p *PBRMetallicRoughness) UnmarshalJSON(data []byte) error {
-	type alias PBRMetallicRoughness
-	tmp := alias(PBRMetallicRoughness{BaseColorFactor: NewRGBA(), MetallicFactor: Float64(1), RoughnessFactor: Float64(1)})
-	err := json.Unmarshal(data, &tmp)
-	if err == nil {
-		*p = PBRMetallicRoughness(tmp)
-	}
-	return err
-}
-
-// MarshalJSON marshal the pbr with the correct default values.
-func (p *PBRMetallicRoughness) MarshalJSON() ([]byte, error) {
-	type alias PBRMetallicRoughness
-	out, err := json.Marshal(&struct{ *alias }{alias: (*alias)(p)})
-	if err == nil {
-		if p.MetallicFactor != nil && *p.MetallicFactor == 1 {
-			out = removeProperty([]byte(`"metallicFactor":1`), out)
-		}
-		if p.RoughnessFactor != nil && *p.RoughnessFactor == 1 {
-			out = removeProperty([]byte(`"roughnessFactor":1`), out)
-		}
-		if p.BaseColorFactor != nil && *p.BaseColorFactor == *NewRGBA() {
-			out = removeProperty([]byte(`"baseColorFactor":[1,1,1,1]`), out)
-		}
-		out = sanitizeJSON(out)
-	}
-	return out, err
-}
-
-// UnmarshalJSON unmarshal the extensions with the supported extensions initialized.
-func (ext *Extensions) UnmarshalJSON(data []byte) error {
-	if len(*ext) == 0 {
-		*ext = make(Extensions)
-	}
-	var raw map[string]json.RawMessage
-	err := json.Unmarshal(data, &raw)
-	if err == nil {
-		for key, value := range raw {
-			if extFactory, ok := extensions[key]; ok {
-				n, err := extFactory(value)
-				if err != nil {
-					(*ext)[key] = value
-				} else {
-					(*ext)[key] = n
-				}
-			} else {
-				(*ext)[key] = value
-			}
-		}
-	}
-
-	return err
-}
-
-func removeProperty(str []byte, b []byte) []byte {
-	b = bytes.Replace(b, str, []byte(""), 1)
-	return bytes.Replace(b, []byte(`,,`), []byte(","), 1)
-}
-
-func sanitizeJSON(b []byte) []byte {
-	b = bytes.Replace(b, []byte(`{,`), []byte("{"), 1)
-	return bytes.Replace(b, []byte(`,}`), []byte("}"), 1)
-}
+package gltf
+
+import (
+	"bytes"
+	"encoding/json"
+	"errors"
+)
+
+// UnmarshalJSON unmarshal the node with the correct default values.
+func (n *Node) UnmarshalJSON(data []byte) error {
+	type alias Node
+	tmp := alias(Node{
+		Matrix:   DefaultMatrix,
+		Rotation: DefaultRotation,
+		Scale:    DefaultScale,
+	})
+	err := json.Unmarshal(data, &tmp)
+	if err == nil {
+		*n = Node(tmp)
+	}
+	return err
+}
+
+// MarshalJSON marshal the node with the correct default values.
+func (n *Node) MarshalJSON() ([]byte, error) {
+	type alias Node
+	out, err := json.Marshal(&struct{ *alias }{alias: (*alias)(n)})
+	if err == nil {
+		if n.Matrix == DefaultMatrix {
+			out = removeProperty([]byte(`"matrix":[1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1]`), out)
+		} else if n.Matrix == emptyMatrix {
+			out = removeProperty([]byte(`"matrix":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]`), out)
+		}
+
+		if n.Rotation == DefaultRotation {
+			out = removeProperty([]byte(`"rotation":[0,0,0,1]`), out)
+		} else if n.Rotation == emptyRotation {
+			out = removeProperty([]byte(`"rotation":[0,0,0,0]`), out)
+		}
+
+		if n.Scale == DefaultScale {
+			out = removeProperty([]byte(`"scale":[1,1,1]`), out)
+		} else if n.Scale == emptyScale {
+			out = removeProperty([]byte(`"scale":[0,0,0]`), out)
+		}
+
+		if n.Translation == DefaultTranslation {
+			out = removeProperty([]byte(`"translation":[0,0,0]`), out)
+		}
+		out = sanitizeJSON(out)
+	}
+	return out, err
+}
+
+// MarshalJSON marshal the camera with the correct default values.
+func (c *Camera) MarshalJSON() ([]byte, error) {
+	type alias Camera
+	if c.Perspective != nil {
+		return json.Marshal(&struct {
+			Type string `json:"type"`
+			*alias
+		}{
+			Type:  "perspective",
+			alias: (*alias)(c),
+		})
+	} else if c.Orthographic != nil {
+		return json.Marshal(&struct {
+			Type string `json:"type"`
+			*alias
+		}{
+			Type:  "orthographic",
+			alias: (*alias)(c),
+		})
+	}
+	return nil, errors.New("gltf: camera must defined either the perspective or orthographic property")
+}
+
+// UnmarshalJSON unmarshal the material with the correct default values.
+func (m *Material) UnmarshalJSON(data []byte) error {
+	type alias Material
+	tmp := alias(Material{AlphaCutoff: Float64(0.5)})
+	err := json.Unmarshal(data, &tmp)
+	if err == nil {
+		*m = Material(tmp)
+	}
+	return err
+}
+
+// MarshalJSON marshal the material with the correct default values.
+func (m *Material) MarshalJSON() ([]byte, error) {
+	type alias Material
+	out, err := json.Marshal(&struct{ *alias }{alias: (*alias)(m)})
+	if err == nil {
+		if m.AlphaCutoff != nil && *m.AlphaCutoff == 0.5 {
+			out = removeProperty([]byte(`"alphaCutoff":0.5`), out)
+		}
+		if m.EmissiveFactor == [3]float64{0, 0, 0} {
+			out = removeProperty([]byte(`"emissiveFactor":[0,0,0]`), out)
+		}
+		out = sanitizeJSON(out)
+	}
+	return out, err
+}
+
+// UnmarshalJSON unmarshal the texture info with the correct default values.
+func (n *NormalTexture) UnmarshalJSON(data []byte) error {
+	type alias NormalTexture
+	tmp := alias(NormalTexture{Scale: Float64(1)})
+	err := json.Unmarshal(data, &tmp)
+	if err == nil {
+		*n = NormalTexture(tmp)
+	}
+	return err
+}
+
+// MarshalJSON marshal the texture info with the correct default values.
+func (n *NormalTexture) MarshalJSON() ([]byte, error) {
+	type alias NormalTexture
+	if n.Scale != nil && *n.Scale == 1 {
+		return json.Marshal(&struct {
+			Scale float64 `json:"scale,omitempty"`
+			*alias
+		}{
+			Scale: 0,
+			alias: (*alias)(n),
+		})
+	}
+	return json.Marshal(&struct{ *alias }{alias: (*alias)(n)})
+}
+
+// UnmarshalJSON unmarshal the texture info with the correct default values.
+func (o *OcclusionTexture) UnmarshalJSON(data []byte) error {
+	type alias OcclusionTexture
+	tmp := alias(OcclusionTexture{Strength: Float64(1)})
+	err := json.Unmarshal(data, &tmp)
+	if err == nil {
+		*o = OcclusionTexture(tmp)
+	}
+	return err
+}
+
+// MarshalJSON marshal the texture info with the correct default values.
+func (o *OcclusionTexture) MarshalJSON() ([]byte, error) {
+	type alias OcclusionTexture
+	if o.Strength != nil && *o.Strength == 1 {
+		return json.Marshal(&struct {
+			Strength float64 `json:"strength,omitempty"`
+			*alias
+		}{
+			Strength: 0,
+			alias:    (*alias)(o),
+		})
+	}
+	return json.Marshal(&struct{ *alias }{alias: (*alias)(o)})
+}
+
+// UnmarshalJSON unmarshal the color with the correct default values.
+func (c *RGBA) UnmarshalJSON(data []byte) error {
+	tmp := [4]float64{1, 1, 1, 1}
+	err := json.Unmarshal(data, &tmp)
+	if err == nil {
+		c.R, c.G, c.B, c.A = tmp[0], tmp[1], tmp[2], tmp[3]
+	}
+	return err
+}
+
+// MarshalJSON marshal the color with the correct default values.
+func (c *RGBA) MarshalJSON() ([]byte, error) {
+	return json.Marshal([4]float64{c.R, c.G, c.B, c.A})
+}
+
+// UnmarshalJSON unmarshal the color with the correct default values.
+func (c *RGB) UnmarshalJSON(data []byte) error {
+	tmp := [3]float64{1, 1, 1}
+	err := json.Unmarshal(data, &tmp)
+	if err == nil {
+		c.R, c.G, c.B = tmp[0], tmp[1], tmp[2]
+	}
+	return err
+}
+
+// MarshalJSON marshal the color with the correct default values.
+func (c *RGB) MarshalJSON() ([]byte, error) {
+	return json.Marshal([3]float64{c.R, c.G, c.B})
+}
+
+// UnmarshalJSON unmarshal the pbr with the correct default values.
+func (p *PBRMetallicRoughness) UnmarshalJSON(data []byte) error {
+	type alias PBRMetallicRoughness
+	tmp := alias(PBRMetallicRoughness{BaseColorFactor: NewRGBA(), MetallicFactor: Float64(1), RoughnessFactor: Float64(1)})
+	err := json.Unmarshal(data, &tmp)
+	if err == nil {
+		*p = PBRMetallicRoughness(tmp)
+	}
+	return err
+}
+
+// MarshalJSON marshal the pbr with the correct default values.
+func (p *PBRMetallicRoughness) MarshalJSON() ([]byte, error) {
+	type alias PBRMetallicRoughness
+	out, err := json.Marshal(&struct{ *alias }{alias: (*alias)(p)})
+	if err == nil {
+		if p.MetallicFactor != nil && *p.MetallicFactor == 1 {
+			out = removeProperty([]byte(`"metallicFactor":1`), out)
+		}
+		if p.RoughnessFactor != nil && *p.RoughnessFactor == 1 {
+			out = removeProperty([]byte(`"roughnessFactor":1`), out)
+		}
+		if p.BaseColorFactor != nil && *p.BaseColorFactor == *NewRGBA() {
+			out = removeProperty([]byte(`"baseColorFactor":[1,1,1,1]`), out)
+		}
+		out = sanitizeJSON(out)
+	}
+	return out, err
+}
+
+// UnmarshalJSON unmarshal the extensions with the supported extensions initialized.
+func (ext *Extensions) UnmarshalJSON(data []byte) error {
+	if len(*ext) == 0 {
+		*ext = make(Extensions)
+	}
+	var raw map[string]json.RawMessage
+	err := json.Unmarshal(data, &raw)
+	if err == nil {
+		for key, value := range raw {
+			if extFactory, ok := extensions[key]; ok {
+				n, err := extFactory(value)
+				if err != nil {
+					(*ext)[key] = value
+				} else {
+					(*ext)[key] = n
+				}
+			} else {
+				(*ext)[key] = value
+			}
+		}
+	}
+
+	return err
+}
+
+func removeProperty(str []byte, b []byte) []byte {
+	b = bytes.Replace(b, str, []byte(""), 1)
+	return bytes.Replace(b, []byte(`,,`), []byte(","), 1)
+}
+
+func sanitizeJSON(b []byte) []byte {
+	b = bytes.Replace(b, []byte(`{,`), []byte("{"), 1)
+	return bytes.Replace(b, []byte(`,}`), []byte("}"), 1)
+}
diff --git a/modeler/compression.go b/modeler/compression.go
new file mode 100644
index 0000000..a8a6111
--- /dev/null
+++ b/modeler/compression.go
@@ -0,0 +1,52 @@
+package modeler
+
+import "math"
+
+func compressUint32(data []uint32) interface{} {
+	u8, u16 := true, true
+	// Start at the end, as there are more possibilities to have a higher numbers.
+	for i := len(data) - 1; i >= 0; i-- {
+		x := data[i]
+		if u16 && x >= math.MaxUint16 {
+			u8, u16 = false, false
+			break
+		} else if u8 && x >= math.MaxUint8 {
+			u8 = false
+		}
+	}
+	if u8 {
+		optData := make([]uint8, len(data))
+		for i, x := range data {
+			optData[i] = uint8(x)
+		}
+		return optData
+	}
+	if u16 {
+		optData := make([]uint16, len(data))
+		for i, x := range data {
+			optData[i] = uint16(x)
+		}
+		return optData
+	}
+	return data
+}
+
+func compressUint16(data []uint16) interface{} {
+	u8 := true
+	// Start at the end, as there are more possibilities to have a higher numbers.
+	for i := len(data) - 1; i >= 0; i-- {
+		x := data[i]
+		if u8 && x >= math.MaxUint8 {
+			u8 = false
+			break
+		}
+	}
+	if u8 {
+		optData := make([]uint8, len(data))
+		for i, x := range data {
+			optData[i] = uint8(x)
+		}
+		return optData
+	}
+	return data
+}
diff --git a/modeler/modeler.go b/modeler/modeler.go
new file mode 100644
index 0000000..1f82b1c
--- /dev/null
+++ b/modeler/modeler.go
@@ -0,0 +1,257 @@
+package modeler
+
+import (
+	"bytes"
+	"image/color"
+	"io"
+	"io/ioutil"
+	"math"
+	"reflect"
+
+	"github.com/qmuntal/gltf"
+	"github.com/qmuntal/gltf/binary"
+)
+
+// CompressionLevel defines the different levels of compression.
+type CompressionLevel uint8
+
+const (
+	// CompressionNone will not apply any compression.
+	CompressionNone CompressionLevel = iota
+	// CompressionLossless will reduce the byte size without sacrificing quality.
+	CompressionLossless
+)
+
+// Modeler wraps a Document and add usefull methods to build it.
+// If Compress is true, all the data added to accessors that support different component types
+// will be evaluated to see if it fits in a smaller component type.
+type Modeler struct {
+	*gltf.Document
+	Compression CompressionLevel
+}
+
+// NewModeler returns a new Modeler instance.
+func NewModeler() *Modeler {
+	return &Modeler{
+		Document: &gltf.Document{
+			Scene:  gltf.Index(0),
+			Scenes: []gltf.Scene{{Name: "Root Scene"}},
+		},
+		Compression: CompressionLossless,
+	}
+}
+
+// AddIndices adds a new INDICES accessor to the Document
+// and fills the buffer with the indices data.
+// If success it returns the index of the new accessor.
+func (m *Modeler) AddIndices(bufferIndex uint32, data interface{}) uint32 {
+	var ok bool
+	switch data.(type) {
+	case []uint8:
+		ok = true
+	case []uint16:
+		ok = true
+		if m.Compression >= CompressionLossless {
+			data = compressUint16(data.([]uint16))
+		}
+	case []uint32:
+		ok = true
+		if m.Compression >= CompressionLossless {
+			data = compressUint32(data.([]uint32))
+		}
+	}
+	componentType, accessorType, length := binary.Type(data)
+	if !ok || length <= 0 {
+		panic("modeler.AddIndices: invalid type " + reflect.TypeOf(data).String())
+	}
+	index := m.addAccessor(bufferIndex, length, data, componentType, accessorType, true)
+	return uint32(index)
+}
+
+// AddNormal adds a new NORMAL accessor to the Document
+// and fills the buffer with the indices data.
+// If success it returns the index of the new accessor.
+func (m *Modeler) AddNormal(bufferIndex uint32, data [][3]float32) uint32 {
+	componentType, accessorType, length := binary.Type(data)
+	index := m.addAccessor(bufferIndex, length, data, componentType, accessorType, false)
+	return uint32(index)
+}
+
+// AddTangent adds a new TANGENT accessor to the Document
+// and fills the buffer with the indices data.
+// If success it returns the index of the new accessor.
+func (m *Modeler) AddTangent(bufferIndex uint32, data [][4]float32) uint32 {
+	componentType, accessorType, length := binary.Type(data)
+	index := m.addAccessor(bufferIndex, length, data, componentType, accessorType, false)
+	return uint32(index)
+}
+
+// AddTextureCoord adds a new TEXTURECOORD accessor to the Document
+// and fills the buffer with the texturecoord data.
+// If success it returns the index of the new accessor.
+func (m *Modeler) AddTextureCoord(bufferIndex uint32, data interface{}) uint32 {
+	var normalized, ok bool
+	switch data.(type) {
+	case [][2]uint8, [][2]uint16:
+		ok = true
+		normalized = true
+	case [][2]float32:
+		ok = true
+	}
+	componentType, accessorType, length := binary.Type(data)
+	if !ok || length <= 0 {
+		panic("modeler.AddTextureCoord: invalid type " + reflect.TypeOf(data).String())
+	}
+	index := m.addAccessor(bufferIndex, length, data, componentType, accessorType, false)
+	m.Document.Accessors[index].Normalized = normalized
+	return uint32(index)
+}
+
+// AddWeights adds a new WEIGTHS accessor to the Document
+// and fills the buffer with the weights data.
+// If success it returns the index of the new accessor.
+func (m *Modeler) AddWeights(bufferIndex uint32, data interface{}) uint32 {
+	var normalized, ok bool
+	switch data.(type) {
+	case [][4]uint8, [][4]uint16:
+		ok = true
+		normalized = true
+	case [][4]float32:
+		ok = true
+	}
+	componentType, accessorType, length := binary.Type(data)
+	if !ok || length <= 0 {
+		panic("modeler.AddWeights: invalid type " + reflect.TypeOf(data).String())
+	}
+	index := m.addAccessor(bufferIndex, length, data, componentType, accessorType, false)
+	m.Document.Accessors[index].Normalized = normalized
+	return uint32(index)
+}
+
+// AddJoints adds a new JOINTS accessor to the Document
+// and fills the buffer with the joints data.
+// If success it returns the index of the new accessor.
+func (m *Modeler) AddJoints(bufferIndex uint32, data interface{}) uint32 {
+	var ok bool
+	switch data.(type) {
+	case [][4]uint8, [][4]uint16:
+		ok = true
+	}
+	componentType, accessorType, length := binary.Type(data)
+	if !ok || length <= 0 {
+		panic("modeler.AddJoints: invalid type " + reflect.TypeOf(data).String())
+	}
+	index := m.addAccessor(bufferIndex, length, data, componentType, accessorType, false)
+	return uint32(index)
+}
+
+// AddPosition adds a new POSITION accessor to the Document
+// and fills the buffer with the vertices data.
+// If success it returns the index of the new accessor.
+func (m *Modeler) AddPosition(bufferIndex uint32, data [][3]float32) uint32 {
+	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(min[i], float64(x))
+			max[i] = math.Max(max[i], float64(x))
+		}
+	}
+	componentType, accessorType, length := binary.Type(data)
+	index := m.addAccessor(bufferIndex, length, data, componentType, accessorType, false)
+	m.Accessors[index].Min = min[:]
+	m.Accessors[index].Max = max[:]
+	return uint32(index)
+}
+
+// AddColor adds a new COLOR accessor to the Document
+// and fills the buffer with the color data.
+// If success it returns the index of the new accessor.
+func (m *Modeler) AddColor(bufferIndex uint32, data interface{}) uint32 {
+	var normalized, ok bool
+	switch data.(type) {
+	case []color.RGBA, []color.RGBA64, [][4]uint8, [][3]uint8, [][4]uint16, [][3]uint16:
+		normalized = true
+		ok = true
+	case []gltf.RGBA, []gltf.RGB, [][3]float32, [][4]float32:
+		ok = true
+	}
+	componentType, accessorType, length := binary.Type(data)
+	if !ok || length <= 0 {
+		panic("modeler.AddColor: invalid type " + reflect.TypeOf(data).String())
+	}
+	index := m.addAccessor(bufferIndex, length, data, componentType, accessorType, false)
+	m.Document.Accessors[index].Normalized = normalized
+	return uint32(index)
+}
+
+// AddImage adds a new image to the Document
+// and fills the buffer with the image data.
+// If success it returns the index of the new image.
+func (m *Modeler) AddImage(bufferIndex uint32, name, mimeType string, r io.Reader) (uint32, error) {
+	buffer := m.buffer(bufferIndex)
+	offset := uint32(len(buffer.Data))
+	switch r := r.(type) {
+	case *bytes.Buffer:
+		buffer.Data = append(buffer.Data, r.Bytes()...)
+	default:
+		data, err := ioutil.ReadAll(r)
+		if err != nil {
+			return 0, err
+		}
+		buffer.Data = append(buffer.Data, data...)
+	}
+	index := m.addBufferView(bufferIndex, uint32(len(buffer.Data))-offset, offset, 0, false)
+	buffer.ByteLength += uint32(len(buffer.Data))
+	m.BufferViews[index].Target = gltf.None
+	m.Images = append(m.Images, gltf.Image{
+		Name:       name,
+		MimeType:   mimeType,
+		BufferView: gltf.Index(index),
+	})
+	return uint32(len(m.Images) - 1), nil
+}
+
+func (m *Modeler) addAccessor(bufferIndex uint32, count int, data interface{}, componentType gltf.ComponentType, accessorType gltf.AccessorType, isIndex bool) uint32 {
+	buffer := m.buffer(bufferIndex)
+	offset := uint32(len(buffer.Data))
+	padding := ((offset+3)/4)*4 - offset
+	elementSize := binary.SizeOfElement(componentType, accessorType)
+	size := uint32(count * elementSize)
+	buffer.ByteLength += uint32(size + padding)
+	buffer.Data = append(buffer.Data, make([]byte, size+padding)...)
+	// Cannot return error as the buffer has enough size and the data type is controled.
+	_ = binary.Write(buffer.Data[offset+padding:], data)
+	index := m.addBufferView(bufferIndex, size, offset+padding, uint32(elementSize), isIndex)
+	m.Accessors = append(m.Accessors, gltf.Accessor{
+		BufferView:    gltf.Index(index),
+		ByteOffset:    0,
+		ComponentType: componentType,
+		Type:          accessorType,
+		Count:         uint32(count),
+	})
+	return uint32(len(m.Accessors) - 1)
+}
+
+func (m *Modeler) addBufferView(buffer, size, offset, stride uint32, isIndices bool) uint32 {
+	bufferView := gltf.BufferView{
+		Buffer:     buffer,
+		ByteLength: size,
+		ByteOffset: offset,
+	}
+	if isIndices {
+		bufferView.Target = gltf.ElementArrayBuffer
+	} else {
+		bufferView.Target = gltf.ArrayBuffer
+		bufferView.ByteStride = stride
+	}
+	m.BufferViews = append(m.BufferViews, bufferView)
+	return uint32(len(m.BufferViews)) - 1
+}
+
+func (m *Modeler) buffer(bufferIndex uint32) *gltf.Buffer {
+	if int(bufferIndex) >= len(m.Buffers) {
+		m.Buffers = append(m.Buffers, make([]gltf.Buffer, int(bufferIndex)-len(m.Buffers)+1)...)
+	}
+	return &m.Buffers[bufferIndex]
+}


Test patch:
diff --git a/binary/bench_test.go b/binary/bench_test.go
index 1d3a1b4..9be62de 100644
--- a/binary/bench_test.go
+++ b/binary/bench_test.go
@@ -1,41 +1,41 @@
-package binary
-
-import (
-	"bytes"
-	"encoding/binary"
-	"testing"
-
-	"github.com/qmuntal/gltf"
-)
-
-func BenchmarkNative(b *testing.B) {
-	s := 1000
-	bs := make([]byte, s*SizeOfElement(gltf.Float, gltf.Vec3))
-	data := make([][3]float32, s)
-	b.ResetTimer()
-	for n := 0; n < b.N; n++ {
-		for i := range data {
-			Float.PutVec3(bs[4*i:], data[i])
-		}
-	}
-}
-
-func BenchmarkWrite(b *testing.B) {
-	s := 1000
-	bs := make([]byte, s*SizeOfElement(gltf.Float, gltf.Vec3))
-	data := make([][3]float32, s)
-	b.ResetTimer()
-	for n := 0; n < b.N; n++ {
-		Write(bs, data)
-	}
-}
-
-func BenchmarkWrite_builtint(b *testing.B) {
-	s := 1000
-	bs := bytes.NewBuffer(make([]byte, s*SizeOfElement(gltf.Float, gltf.Vec3)))
-	data := make([][3]float32, s)
-	b.ResetTimer()
-	for n := 0; n < b.N; n++ {
-		binary.Write(bs, binary.LittleEndian, data)
-	}
-}
+package binary
+
+import (
+	"bytes"
+	"encoding/binary"
+	"testing"
+
+	"github.com/qmuntal/gltf"
+)
+
+func BenchmarkNative(b *testing.B) {
+	s := 1000
+	bs := make([]byte, s*SizeOfElement(gltf.Float, gltf.Vec3))
+	data := make([][3]float32, s)
+	b.ResetTimer()
+	for n := 0; n < b.N; n++ {
+		for i := range data {
+			Float.PutVec3(bs[4*i:], data[i])
+		}
+	}
+}
+
+func BenchmarkWrite(b *testing.B) {
+	s := 1000
+	bs := make([]byte, s*SizeOfElement(gltf.Float, gltf.Vec3))
+	data := make([][3]float32, s)
+	b.ResetTimer()
+	for n := 0; n < b.N; n++ {
+		Write(bs, data)
+	}
+}
+
+func BenchmarkWrite_builtint(b *testing.B) {
+	s := 1000
+	bs := bytes.NewBuffer(make([]byte, s*SizeOfElement(gltf.Float, gltf.Vec3)))
+	data := make([][3]float32, s)
+	b.ResetTimer()
+	for n := 0; n < b.N; n++ {
+		binary.Write(bs, binary.LittleEndian, data)
+	}
+}
diff --git a/binary/encode_test.go b/binary/encode_test.go
index c86599b..3b3b230 100644
--- a/binary/encode_test.go
+++ b/binary/encode_test.go
@@ -2,9 +2,12 @@ package binary
 
 import (
 	"encoding/binary"
+	"image/color"
 	"math"
 	"reflect"
 	"testing"
+
+	"github.com/qmuntal/gltf"
 )
 
 func buildBuffer1(n int, empty ...int) []byte {
@@ -61,8 +64,8 @@ func TestRead(t *testing.T) {
 		{"empty", args{make([]byte, 0), []int8{}}, []int8{}, false},
 		{"int8", args{buildBuffer1(4), make([]int8, 4)}, []int8{1, 2, 3, 4}, false},
 		{"int8-FE", args{[]byte{0xFE}, make([]int8, 1)}, []int8{-2}, false},
-		{"[2]int8", args{buildBuffer1(2 * 2), make([][2]int8, 2)}, [][2]int8{{1, 2}, {3, 4}}, false},
-		{"[3]int8", args{buildBuffer1(2 * 3), make([][3]int8, 2)}, [][3]int8{{1, 2, 3}, {4, 5, 6}}, false},
+		{"[2]int8", args{[]byte{1, 2, 0, 0, 3, 4, 0, 0}, make([][2]int8, 2)}, [][2]int8{{1, 2}, {3, 4}}, false},
+		{"[3]int8", args{[]byte{1, 2, 3, 0, 4, 5, 6, 0}, make([][3]int8, 2)}, [][3]int8{{1, 2, 3}, {4, 5, 6}}, false},
 		{"[4]int8", args{buildBuffer1(2 * 4), make([][4]int8, 2)}, [][4]int8{{1, 2, 3, 4}, {5, 6, 7, 8}}, false},
 		{"[2][2]int8", args{buildBuffer1(16, 2, 3, 6, 7, 10, 11, 14, 15), make([][2][2]int8, 2)}, [][2][2]int8{
 			{{1, 5}, {2, 6}},
@@ -78,8 +81,8 @@ func TestRead(t *testing.T) {
 		}, false},
 		{"uint8", args{buildBuffer1(4), make([]uint8, 4)}, []uint8{1, 2, 3, 4}, false},
 		{"uint8-FE", args{[]byte{0xFE}, make([]uint8, 1)}, []uint8{254}, false},
-		{"[2]uint8", args{buildBuffer1(2 * 2), make([][2]uint8, 2)}, [][2]uint8{{1, 2}, {3, 4}}, false},
-		{"[3]uint8", args{buildBuffer1(2 * 3), make([][3]uint8, 2)}, [][3]uint8{{1, 2, 3}, {4, 5, 6}}, false},
+		{"[2]uint8", args{[]byte{1, 2, 0, 0, 3, 4, 0, 0}, make([][2]uint8, 2)}, [][2]uint8{{1, 2}, {3, 4}}, false},
+		{"[3]uint8", args{[]byte{1, 2, 3, 0, 4, 5, 6, 0}, make([][3]uint8, 2)}, [][3]uint8{{1, 2, 3}, {4, 5, 6}}, false},
 		{"[4]uint8", args{buildBuffer1(2 * 4), make([][4]uint8, 2)}, [][4]uint8{{1, 2, 3, 4}, {5, 6, 7, 8}}, false},
 		{"[2][2]uint8", args{buildBuffer1(16, 2, 3, 6, 7, 10, 11), make([][2][2]uint8, 2)}, [][2][2]uint8{
 			{{1, 5}, {2, 6}},
@@ -96,7 +99,7 @@ func TestRead(t *testing.T) {
 		{"int16", args{buildBuffer2(4), make([]int16, 4)}, []int16{1, 2, 3, 4}, false},
 		{"int16-FE", args{[]byte{0xFE, 0xFF}, make([]int16, 1)}, []int16{-2}, false},
 		{"[2]int16", args{buildBuffer2(2 * 2), make([][2]int16, 2)}, [][2]int16{{1, 2}, {3, 4}}, false},
-		{"[3]int16", args{buildBuffer2(2 * 3), make([][3]int16, 2)}, [][3]int16{{1, 2, 3}, {4, 5, 6}}, false},
+		{"[3]int16", args{[]byte{1, 0, 2, 0, 3, 0, 0, 0, 4, 0, 5, 0, 6, 0, 0, 0}, make([][3]int16, 2)}, [][3]int16{{1, 2, 3}, {4, 5, 6}}, false},
 		{"[4]int16", args{buildBuffer2(2 * 4), make([][4]int16, 2)}, [][4]int16{{1, 2, 3, 4}, {5, 6, 7, 8}}, false},
 		{"[2][2]int16", args{buildBuffer2(2 * 2 * 2), make([][2][2]int16, 2)}, [][2][2]int16{
 			{{1, 3}, {2, 4}},
@@ -113,7 +116,7 @@ func TestRead(t *testing.T) {
 		{"uint16", args{buildBuffer2(4), make([]uint16, 4)}, []uint16{1, 2, 3, 4}, false},
 		{"uint16-FE", args{[]byte{0xFE, 0xFF}, make([]uint16, 1)}, []uint16{65534}, false},
 		{"[2]uint16", args{buildBuffer2(2 * 2), make([][2]uint16, 2)}, [][2]uint16{{1, 2}, {3, 4}}, false},
-		{"[3]uint16", args{buildBuffer2(2 * 3), make([][3]uint16, 2)}, [][3]uint16{{1, 2, 3}, {4, 5, 6}}, false},
+		{"[3]uint16", args{[]byte{1, 0, 2, 0, 3, 0, 0, 0, 4, 0, 5, 0, 6, 0, 0, 0}, make([][3]uint16, 2)}, [][3]uint16{{1, 2, 3}, {4, 5, 6}}, false},
 		{"[4]uint16", args{buildBuffer2(2 * 4), make([][4]uint16, 2)}, [][4]uint16{{1, 2, 3, 4}, {5, 6, 7, 8}}, false},
 		{"[2][2]uint16", args{buildBuffer2(2 * 2 * 2), make([][2][2]uint16, 2)}, [][2][2]uint16{
 			{{1, 3}, {2, 4}},
@@ -161,6 +164,10 @@ func TestRead(t *testing.T) {
 			{{1, 5, 9, 13}, {2, 6, 10, 14}, {3, 7, 11, 15}, {4, 8, 12, 16}},
 			{{17, 21, 25, 29}, {18, 22, 26, 30}, {19, 23, 27, 31}, {20, 24, 28, 32}},
 		}, false},
+		{"color.RGBA", args{buildBuffer1(2 * 4), make([]color.RGBA, 2)}, []color.RGBA{{1, 2, 3, 4}, {5, 6, 7, 8}}, false},
+		{"color.RGBA64", args{buildBuffer2(2 * 4), make([]color.RGBA64, 2)}, []color.RGBA64{{1, 2, 3, 4}, {5, 6, 7, 8}}, false},
+		{"gltf.RGBA", args{buildBufferF(2 * 4), make([]gltf.RGBA, 2)}, []gltf.RGBA{{R: 1, G: 2, B: 3, A: 4}, {R: 5, G: 6, B: 7, A: 8}}, false},
+		{"gltf.RGB", args{buildBufferF(2 * 3), make([]gltf.RGB, 2)}, []gltf.RGB{{R: 1, G: 2, B: 3}, {R: 4, G: 5, B: 6}}, false},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
@@ -190,8 +197,8 @@ func TestWrite(t *testing.T) {
 		{"empty", args{0, []int8{}}, []byte{}, false},
 		{"int8", args{4, []int8{1, 2, 3, 4}}, buildBuffer1(4), false},
 		{"int8-FE", args{1, []int8{-2}}, []byte{0xFE}, false},
-		{"[2]int8", args{4, [][2]int8{{1, 2}, {3, 4}}}, buildBuffer1(2 * 2), false},
-		{"[3]int8", args{6, [][3]int8{{1, 2, 3}, {4, 5, 6}}}, buildBuffer1(2 * 3), false},
+		{"[2]int8", args{8, [][2]int8{{1, 2}, {3, 4}}}, []byte{1, 2, 0, 0, 3, 4, 0, 0}, false},
+		{"[3]int8", args{8, [][3]int8{{1, 2, 3}, {4, 5, 6}}}, []byte{1, 2, 3, 0, 4, 5, 6, 0}, false},
 		{"[4]int8", args{8, [][4]int8{{1, 2, 3, 4}, {5, 6, 7, 8}}}, buildBuffer1(2 * 4), false},
 		{"[2][2]int8", args{16, [][2][2]int8{
 			{{1, 5}, {2, 6}},
@@ -207,8 +214,8 @@ func TestWrite(t *testing.T) {
 		}}, buildBuffer1(2 * 4 * 4), false},
 		{"uint8", args{4, []uint8{1, 2, 3, 4}}, buildBuffer1(4), false},
 		{"uint8-FE", args{1, []uint8{254}}, []byte{0xFE}, false},
-		{"[2]uint8", args{4, [][2]uint8{{1, 2}, {3, 4}}}, buildBuffer1(2 * 2), false},
-		{"[3]uint8", args{6, [][3]uint8{{1, 2, 3}, {4, 5, 6}}}, buildBuffer1(2 * 3), false},
+		{"[2]uint8", args{8, [][2]uint8{{1, 2}, {3, 4}}}, []byte{1, 2, 0, 0, 3, 4, 0, 0}, false},
+		{"[3]uint8", args{8, [][3]uint8{{1, 2, 3}, {4, 5, 6}}}, []byte{1, 2, 3, 0, 4, 5, 6, 0}, false},
 		{"[4]uint8", args{8, [][4]uint8{{1, 2, 3, 4}, {5, 6, 7, 8}}}, buildBuffer1(2 * 4), false},
 		{"[2][2]uint8", args{16, [][2][2]uint8{
 			{{1, 5}, {2, 6}},
@@ -225,7 +232,7 @@ func TestWrite(t *testing.T) {
 		{"int16", args{8, []int16{1, 2, 3, 4}}, buildBuffer2(4), false},
 		{"int16-FE", args{2, []int16{-2}}, []byte{0xFE, 0xFF}, false},
 		{"[2]int16", args{8, [][2]int16{{1, 2}, {3, 4}}}, buildBuffer2(2 * 2), false},
-		{"[3]int16", args{12, [][3]int16{{1, 2, 3}, {4, 5, 6}}}, buildBuffer2(2 * 3), false},
+		{"[3]int16", args{16, [][3]int16{{1, 2, 3}, {4, 5, 6}}}, []byte{1, 0, 2, 0, 3, 0, 0, 0, 4, 0, 5, 0, 6, 0, 0, 0}, false},
 		{"[4]int16", args{16, [][4]int16{{1, 2, 3, 4}, {5, 6, 7, 8}}}, buildBuffer2(2 * 4), false},
 		{"[2][2]int16", args{16, [][2][2]int16{
 			{{1, 3}, {2, 4}},
@@ -242,7 +249,7 @@ func TestWrite(t *testing.T) {
 		{"uint16", args{8, []uint16{1, 2, 3, 4}}, buildBuffer2(4), false},
 		{"uint16-FE", args{2, []uint16{65534}}, []byte{0xFE, 0xFF}, false},
 		{"[2]uint16", args{8, [][2]uint16{{1, 2}, {3, 4}}}, buildBuffer2(2 * 2), false},
-		{"[3]uint16", args{12, [][3]uint16{{1, 2, 3}, {4, 5, 6}}}, buildBuffer2(2 * 3), false},
+		{"[3]uint16", args{16, [][3]uint16{{1, 2, 3}, {4, 5, 6}}}, []byte{1, 0, 2, 0, 3, 0, 0, 0, 4, 0, 5, 0, 6, 0, 0, 0}, false},
 		{"[4]uint16", args{16, [][4]uint16{{1, 2, 3, 4}, {5, 6, 7, 8}}}, buildBuffer2(2 * 4), false},
 		{"[2][2]uint16", args{16, [][2][2]uint16{
 			{{1, 3}, {2, 4}},
@@ -290,6 +297,10 @@ func TestWrite(t *testing.T) {
 			{{1, 5, 9, 13}, {2, 6, 10, 14}, {3, 7, 11, 15}, {4, 8, 12, 16}},
 			{{17, 21, 25, 29}, {18, 22, 26, 30}, {19, 23, 27, 31}, {20, 24, 28, 32}},
 		}}, buildBufferF(32), false},
+		{"color.RGBA", args{8, []color.RGBA{{1, 2, 3, 4}, {5, 6, 7, 8}}}, buildBuffer1(2 * 4), false},
+		{"color.RGBA64", args{16, []color.RGBA64{{1, 2, 3, 4}, {5, 6, 7, 8}}}, buildBuffer2(2 * 4), false},
+		{"gltf.RGBA", args{32, []gltf.RGBA{{R: 1, G: 2, B: 3, A: 4}, {R: 5, G: 6, B: 7, A: 8}}}, buildBufferF(2 * 4), false},
+		{"gltf.RGB", args{24, []gltf.RGB{{R: 1, G: 2, B: 3}, {R: 4, G: 5, B: 6}}}, buildBufferF(2 * 3), false},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
diff --git a/binary/example_test.go b/binary/example_test.go
index f2110b2..e89335d 100644
--- a/binary/example_test.go
+++ b/binary/example_test.go
@@ -1,46 +1,46 @@
-package binary_test
-
-import (
-	"fmt"
-	"github.com/qmuntal/gltf"
-	"github.com/qmuntal/gltf/binary"
-)
-
-func ExampleWrite() {
-	// Define data
-	indices := []uint8{0, 1, 2, 3, 1, 0, 0, 2, 3, 1, 4, 2, 4, 3, 2, 4, 1, 3}
-	vertices := [][3]float32{{43, 43, 0}, {83, 43, 0}, {63, 63, 40}, {43, 83, 0}, {83, 83, 0}}
-
-	// Allocate buffer
-	sizeIndices := len(indices) * binary.SizeOfElement(gltf.UnsignedByte, gltf.Scalar)
-	sizeVertices := len(vertices) * binary.SizeOfElement(gltf.Float, gltf.Vec3)
-	b := make([]byte, sizeIndices+sizeVertices)
-
-	// Write
-	binary.Write(b, indices)
-	binary.Write(b[sizeIndices:], vertices)
-
-	fmt.Print(b)
-	// Output:
-	// [0 1 2 3 1 0 0 2 3 1 4 2 4 3 2 4 1 3 0 0 44 66 0 0 44 66 0 0 0 0 0 0 166 66 0 0 44 66 0 0 0 0 0 0 124 66 0 0 124 66 0 0 32 66 0 0 44 66 0 0 166 66 0 0 0 0 0 0 166 66 0 0 166 66 0 0 0 0]
-}
-
-func ExampleRead() {
-	// Allocate data
-	indices := make([]uint8, 18)
-	vertices := make([][3]float32, 5)
-
-	// Define buffer
-	b := []byte{0, 1, 2, 3, 1, 0, 0, 2, 3, 1, 4, 2, 4, 3, 2, 4, 1, 3, 0, 0, 44, 66, 0, 0, 44, 66, 0, 0, 0, 0, 0, 0, 166, 66, 0, 0, 44, 66, 0, 0, 0, 0, 0, 0, 124, 66, 0, 0, 124, 66, 0, 0, 32, 66, 0, 0, 44, 66, 0, 0, 166, 66, 0, 0, 0, 0, 0, 0, 166, 66, 0, 0, 166, 66, 0, 0, 0, 0}
-	sizeIndices := len(indices) * binary.SizeOfElement(gltf.UnsignedByte, gltf.Scalar)
-
-	// Write
-	binary.Read(b, indices)
-	binary.Read(b[sizeIndices:], vertices)
-
-	fmt.Println(indices)
-	fmt.Println(vertices)
-	// Output:
-	// [0 1 2 3 1 0 0 2 3 1 4 2 4 3 2 4 1 3]
-	// [[43 43 0] [83 43 0] [63 63 40] [43 83 0] [83 83 0]]
-}
+package binary_test
+
+import (
+	"fmt"
+	"github.com/qmuntal/gltf"
+	"github.com/qmuntal/gltf/binary"
+)
+
+func ExampleWrite() {
+	// Define data
+	indices := []uint8{0, 1, 2, 3, 1, 0, 0, 2, 3, 1, 4, 2, 4, 3, 2, 4, 1, 3}
+	vertices := [][3]float32{{43, 43, 0}, {83, 43, 0}, {63, 63, 40}, {43, 83, 0}, {83, 83, 0}}
+
+	// Allocate buffer
+	sizeIndices := len(indices) * binary.SizeOfElement(gltf.UnsignedByte, gltf.Scalar)
+	sizeVertices := len(vertices) * binary.SizeOfElement(gltf.Float, gltf.Vec3)
+	b := make([]byte, sizeIndices+sizeVertices)
+
+	// Write
+	binary.Write(b, indices)
+	binary.Write(b[sizeIndices:], vertices)
+
+	fmt.Print(b)
+	// Output:
+	// [0 1 2 3 1 0 0 2 3 1 4 2 4 3 2 4 1 3 0 0 44 66 0 0 44 66 0 0 0 0 0 0 166 66 0 0 44 66 0 0 0 0 0 0 124 66 0 0 124 66 0 0 32 66 0 0 44 66 0 0 166 66 0 0 0 0 0 0 166 66 0 0 166 66 0 0 0 0]
+}
+
+func ExampleRead() {
+	// Allocate data
+	indices := make([]uint8, 18)
+	vertices := make([][3]float32, 5)
+
+	// Define buffer
+	b := []byte{0, 1, 2, 3, 1, 0, 0, 2, 3, 1, 4, 2, 4, 3, 2, 4, 1, 3, 0, 0, 44, 66, 0, 0, 44, 66, 0, 0, 0, 0, 0, 0, 166, 66, 0, 0, 44, 66, 0, 0, 0, 0, 0, 0, 124, 66, 0, 0, 124, 66, 0, 0, 32, 66, 0, 0, 44, 66, 0, 0, 166, 66, 0, 0, 0, 0, 0, 0, 166, 66, 0, 0, 166, 66, 0, 0, 0, 0}
+	sizeIndices := len(indices) * binary.SizeOfElement(gltf.UnsignedByte, gltf.Scalar)
+
+	// Write
+	binary.Read(b, indices)
+	binary.Read(b[sizeIndices:], vertices)
+
+	fmt.Println(indices)
+	fmt.Println(vertices)
+	// Output:
+	// [0 1 2 3 1 0 0 2 3 1 4 2 4 3 2 4 1 3]
+	// [[43 43 0] [83 43 0] [63 63 40] [43 83 0] [83 83 0]]
+}
diff --git a/example_test.go b/example_test.go
index d6ad428..f8b2f78 100644
--- a/example_test.go
+++ b/example_test.go
@@ -47,7 +47,7 @@ func ExampleSave() {
 		Scene:    gltf.Index(0),
 		Scenes:   []gltf.Scene{{Name: "Root Scene", Nodes: []uint32{0}}},
 	}
-	if err := gltf.SaveBinary(doc, "./a.gltf"); err != nil {
+	if err := gltf.SaveBinary(doc, "./example.glb"); err != nil {
 		panic(err)
 	}
 }
diff --git a/marshal_test.go b/marshal_test.go
index 3a3c202..c0d7120 100644
--- a/marshal_test.go
+++ b/marshal_test.go
@@ -1,419 +1,419 @@
-package gltf
-
-import (
-	"encoding/json"
-	"reflect"
-	"testing"
-)
-
-func TestImage_MarshalData(t *testing.T) {
-	tests := []struct {
-		name    string
-		im      *Image
-		want    []uint8
-		wantErr bool
-	}{
-		{"error", &Image{URI: "data:image/png;base64,_"}, []uint8{}, true},
-		{"external", &Image{URI: "http://web.com"}, []uint8{}, false},
-		{"empty", &Image{URI: "data:image/png;base64,"}, []uint8{}, false},
-		{"empty", &Image{URI: "data:image/jpeg;base64,"}, []uint8{}, false},
-		{"test", &Image{URI: "data:image/png;base64,TEST"}, []uint8{76, 68, 147}, false},
-		{"complex", &Image{URI: "data:image/png;base64,YW55IGNhcm5hbCBwbGVhcw=="}, []uint8{97, 110, 121, 32, 99, 97, 114, 110, 97, 108, 32, 112, 108, 101, 97, 115}, false},
-	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			got, err := tt.im.MarshalData()
-			if (err != nil) != tt.wantErr {
-				t.Errorf("Image.MarshalData() error = %v, wantErr %v", err, tt.wantErr)
-				return
-			}
-			if !reflect.DeepEqual(got, tt.want) {
-				t.Errorf("Image.MarshalData() = %v, want %v", got, tt.want)
-			}
-		})
-	}
-}
-
-func TestNode_UnmarshalJSON(t *testing.T) {
-	type args struct {
-		data []byte
-	}
-	tests := []struct {
-		name    string
-		n       *Node
-		args    args
-		want    *Node
-		wantErr bool
-	}{
-		{"default", new(Node), args{[]byte("{}")}, &Node{
-			Matrix:   [16]float64{1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1},
-			Rotation: [4]float64{0, 0, 0, 1},
-			Scale:    [3]float64{1, 1, 1},
-		}, false},
-		{"nodefault", new(Node), args{[]byte(`{"matrix":[1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1],"rotation":[0,0,0,1],"scale":[1,1,1],"camera":0,"mesh":0,"skin":0}`)}, &Node{
-			Matrix:   [16]float64{1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1},
-			Rotation: [4]float64{0, 0, 0, 1},
-			Scale:    [3]float64{1, 1, 1},
-			Camera:   Index(0),
-			Mesh:     Index(0),
-			Skin:     Index(0),
-		}, false},
-		{"nodefault", new(Node), args{[]byte(`{"matrix":[1,2,2,0,0,1,3,4,0,0,1,0,5,0,0,5],"rotation":[1,2,3,4],"scale":[2,3,4],"camera":1,"mesh":2,"skin":3}`)}, &Node{
-			Matrix:   [16]float64{1, 2, 2, 0, 0, 1, 3, 4, 0, 0, 1, 0, 5, 0, 0, 5},
-			Rotation: [4]float64{1, 2, 3, 4},
-			Scale:    [3]float64{2, 3, 4},
-			Camera:   Index(1),
-			Mesh:     Index(2),
-			Skin:     Index(3),
-		}, false},
-	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			if err := tt.n.UnmarshalJSON(tt.args.data); (err != nil) != tt.wantErr {
-				t.Errorf("Node.UnmarshalJSON() error = %v, wantErr %v", err, tt.wantErr)
-				return
-			}
-			if !reflect.DeepEqual(tt.n, tt.want) {
-				t.Errorf("Node.UnmarshalJSON() = %v, want %v", tt.n, tt.want)
-			}
-		})
-	}
-}
-
-func TestMaterial_UnmarshalJSON(t *testing.T) {
-	type args struct {
-		data []byte
-	}
-	tests := []struct {
-		name    string
-		m       *Material
-		args    args
-		want    *Material
-		wantErr bool
-	}{
-		{"default", new(Material), args{[]byte("{}")}, &Material{AlphaCutoff: Float64(0.5), AlphaMode: Opaque}, false},
-		{"nodefault", new(Material), args{[]byte(`{"alphaCutoff": 0.2, "alphaMode": "MASK"}`)}, &Material{AlphaCutoff: Float64(0.2), AlphaMode: Mask}, false},
-	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			if err := tt.m.UnmarshalJSON(tt.args.data); (err != nil) != tt.wantErr {
-				t.Errorf("Material.UnmarshalJSON() error = %v, wantErr %v", err, tt.wantErr)
-				return
-			}
-			if !reflect.DeepEqual(tt.m, tt.want) {
-				t.Errorf("Material.UnmarshalJSON() = %v, want %v", tt.m, tt.want)
-			}
-		})
-	}
-}
-
-func TestNormalTexture_UnmarshalJSON(t *testing.T) {
-	type args struct {
-		data []byte
-	}
-	tests := []struct {
-		name    string
-		n       *NormalTexture
-		args    args
-		want    *NormalTexture
-		wantErr bool
-	}{
-		{"default", new(NormalTexture), args{[]byte("{}")}, &NormalTexture{Scale: Float64(1)}, false},
-		{"empty", new(NormalTexture), args{[]byte(`{"scale": 0, "index": 0}`)}, &NormalTexture{Scale: Float64(0), Index: Index(0)}, false},
-		{"nodefault", new(NormalTexture), args{[]byte(`{"scale": 0.5, "index":2}`)}, &NormalTexture{Scale: Float64(0.5), Index: Index(2)}, false},
-	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			if err := tt.n.UnmarshalJSON(tt.args.data); (err != nil) != tt.wantErr {
-				t.Errorf("NormalTexture.UnmarshalJSON() error = %v, wantErr %v", err, tt.wantErr)
-				return
-			}
-			if !reflect.DeepEqual(tt.n, tt.want) {
-				t.Errorf("NormalTexture.UnmarshalJSON() = %v, want %v", tt.n, tt.want)
-			}
-		})
-	}
-}
-
-func TestOcclusionTexture_UnmarshalJSON(t *testing.T) {
-	type args struct {
-		data []byte
-	}
-	tests := []struct {
-		name    string
-		o       *OcclusionTexture
-		args    args
-		want    *OcclusionTexture
-		wantErr bool
-	}{
-		{"default", new(OcclusionTexture), args{[]byte("{}")}, &OcclusionTexture{Strength: Float64(1)}, false},
-		{"empty", new(OcclusionTexture), args{[]byte(`{"strength": 0, "index": 0}`)}, &OcclusionTexture{Strength: Float64(0), Index: Index(0)}, false},
-		{"nodefault", new(OcclusionTexture), args{[]byte(`{"strength": 0.5, "index":2}`)}, &OcclusionTexture{Strength: Float64(0.5), Index: Index(2)}, false},
-	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			if err := tt.o.UnmarshalJSON(tt.args.data); (err != nil) != tt.wantErr {
-				t.Errorf("OcclusionTexture.UnmarshalJSON() error = %v, wantErr %v", err, tt.wantErr)
-				return
-			}
-			if !reflect.DeepEqual(tt.o, tt.want) {
-				t.Errorf("OcclusionTexture.UnmarshalJSON() = %v, want %v", tt.o, tt.want)
-			}
-		})
-	}
-}
-
-func TestPBRMetallicRoughness_UnmarshalJSON(t *testing.T) {
-	type args struct {
-		data []byte
-	}
-	tests := []struct {
-		name    string
-		p       *PBRMetallicRoughness
-		args    args
-		want    *PBRMetallicRoughness
-		wantErr bool
-	}{
-		{"default", new(PBRMetallicRoughness), args{[]byte("{}")}, &PBRMetallicRoughness{BaseColorFactor: NewRGBA(), MetallicFactor: Float64(1), RoughnessFactor: Float64(1)}, false},
-		{"nodefault", new(PBRMetallicRoughness), args{[]byte(`{"baseColorFactor": [0.1,0.2,0.6,0.7],"metallicFactor":0.5,"roughnessFactor":0.1}`)}, &PBRMetallicRoughness{
-			BaseColorFactor: &RGBA{R: 0.1, G: 0.2, B: 0.6, A: 0.7}, MetallicFactor: Float64(0.5), RoughnessFactor: Float64(0.1),
-		}, false},
-	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			if err := tt.p.UnmarshalJSON(tt.args.data); (err != nil) != tt.wantErr {
-				t.Errorf("PBRMetallicRoughness.UnmarshalJSON() error = %v, wantErr %v", err, tt.wantErr)
-				return
-			}
-			if !reflect.DeepEqual(tt.p, tt.want) {
-				t.Errorf("PBRMetallicRoughness.UnmarshalJSON() = %v, want %v", tt.p, tt.want)
-			}
-		})
-	}
-}
-
-func TestNode_MarshalJSON(t *testing.T) {
-	tests := []struct {
-		name    string
-		n       *Node
-		want    []byte
-		wantErr bool
-	}{
-		{"default", &Node{
-			Matrix:   [16]float64{1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1},
-			Rotation: [4]float64{0, 0, 0, 1},
-			Scale:    [3]float64{1, 1, 1},
-		}, []byte("{}"), false},
-		{"default2", &Node{
-			Matrix:   [16]float64{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
-			Rotation: [4]float64{0, 0, 0, 0},
-			Scale:    [3]float64{0, 0, 0},
-		}, []byte("{}"), false},
-		{"empty", &Node{
-			Matrix:   [16]float64{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
-			Rotation: [4]float64{0, 0, 0, 0},
-			Scale:    [3]float64{0, 0, 0},
-			Camera:   Index(0),
-			Skin:     Index(0),
-			Mesh:     Index(0),
-		}, []byte(`{"camera":0,"skin":0,"mesh":0}`), false},
-		{"nodefault", &Node{
-			Matrix:      [16]float64{1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
-			Rotation:    [4]float64{1, 0, 0, 0},
-			Scale:       [3]float64{1, 0, 0},
-			Translation: [3]float64{1, 0, 0},
-			Camera:      Index(1),
-			Skin:        Index(1),
-			Mesh:        Index(1),
-		}, []byte(`{"camera":1,"skin":1,"matrix":[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"mesh":1,"rotation":[1,0,0,0],"scale":[1,0,0],"translation":[1,0,0]}`), false},
-	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			got, err := tt.n.MarshalJSON()
-			if (err != nil) != tt.wantErr {
-				t.Errorf("Node.MarshalJSON() error = %v, wantErr %v", err, tt.wantErr)
-				return
-			}
-			if !reflect.DeepEqual(got, tt.want) {
-				t.Errorf("Node.MarshalJSON() = %v, want %v", string(got), string(tt.want))
-			}
-		})
-	}
-}
-
-func TestMaterial_MarshalJSON(t *testing.T) {
-	tests := []struct {
-		name    string
-		m       *Material
-		want    []byte
-		wantErr bool
-	}{
-		{"default", &Material{AlphaCutoff: Float64(0.5), AlphaMode: Opaque}, []byte(`{}`), false},
-		{"empty", &Material{AlphaMode: Blend}, []byte(`{"alphaMode":"BLEND"}`), false},
-		{"nodefault", &Material{AlphaCutoff: Float64(1), AlphaMode: Blend}, []byte(`{"alphaMode":"BLEND","alphaCutoff":1}`), false},
-	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			got, err := tt.m.MarshalJSON()
-			if (err != nil) != tt.wantErr {
-				t.Errorf("Material.MarshalJSON() error = %v, wantErr %v", err, tt.wantErr)
-				return
-			}
-			if !reflect.DeepEqual(got, tt.want) {
-				t.Errorf("Material.MarshalJSON() = %v, want %v", string(got), string(tt.want))
-			}
-		})
-	}
-}
-
-func TestNormalTexture_MarshalJSON(t *testing.T) {
-	tests := []struct {
-		name    string
-		n       *NormalTexture
-		want    []byte
-		wantErr bool
-	}{
-		{"default", &NormalTexture{Scale: Float64(1)}, []byte(`{}`), false},
-		{"empty", &NormalTexture{Index: Index(0)}, []byte(`{"index":0}`), false},
-		{"nodefault", &NormalTexture{Index: Index(1), Scale: Float64(0.5)}, []byte(`{"index":1,"scale":0.5}`), false},
-	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			got, err := tt.n.MarshalJSON()
-			if (err != nil) != tt.wantErr {
-				t.Errorf("NormalTexture.MarshalJSON() error = %v, wantErr %v", err, tt.wantErr)
-				return
-			}
-			if !reflect.DeepEqual(got, tt.want) {
-				t.Errorf("NormalTexture.MarshalJSON() = %v, want %v", string(got), string(tt.want))
-			}
-		})
-	}
-}
-
-func TestOcclusionTexture_MarshalJSON(t *testing.T) {
-	tests := []struct {
-		name    string
-		o       *OcclusionTexture
-		want    []byte
-		wantErr bool
-	}{
-		{"default", &OcclusionTexture{Strength: Float64(1)}, []byte(`{}`), false},
-		{"empty", &OcclusionTexture{Index: Index(0)}, []byte(`{"index":0}`), false},
-		{"nodefault", &OcclusionTexture{Index: Index(1), Strength: Float64(0.5)}, []byte(`{"index":1,"strength":0.5}`), false},
-	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			got, err := tt.o.MarshalJSON()
-			if (err != nil) != tt.wantErr {
-				t.Errorf("OcclusionTexture.MarshalJSON() error = %v, wantErr %v", err, tt.wantErr)
-				return
-			}
-			if !reflect.DeepEqual(got, tt.want) {
-				t.Errorf("OcclusionTexture.MarshalJSON() = %v, want %v", string(got), string(tt.want))
-			}
-		})
-	}
-}
-
-func TestPBRMetallicRoughness_MarshalJSON(t *testing.T) {
-	tests := []struct {
-		name    string
-		p       *PBRMetallicRoughness
-		want    []byte
-		wantErr bool
-	}{
-		{"default", &PBRMetallicRoughness{MetallicFactor: Float64(1), RoughnessFactor: Float64(1), BaseColorFactor: NewRGBA()}, []byte(`{}`), false},
-		{"empty", &PBRMetallicRoughness{MetallicFactor: Float64(0), RoughnessFactor: Float64(0)}, []byte(`{"metallicFactor":0,"roughnessFactor":0}`), false},
-		{"nodefault", &PBRMetallicRoughness{MetallicFactor: Float64(0.5), RoughnessFactor: Float64(0.5), BaseColorFactor: &RGBA{R: 1, G: 0.5, B: 1, A: 1}}, []byte(`{"baseColorFactor":[1,0.5,1,1],"metallicFactor":0.5,"roughnessFactor":0.5}`), false},
-	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			got, err := tt.p.MarshalJSON()
-			if (err != nil) != tt.wantErr {
-				t.Errorf("PBRMetallicRoughness.MarshalJSON() error = %v, wantErr %v", err, tt.wantErr)
-				return
-			}
-			if !reflect.DeepEqual(got, tt.want) {
-				t.Errorf("PBRMetallicRoughness.MarshalJSON() = %v, want %v", string(got), string(tt.want))
-			}
-		})
-	}
-}
-
-func TestCamera_MarshalJSON(t *testing.T) {
-	tests := []struct {
-		name    string
-		c       *Camera
-		want    []byte
-		wantErr bool
-	}{
-		{"empty", &Camera{}, nil, true},
-	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			got, err := tt.c.MarshalJSON()
-			if (err != nil) != tt.wantErr {
-				t.Errorf("Camera.MarshalJSON() error = %v, wantErr %v", err, tt.wantErr)
-				return
-			}
-			if !reflect.DeepEqual(got, tt.want) {
-				t.Errorf("Camera.MarshalJSON() = %v, want %v", got, tt.want)
-			}
-		})
-	}
-}
-
-type fakeExt struct {
-	A int `json:"a"`
-}
-
-func (f *fakeExt) UnmarshalJSON(data []byte) error {
-	type alias fakeExt
-	tmp := alias(fakeExt{})
-	err := json.Unmarshal(data, &tmp)
-	if err == nil {
-		*f = fakeExt(tmp)
-	}
-	return err
-}
-
-func TestExtensions_UnmarshalJSON(t *testing.T) {
-	RegisterExtension("fake_ext", func(data []byte) (interface{}, error) {
-		e := new(fakeExt)
-		err := json.Unmarshal(data, e)
-		return e, err
-	})
-	type args struct {
-		data []byte
-	}
-	tests := []struct {
-		name    string
-		args    args
-		want    *Extensions
-		wantErr bool
-	}{
-		{"fake_ext", args{[]byte(`{"fake_ext": {"a":2}}`)}, &Extensions{
-			"fake_ext": &fakeExt{A: 2},
-		}, false},
-		{"err", args{[]byte(`{"fake_ext": {{"a":2}}`)}, &Extensions{}, true},
-		{"errext", args{[]byte(`{"fake_ext": {"a":"incorrect"}}`)}, &Extensions{
-			"fake_ext": json.RawMessage([]byte(`{"a":"incorrect"}`)),
-		}, false},
-		{"noregistered", args{[]byte(`{"fake_ext_1": {"a":"incorrect"}}`)}, &Extensions{
-			"fake_ext_1": json.RawMessage([]byte(`{"a":"incorrect"}`)),
-		}, false},
-	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			ext := Extensions{}
-			if err := ext.UnmarshalJSON(tt.args.data); (err != nil) != tt.wantErr {
-				t.Errorf("Extensions.UnmarshalJSON() error = %v, wantErr %v", err, tt.wantErr)
-				return
-			}
-			if !reflect.DeepEqual(&ext, tt.want) {
-				t.Errorf("Extensions.UnmarshalJSON() = %v, want %v", &ext, tt.want)
-			}
-		})
-	}
-}
+package gltf
+
+import (
+	"encoding/json"
+	"reflect"
+	"testing"
+)
+
+func TestImage_MarshalData(t *testing.T) {
+	tests := []struct {
+		name    string
+		im      *Image
+		want    []uint8
+		wantErr bool
+	}{
+		{"error", &Image{URI: "data:image/png;base64,_"}, []uint8{}, true},
+		{"external", &Image{URI: "http://web.com"}, []uint8{}, false},
+		{"empty", &Image{URI: "data:image/png;base64,"}, []uint8{}, false},
+		{"empty", &Image{URI: "data:image/jpeg;base64,"}, []uint8{}, false},
+		{"test", &Image{URI: "data:image/png;base64,TEST"}, []uint8{76, 68, 147}, false},
+		{"complex", &Image{URI: "data:image/png;base64,YW55IGNhcm5hbCBwbGVhcw=="}, []uint8{97, 110, 121, 32, 99, 97, 114, 110, 97, 108, 32, 112, 108, 101, 97, 115}, false},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := tt.im.MarshalData()
+			if (err != nil) != tt.wantErr {
+				t.Errorf("Image.MarshalData() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("Image.MarshalData() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func TestNode_UnmarshalJSON(t *testing.T) {
+	type args struct {
+		data []byte
+	}
+	tests := []struct {
+		name    string
+		n       *Node
+		args    args
+		want    *Node
+		wantErr bool
+	}{
+		{"default", new(Node), args{[]byte("{}")}, &Node{
+			Matrix:   [16]float64{1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1},
+			Rotation: [4]float64{0, 0, 0, 1},
+			Scale:    [3]float64{1, 1, 1},
+		}, false},
+		{"nodefault", new(Node), args{[]byte(`{"matrix":[1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1],"rotation":[0,0,0,1],"scale":[1,1,1],"camera":0,"mesh":0,"skin":0}`)}, &Node{
+			Matrix:   [16]float64{1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1},
+			Rotation: [4]float64{0, 0, 0, 1},
+			Scale:    [3]float64{1, 1, 1},
+			Camera:   Index(0),
+			Mesh:     Index(0),
+			Skin:     Index(0),
+		}, false},
+		{"nodefault", new(Node), args{[]byte(`{"matrix":[1,2,2,0,0,1,3,4,0,0,1,0,5,0,0,5],"rotation":[1,2,3,4],"scale":[2,3,4],"camera":1,"mesh":2,"skin":3}`)}, &Node{
+			Matrix:   [16]float64{1, 2, 2, 0, 0, 1, 3, 4, 0, 0, 1, 0, 5, 0, 0, 5},
+			Rotation: [4]float64{1, 2, 3, 4},
+			Scale:    [3]float64{2, 3, 4},
+			Camera:   Index(1),
+			Mesh:     Index(2),
+			Skin:     Index(3),
+		}, false},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if err := tt.n.UnmarshalJSON(tt.args.data); (err != nil) != tt.wantErr {
+				t.Errorf("Node.UnmarshalJSON() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(tt.n, tt.want) {
+				t.Errorf("Node.UnmarshalJSON() = %v, want %v", tt.n, tt.want)
+			}
+		})
+	}
+}
+
+func TestMaterial_UnmarshalJSON(t *testing.T) {
+	type args struct {
+		data []byte
+	}
+	tests := []struct {
+		name    string
+		m       *Material
+		args    args
+		want    *Material
+		wantErr bool
+	}{
+		{"default", new(Material), args{[]byte("{}")}, &Material{AlphaCutoff: Float64(0.5), AlphaMode: Opaque}, false},
+		{"nodefault", new(Material), args{[]byte(`{"alphaCutoff": 0.2, "alphaMode": "MASK"}`)}, &Material{AlphaCutoff: Float64(0.2), AlphaMode: Mask}, false},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if err := tt.m.UnmarshalJSON(tt.args.data); (err != nil) != tt.wantErr {
+				t.Errorf("Material.UnmarshalJSON() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(tt.m, tt.want) {
+				t.Errorf("Material.UnmarshalJSON() = %v, want %v", tt.m, tt.want)
+			}
+		})
+	}
+}
+
+func TestNormalTexture_UnmarshalJSON(t *testing.T) {
+	type args struct {
+		data []byte
+	}
+	tests := []struct {
+		name    string
+		n       *NormalTexture
+		args    args
+		want    *NormalTexture
+		wantErr bool
+	}{
+		{"default", new(NormalTexture), args{[]byte("{}")}, &NormalTexture{Scale: Float64(1)}, false},
+		{"empty", new(NormalTexture), args{[]byte(`{"scale": 0, "index": 0}`)}, &NormalTexture{Scale: Float64(0), Index: Index(0)}, false},
+		{"nodefault", new(NormalTexture), args{[]byte(`{"scale": 0.5, "index":2}`)}, &NormalTexture{Scale: Float64(0.5), Index: Index(2)}, false},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if err := tt.n.UnmarshalJSON(tt.args.data); (err != nil) != tt.wantErr {
+				t.Errorf("NormalTexture.UnmarshalJSON() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(tt.n, tt.want) {
+				t.Errorf("NormalTexture.UnmarshalJSON() = %v, want %v", tt.n, tt.want)
+			}
+		})
+	}
+}
+
+func TestOcclusionTexture_UnmarshalJSON(t *testing.T) {
+	type args struct {
+		data []byte
+	}
+	tests := []struct {
+		name    string
+		o       *OcclusionTexture
+		args    args
+		want    *OcclusionTexture
+		wantErr bool
+	}{
+		{"default", new(OcclusionTexture), args{[]byte("{}")}, &OcclusionTexture{Strength: Float64(1)}, false},
+		{"empty", new(OcclusionTexture), args{[]byte(`{"strength": 0, "index": 0}`)}, &OcclusionTexture{Strength: Float64(0), Index: Index(0)}, false},
+		{"nodefault", new(OcclusionTexture), args{[]byte(`{"strength": 0.5, "index":2}`)}, &OcclusionTexture{Strength: Float64(0.5), Index: Index(2)}, false},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if err := tt.o.UnmarshalJSON(tt.args.data); (err != nil) != tt.wantErr {
+				t.Errorf("OcclusionTexture.UnmarshalJSON() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(tt.o, tt.want) {
+				t.Errorf("OcclusionTexture.UnmarshalJSON() = %v, want %v", tt.o, tt.want)
+			}
+		})
+	}
+}
+
+func TestPBRMetallicRoughness_UnmarshalJSON(t *testing.T) {
+	type args struct {
+		data []byte
+	}
+	tests := []struct {
+		name    string
+		p       *PBRMetallicRoughness
+		args    args
+		want    *PBRMetallicRoughness
+		wantErr bool
+	}{
+		{"default", new(PBRMetallicRoughness), args{[]byte("{}")}, &PBRMetallicRoughness{BaseColorFactor: NewRGBA(), MetallicFactor: Float64(1), RoughnessFactor: Float64(1)}, false},
+		{"nodefault", new(PBRMetallicRoughness), args{[]byte(`{"baseColorFactor": [0.1,0.2,0.6,0.7],"metallicFactor":0.5,"roughnessFactor":0.1}`)}, &PBRMetallicRoughness{
+			BaseColorFactor: &RGBA{R: 0.1, G: 0.2, B: 0.6, A: 0.7}, MetallicFactor: Float64(0.5), RoughnessFactor: Float64(0.1),
+		}, false},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if err := tt.p.UnmarshalJSON(tt.args.data); (err != nil) != tt.wantErr {
+				t.Errorf("PBRMetallicRoughness.UnmarshalJSON() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(tt.p, tt.want) {
+				t.Errorf("PBRMetallicRoughness.UnmarshalJSON() = %v, want %v", tt.p, tt.want)
+			}
+		})
+	}
+}
+
+func TestNode_MarshalJSON(t *testing.T) {
+	tests := []struct {
+		name    string
+		n       *Node
+		want    []byte
+		wantErr bool
+	}{
+		{"default", &Node{
+			Matrix:   [16]float64{1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1},
+			Rotation: [4]float64{0, 0, 0, 1},
+			Scale:    [3]float64{1, 1, 1},
+		}, []byte("{}"), false},
+		{"default2", &Node{
+			Matrix:   [16]float64{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
+			Rotation: [4]float64{0, 0, 0, 0},
+			Scale:    [3]float64{0, 0, 0},
+		}, []byte("{}"), false},
+		{"empty", &Node{
+			Matrix:   [16]float64{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
+			Rotation: [4]float64{0, 0, 0, 0},
+			Scale:    [3]float64{0, 0, 0},
+			Camera:   Index(0),
+			Skin:     Index(0),
+			Mesh:     Index(0),
+		}, []byte(`{"camera":0,"skin":0,"mesh":0}`), false},
+		{"nodefault", &Node{
+			Matrix:      [16]float64{1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
+			Rotation:    [4]float64{1, 0, 0, 0},
+			Scale:       [3]float64{1, 0, 0},
+			Translation: [3]float64{1, 0, 0},
+			Camera:      Index(1),
+			Skin:        Index(1),
+			Mesh:        Index(1),
+		}, []byte(`{"camera":1,"skin":1,"matrix":[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"mesh":1,"rotation":[1,0,0,0],"scale":[1,0,0],"translation":[1,0,0]}`), false},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := tt.n.MarshalJSON()
+			if (err != nil) != tt.wantErr {
+				t.Errorf("Node.MarshalJSON() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("Node.MarshalJSON() = %v, want %v", string(got), string(tt.want))
+			}
+		})
+	}
+}
+
+func TestMaterial_MarshalJSON(t *testing.T) {
+	tests := []struct {
+		name    string
+		m       *Material
+		want    []byte
+		wantErr bool
+	}{
+		{"default", &Material{AlphaCutoff: Float64(0.5), AlphaMode: Opaque}, []byte(`{}`), false},
+		{"empty", &Material{AlphaMode: Blend}, []byte(`{"alphaMode":"BLEND"}`), false},
+		{"nodefault", &Material{AlphaCutoff: Float64(1), AlphaMode: Blend}, []byte(`{"alphaMode":"BLEND","alphaCutoff":1}`), false},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := tt.m.MarshalJSON()
+			if (err != nil) != tt.wantErr {
+				t.Errorf("Material.MarshalJSON() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("Material.MarshalJSON() = %v, want %v", string(got), string(tt.want))
+			}
+		})
+	}
+}
+
+func TestNormalTexture_MarshalJSON(t *testing.T) {
+	tests := []struct {
+		name    string
+		n       *NormalTexture
+		want    []byte
+		wantErr bool
+	}{
+		{"default", &NormalTexture{Scale: Float64(1)}, []byte(`{}`), false},
+		{"empty", &NormalTexture{Index: Index(0)}, []byte(`{"index":0}`), false},
+		{"nodefault", &NormalTexture{Index: Index(1), Scale: Float64(0.5)}, []byte(`{"index":1,"scale":0.5}`), false},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := tt.n.MarshalJSON()
+			if (err != nil) != tt.wantErr {
+				t.Errorf("NormalTexture.MarshalJSON() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("NormalTexture.MarshalJSON() = %v, want %v", string(got), string(tt.want))
+			}
+		})
+	}
+}
+
+func TestOcclusionTexture_MarshalJSON(t *testing.T) {
+	tests := []struct {
+		name    string
+		o       *OcclusionTexture
+		want    []byte
+		wantErr bool
+	}{
+		{"default", &OcclusionTexture{Strength: Float64(1)}, []byte(`{}`), false},
+		{"empty", &OcclusionTexture{Index: Index(0)}, []byte(`{"index":0}`), false},
+		{"nodefault", &OcclusionTexture{Index: Index(1), Strength: Float64(0.5)}, []byte(`{"index":1,"strength":0.5}`), false},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := tt.o.MarshalJSON()
+			if (err != nil) != tt.wantErr {
+				t.Errorf("OcclusionTexture.MarshalJSON() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("OcclusionTexture.MarshalJSON() = %v, want %v", string(got), string(tt.want))
+			}
+		})
+	}
+}
+
+func TestPBRMetallicRoughness_MarshalJSON(t *testing.T) {
+	tests := []struct {
+		name    string
+		p       *PBRMetallicRoughness
+		want    []byte
+		wantErr bool
+	}{
+		{"default", &PBRMetallicRoughness{MetallicFactor: Float64(1), RoughnessFactor: Float64(1), BaseColorFactor: NewRGBA()}, []byte(`{}`), false},
+		{"empty", &PBRMetallicRoughness{MetallicFactor: Float64(0), RoughnessFactor: Float64(0)}, []byte(`{"metallicFactor":0,"roughnessFactor":0}`), false},
+		{"nodefault", &PBRMetallicRoughness{MetallicFactor: Float64(0.5), RoughnessFactor: Float64(0.5), BaseColorFactor: &RGBA{R: 1, G: 0.5, B: 1, A: 1}}, []byte(`{"baseColorFactor":[1,0.5,1,1],"metallicFactor":0.5,"roughnessFactor":0.5}`), false},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := tt.p.MarshalJSON()
+			if (err != nil) != tt.wantErr {
+				t.Errorf("PBRMetallicRoughness.MarshalJSON() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("PBRMetallicRoughness.MarshalJSON() = %v, want %v", string(got), string(tt.want))
+			}
+		})
+	}
+}
+
+func TestCamera_MarshalJSON(t *testing.T) {
+	tests := []struct {
+		name    string
+		c       *Camera
+		want    []byte
+		wantErr bool
+	}{
+		{"empty", &Camera{}, nil, true},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := tt.c.MarshalJSON()
+			if (err != nil) != tt.wantErr {
+				t.Errorf("Camera.MarshalJSON() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("Camera.MarshalJSON() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+type fakeExt struct {
+	A int `json:"a"`
+}
+
+func (f *fakeExt) UnmarshalJSON(data []byte) error {
+	type alias fakeExt
+	tmp := alias(fakeExt{})
+	err := json.Unmarshal(data, &tmp)
+	if err == nil {
+		*f = fakeExt(tmp)
+	}
+	return err
+}
+
+func TestExtensions_UnmarshalJSON(t *testing.T) {
+	RegisterExtension("fake_ext", func(data []byte) (interface{}, error) {
+		e := new(fakeExt)
+		err := json.Unmarshal(data, e)
+		return e, err
+	})
+	type args struct {
+		data []byte
+	}
+	tests := []struct {
+		name    string
+		args    args
+		want    *Extensions
+		wantErr bool
+	}{
+		{"fake_ext", args{[]byte(`{"fake_ext": {"a":2}}`)}, &Extensions{
+			"fake_ext": &fakeExt{A: 2},
+		}, false},
+		{"err", args{[]byte(`{"fake_ext": {{"a":2}}`)}, &Extensions{}, true},
+		{"errext", args{[]byte(`{"fake_ext": {"a":"incorrect"}}`)}, &Extensions{
+			"fake_ext": json.RawMessage([]byte(`{"a":"incorrect"}`)),
+		}, false},
+		{"noregistered", args{[]byte(`{"fake_ext_1": {"a":"incorrect"}}`)}, &Extensions{
+			"fake_ext_1": json.RawMessage([]byte(`{"a":"incorrect"}`)),
+		}, false},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			ext := Extensions{}
+			if err := ext.UnmarshalJSON(tt.args.data); (err != nil) != tt.wantErr {
+				t.Errorf("Extensions.UnmarshalJSON() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(&ext, tt.want) {
+				t.Errorf("Extensions.UnmarshalJSON() = %v, want %v", &ext, tt.want)
+			}
+		})
+	}
+}
diff --git a/modeler/compression_test.go b/modeler/compression_test.go
new file mode 100644
index 0000000..e029ba3
--- /dev/null
+++ b/modeler/compression_test.go
@@ -0,0 +1,55 @@
+package modeler
+
+import (
+	"math"
+	"reflect"
+	"testing"
+)
+
+func Test_compressUint32(t *testing.T) {
+	type args struct {
+		data []uint32
+	}
+	tests := []struct {
+		name string
+		args args
+		want interface{}
+	}{
+		{"empty", args{[]uint32{}}, []uint8{}},
+		{"32", args{[]uint32{1, math.MaxUint16}}, []uint32{1, math.MaxUint16}},
+		{"32-2", args{[]uint32{1, math.MaxUint8, math.MaxUint16}}, []uint32{1, math.MaxUint8, math.MaxUint16}},
+		{"32-3", args{[]uint32{1, math.MaxUint16, math.MaxUint8}}, []uint32{1, math.MaxUint16, math.MaxUint8}},
+		{"32-4", args{[]uint32{1, math.MaxUint8}}, []uint16{1, math.MaxUint8}},
+		{"16", args{[]uint32{1, math.MaxUint16 - 1}}, []uint16{1, math.MaxUint16 - 1}},
+		{"8", args{[]uint32{1, math.MaxUint8 - 1}}, []uint8{1, math.MaxUint8 - 1}},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := compressUint32(tt.args.data); !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("compressUint32() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func Test_compressUint16(t *testing.T) {
+	type args struct {
+		data []uint16
+	}
+	tests := []struct {
+		name string
+		args args
+		want interface{}
+	}{
+		{"empty", args{[]uint16{}}, []uint8{}},
+		{"16", args{[]uint16{1, math.MaxUint8}}, []uint16{1, math.MaxUint8}},
+		{"8", args{[]uint16{1, math.MaxUint8 - 1}}, []uint8{1, math.MaxUint8 - 1}},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := compressUint16(tt.args.data); !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("compressUint16() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
diff --git a/modeler/example_test.go b/modeler/example_test.go
new file mode 100644
index 0000000..178e5e4
--- /dev/null
+++ b/modeler/example_test.go
@@ -0,0 +1,30 @@
+package modeler_test
+
+import (
+	"github.com/qmuntal/gltf"
+	"github.com/qmuntal/gltf/modeler"
+)
+
+func Example() {
+	m := modeler.NewModeler()
+	positionAccessor := m.AddPosition(0, [][3]float32{{43, 43, 0}, {83, 43, 0}, {63, 63, 40}, {43, 83, 0}, {83, 83, 0}})
+	indicesAccessor := m.AddIndices(0, []uint8{0, 1, 2, 3, 1, 0, 0, 2, 3, 1, 4, 2, 4, 3, 2, 4, 1, 3})
+	colorIndices := m.AddColor(0, [][3]uint8{{50, 155, 255}, {0, 100, 200}, {255, 155, 50}, {155, 155, 155}, {25, 25, 25}})
+	m.Document.Meshes = []gltf.Mesh{{
+		Name: "Pyramid",
+		Primitives: []gltf.Primitive{
+			{
+				Indices: gltf.Index(indicesAccessor),
+				Attributes: map[string]uint32{
+					"POSITION": positionAccessor,
+					"COLOR_0":  colorIndices,
+				},
+			},
+		},
+	}}
+	m.Nodes = []gltf.Node{{Name: "Root", Mesh: gltf.Index(0)}}
+	m.Scenes[0].Nodes = append(m.Scenes[0].Nodes, 0)
+	if err := gltf.SaveBinary(m.Document, "./example.glb"); err != nil {
+		panic(err)
+	}
+}
diff --git a/modeler/modeler_test.go b/modeler/modeler_test.go
new file mode 100644
index 0000000..3fa898f
--- /dev/null
+++ b/modeler/modeler_test.go
@@ -0,0 +1,625 @@
+package modeler
+
+import (
+	"bytes"
+	"errors"
+	"io"
+	"reflect"
+	"testing"
+
+	"github.com/go-test/deep"
+	"github.com/qmuntal/gltf"
+)
+
+func TestNewModeler(t *testing.T) {
+	tests := []struct {
+		name string
+		want *Modeler
+	}{
+		{"base", &Modeler{Document: &gltf.Document{
+			Scene:  gltf.Index(0),
+			Scenes: []gltf.Scene{{Name: "Root Scene"}},
+		}, Compression: CompressionLossless}},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := NewModeler(); !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("NewModeler() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func TestModeler_AddNormal(t *testing.T) {
+	type args struct {
+		bufferIndex uint32
+		data        [][3]float32
+	}
+	tests := []struct {
+		name    string
+		m       *Modeler
+		args    args
+		want    uint32
+		wantDoc *gltf.Document
+	}{
+		{"base", &Modeler{Document: &gltf.Document{
+			Accessors: []gltf.Accessor{{}},
+			Buffers:   []gltf.Buffer{{ByteLength: 10}},
+		}}, args{0, [][3]float32{{1, 2, 3}}}, 1, &gltf.Document{
+			Accessors: []gltf.Accessor{
+				{},
+				{BufferView: gltf.Index(0), Count: 1, Type: gltf.Vec3, ComponentType: gltf.Float},
+			},
+			BufferViews: []gltf.BufferView{
+				{ByteLength: 12, Target: gltf.ArrayBuffer, ByteStride: 12},
+			},
+			Buffers: []gltf.Buffer{
+				{ByteLength: 22, Data: []byte{0, 0, 128, 63, 0, 0, 0, 64, 0, 0, 64, 64}},
+			},
+		}},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got := tt.m.AddNormal(tt.args.bufferIndex, tt.args.data)
+			if tt.want != got {
+				t.Errorf("Modeler.AddNormal() = %v, want %v", got, tt.want)
+				return
+			}
+			if diff := deep.Equal(tt.m.Document, tt.wantDoc); diff != nil {
+				t.Errorf("Modeler.AddNormal() = %v", diff)
+				return
+			}
+		})
+	}
+}
+
+func TestModeler_AddTangent(t *testing.T) {
+	type args struct {
+		bufferIndex uint32
+		data        [][4]float32
+	}
+	tests := []struct {
+		name    string
+		m       *Modeler
+		args    args
+		want    uint32
+		wantDoc *gltf.Document
+	}{
+		{"base", &Modeler{Document: &gltf.Document{
+			Accessors: []gltf.Accessor{{}},
+			Buffers:   []gltf.Buffer{{ByteLength: 10}},
+		}}, args{0, [][4]float32{{1, 2, 3, 4}, {}}}, 1, &gltf.Document{
+			Accessors: []gltf.Accessor{
+				{},
+				{BufferView: gltf.Index(0), Count: 2, Type: gltf.Vec4, ComponentType: gltf.Float},
+			},
+			BufferViews: []gltf.BufferView{
+				{ByteLength: 32, Target: gltf.ArrayBuffer, ByteStride: 16},
+			},
+			Buffers: []gltf.Buffer{
+				{ByteLength: 42, Data: []byte{0, 0, 128, 63, 0, 0, 0, 64, 0, 0, 64, 64, 0, 0, 128, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}},
+			},
+		}},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got := tt.m.AddTangent(tt.args.bufferIndex, tt.args.data)
+			if tt.want != got {
+				t.Errorf("Modeler.AddTangent() = %v, want %v", got, tt.want)
+				return
+			}
+			if diff := deep.Equal(tt.m.Document, tt.wantDoc); diff != nil {
+				t.Errorf("Modeler.AddTangent() = %v", diff)
+				return
+			}
+		})
+	}
+}
+
+func TestModeler_AddPosition(t *testing.T) {
+	type args struct {
+		bufferIndex uint32
+		data        [][3]float32
+	}
+	tests := []struct {
+		name    string
+		m       *Modeler
+		args    args
+		want    uint32
+		wantDoc *gltf.Document
+	}{
+		{"base", &Modeler{Document: &gltf.Document{
+			Accessors: []gltf.Accessor{{}},
+			Buffers:   []gltf.Buffer{{ByteLength: 10}},
+		}}, args{0, [][3]float32{{1, 2, 3}, {0, 0, -1}}}, 1, &gltf.Document{
+			Accessors: []gltf.Accessor{
+				{},
+				{BufferView: gltf.Index(0), Count: 2, Type: gltf.Vec3, ComponentType: gltf.Float, Max: []float64{1, 2, 3}, Min: []float64{0, 0, -1}},
+			},
+			BufferViews: []gltf.BufferView{
+				{ByteLength: 24, Target: gltf.ArrayBuffer, ByteStride: 12},
+			},
+			Buffers: []gltf.Buffer{
+				{ByteLength: 34, Data: []byte{0, 0, 128, 63, 0, 0, 0, 64, 0, 0, 64, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 191}},
+			},
+		}},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got := tt.m.AddPosition(tt.args.bufferIndex, tt.args.data)
+			if tt.want != got {
+				t.Errorf("Modeler.AddPosition() = %v, want %v", got, tt.want)
+				return
+			}
+			if diff := deep.Equal(tt.m.Document, tt.wantDoc); diff != nil {
+				t.Errorf("Modeler.AddPosition() = %v", diff)
+				return
+			}
+		})
+	}
+}
+
+func TestModeler_AddJoints(t *testing.T) {
+	type args struct {
+		bufferIndex uint32
+		data        interface{}
+	}
+	tests := []struct {
+		name    string
+		m       *Modeler
+		args    args
+		want    uint32
+		wantDoc *gltf.Document
+	}{
+		{"uint8", &Modeler{Document: &gltf.Document{
+			Accessors: []gltf.Accessor{{}},
+			Buffers:   []gltf.Buffer{{ByteLength: 10}},
+		}}, args{0, [][4]uint8{{1, 2, 3, 4}}}, 1, &gltf.Document{
+			Accessors: []gltf.Accessor{
+				{},
+				{BufferView: gltf.Index(0), Count: 1, Type: gltf.Vec4, ComponentType: gltf.UnsignedByte},
+			},
+			BufferViews: []gltf.BufferView{
+				{ByteLength: 4, Target: gltf.ArrayBuffer, ByteStride: 4},
+			},
+			Buffers: []gltf.Buffer{
+				{ByteLength: 14, Data: []byte{1, 2, 3, 4}},
+			},
+		}},
+		{"uint16", &Modeler{Document: &gltf.Document{
+			Accessors: []gltf.Accessor{{}},
+			Buffers:   []gltf.Buffer{{ByteLength: 10}},
+		}}, args{0, [][4]uint16{{1, 2, 3, 4}}}, 1, &gltf.Document{
+			Accessors: []gltf.Accessor{
+				{},
+				{BufferView: gltf.Index(0), Count: 1, Type: gltf.Vec4, ComponentType: gltf.UnsignedShort},
+			},
+			BufferViews: []gltf.BufferView{
+				{ByteLength: 8, Target: gltf.ArrayBuffer, ByteStride: 8},
+			},
+			Buffers: []gltf.Buffer{
+				{ByteLength: 18, Data: []byte{1, 0, 2, 0, 3, 0, 4, 0}},
+			},
+		}},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got := tt.m.AddJoints(tt.args.bufferIndex, tt.args.data)
+			if tt.want != got {
+				t.Errorf("Modeler.AddJoints() = %v, want %v", got, tt.want)
+				return
+			}
+			if diff := deep.Equal(tt.m.Document, tt.wantDoc); diff != nil {
+				t.Errorf("Modeler.AddJoints() = %v", diff)
+				return
+			}
+		})
+	}
+}
+
+func TestModeler_AddWeights(t *testing.T) {
+	type args struct {
+		bufferIndex uint32
+		data        interface{}
+	}
+	tests := []struct {
+		name    string
+		m       *Modeler
+		args    args
+		want    uint32
+		wantDoc *gltf.Document
+	}{
+		{"uint8", &Modeler{Document: &gltf.Document{
+			Accessors: []gltf.Accessor{{}},
+			Buffers:   []gltf.Buffer{{ByteLength: 10}},
+		}}, args{0, [][4]uint8{{1, 2, 3, 4}}}, 1, &gltf.Document{
+			Accessors: []gltf.Accessor{
+				{},
+				{BufferView: gltf.Index(0), Count: 1, Type: gltf.Vec4, ComponentType: gltf.UnsignedByte, Normalized: true},
+			},
+			BufferViews: []gltf.BufferView{
+				{ByteLength: 4, Target: gltf.ArrayBuffer, ByteStride: 4},
+			},
+			Buffers: []gltf.Buffer{
+				{ByteLength: 14, Data: []byte{1, 2, 3, 4}},
+			},
+		}},
+		{"uint16", &Modeler{Document: &gltf.Document{
+			Accessors: []gltf.Accessor{{}},
+			Buffers:   []gltf.Buffer{{ByteLength: 10}},
+		}}, args{0, [][4]uint16{{1, 2, 3, 4}}}, 1, &gltf.Document{
+			Accessors: []gltf.Accessor{
+				{},
+				{BufferView: gltf.Index(0), Count: 1, Type: gltf.Vec4, ComponentType: gltf.UnsignedShort, Normalized: true},
+			},
+			BufferViews: []gltf.BufferView{
+				{ByteLength: 8, Target: gltf.ArrayBuffer, ByteStride: 8},
+			},
+			Buffers: []gltf.Buffer{
+				{ByteLength: 18, Data: []byte{1, 0, 2, 0, 3, 0, 4, 0}},
+			},
+		}},
+		{"float", &Modeler{Document: &gltf.Document{
+			Accessors: []gltf.Accessor{{}},
+			Buffers:   []gltf.Buffer{{ByteLength: 10}},
+		}}, args{0, [][4]float32{{1, 2, 3, 4}, {}}}, 1, &gltf.Document{
+			Accessors: []gltf.Accessor{
+				{},
+				{BufferView: gltf.Index(0), Count: 2, Type: gltf.Vec4, ComponentType: gltf.Float},
+			},
+			BufferViews: []gltf.BufferView{
+				{ByteLength: 32, Target: gltf.ArrayBuffer, ByteStride: 16},
+			},
+			Buffers: []gltf.Buffer{
+				{ByteLength: 42, Data: []byte{0, 0, 128, 63, 0, 0, 0, 64, 0, 0, 64, 64, 0, 0, 128, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}},
+			},
+		}},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got := tt.m.AddWeights(tt.args.bufferIndex, tt.args.data)
+			if tt.want != got {
+				t.Errorf("Modeler.AddWeights() = %v, want %v", got, tt.want)
+				return
+			}
+			if diff := deep.Equal(tt.m.Document, tt.wantDoc); diff != nil {
+				t.Errorf("Modeler.AddWeights() = %v", diff)
+				return
+			}
+		})
+	}
+}
+
+func TestModeler_AddTextureCoord(t *testing.T) {
+	type args struct {
+		bufferIndex uint32
+		data        interface{}
+	}
+	tests := []struct {
+		name    string
+		m       *Modeler
+		args    args
+		want    uint32
+		wantDoc *gltf.Document
+	}{
+		{"uint8", &Modeler{Document: &gltf.Document{
+			Accessors: []gltf.Accessor{{}},
+			Buffers:   []gltf.Buffer{{ByteLength: 10}},
+		}}, args{0, [][2]uint8{{1, 2}}}, 1, &gltf.Document{
+			Accessors: []gltf.Accessor{
+				{},
+				{BufferView: gltf.Index(0), Count: 1, Type: gltf.Vec2, ComponentType: gltf.UnsignedByte, Normalized: true},
+			},
+			BufferViews: []gltf.BufferView{
+				{ByteLength: 4, Target: gltf.ArrayBuffer, ByteStride: 4},
+			},
+			Buffers: []gltf.Buffer{
+				{ByteLength: 14, Data: []byte{1, 2, 0, 0}},
+			},
+		}},
+		{"uint16", &Modeler{Document: &gltf.Document{
+			Accessors: []gltf.Accessor{{}},
+			Buffers:   []gltf.Buffer{{ByteLength: 10}},
+		}}, args{0, [][2]uint16{{1, 2}}}, 1, &gltf.Document{
+			Accessors: []gltf.Accessor{
+				{},
+				{BufferView: gltf.Index(0), Count: 1, Type: gltf.Vec2, ComponentType: gltf.UnsignedShort, Normalized: true},
+			},
+			BufferViews: []gltf.BufferView{
+				{ByteLength: 4, Target: gltf.ArrayBuffer, ByteStride: 4},
+			},
+			Buffers: []gltf.Buffer{
+				{ByteLength: 14, Data: []byte{1, 0, 2, 0}},
+			},
+		}},
+		{"float", &Modeler{Document: &gltf.Document{
+			Accessors: []gltf.Accessor{{}},
+			Buffers:   []gltf.Buffer{{ByteLength: 10}},
+		}}, args{2, [][2]float32{{1, 2}, {}}}, 1, &gltf.Document{
+			Accessors: []gltf.Accessor{
+				{},
+				{BufferView: gltf.Index(0), Count: 2, Type: gltf.Vec2, ComponentType: gltf.Float},
+			},
+			BufferViews: []gltf.BufferView{
+				{ByteLength: 16, Target: gltf.ArrayBuffer, Buffer: 2, ByteStride: 8},
+			},
+			Buffers: []gltf.Buffer{
+				{ByteLength: 10},
+				{},
+				{ByteLength: 16, Data: []byte{0, 0, 128, 63, 0, 0, 0, 64, 0, 0, 0, 0, 0, 0, 0, 0}},
+			},
+		}},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got := tt.m.AddTextureCoord(tt.args.bufferIndex, tt.args.data)
+			if tt.want != got {
+				t.Errorf("Modeler.AddTextureCoord() = %v, want %v", got, tt.want)
+				return
+			}
+			if diff := deep.Equal(tt.m.Document, tt.wantDoc); diff != nil {
+				t.Errorf("Modeler.AddTextureCoord() = %v", diff)
+				return
+			}
+		})
+	}
+}
+
+func TestModeler_AddIndices(t *testing.T) {
+	type args struct {
+		bufferIndex uint32
+		data        interface{}
+	}
+	tests := []struct {
+		name    string
+		m       *Modeler
+		args    args
+		want    uint32
+		wantDoc *gltf.Document
+	}{
+		{"uint8", &Modeler{Document: &gltf.Document{
+			Accessors: []gltf.Accessor{{}},
+			Buffers:   []gltf.Buffer{{ByteLength: 10}},
+		}}, args{0, []uint8{1, 2}}, 1, &gltf.Document{
+			Accessors: []gltf.Accessor{
+				{},
+				{BufferView: gltf.Index(0), Count: 2, Type: gltf.Scalar, ComponentType: gltf.UnsignedByte},
+			},
+			BufferViews: []gltf.BufferView{
+				{ByteLength: 2, Target: gltf.ElementArrayBuffer},
+			},
+			Buffers: []gltf.Buffer{
+				{ByteLength: 12, Data: []byte{1, 2}},
+			},
+		}},
+		{"uint16", &Modeler{Document: &gltf.Document{
+			Accessors: []gltf.Accessor{{}},
+			Buffers:   []gltf.Buffer{{ByteLength: 10}},
+		}}, args{0, []uint16{1, 2}}, 1, &gltf.Document{
+			Accessors: []gltf.Accessor{
+				{},
+				{BufferView: gltf.Index(0), Count: 2, Type: gltf.Scalar, ComponentType: gltf.UnsignedShort},
+			},
+			BufferViews: []gltf.BufferView{
+				{ByteLength: 4, Target: gltf.ElementArrayBuffer},
+			},
+			Buffers: []gltf.Buffer{
+				{ByteLength: 14, Data: []byte{1, 0, 2, 0}},
+			},
+		}},
+		{"uint16-compress", &Modeler{Document: &gltf.Document{
+			Accessors: []gltf.Accessor{{}},
+			Buffers:   []gltf.Buffer{{ByteLength: 10}},
+		}, Compression: CompressionLossless}, args{0, []uint16{1, 2}}, 1, &gltf.Document{
+			Accessors: []gltf.Accessor{
+				{},
+				{BufferView: gltf.Index(0), Count: 2, Type: gltf.Scalar, ComponentType: gltf.UnsignedByte},
+			},
+			BufferViews: []gltf.BufferView{
+				{ByteLength: 2, Target: gltf.ElementArrayBuffer},
+			},
+			Buffers: []gltf.Buffer{
+				{ByteLength: 12, Data: []byte{1, 2}},
+			},
+		}},
+		{"uint32", &Modeler{Document: &gltf.Document{
+			Accessors: []gltf.Accessor{{}},
+			Buffers:   []gltf.Buffer{{ByteLength: 10}},
+		}}, args{0, []uint32{1, 2}}, 1, &gltf.Document{
+			Accessors: []gltf.Accessor{
+				{},
+				{BufferView: gltf.Index(0), Count: 2, Type: gltf.Scalar, ComponentType: gltf.UnsignedInt},
+			},
+			BufferViews: []gltf.BufferView{
+				{ByteLength: 8, Target: gltf.ElementArrayBuffer},
+			},
+			Buffers: []gltf.Buffer{
+				{ByteLength: 18, Data: []byte{1, 0, 0, 0, 2, 0, 0, 0}},
+			},
+		}},
+		{"uint32-compress", &Modeler{Document: &gltf.Document{
+			Accessors: []gltf.Accessor{{}},
+			Buffers:   []gltf.Buffer{{ByteLength: 10}},
+		}, Compression: CompressionLossless}, args{0, []uint32{1, 2}}, 1, &gltf.Document{
+			Accessors: []gltf.Accessor{
+				{},
+				{BufferView: gltf.Index(0), Count: 2, Type: gltf.Scalar, ComponentType: gltf.UnsignedByte},
+			},
+			BufferViews: []gltf.BufferView{
+				{ByteLength: 2, Target: gltf.ElementArrayBuffer},
+			},
+			Buffers: []gltf.Buffer{
+				{ByteLength: 12, Data: []byte{1, 2}},
+			},
+		}},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got := tt.m.AddIndices(tt.args.bufferIndex, tt.args.data)
+			if tt.want != got {
+				t.Errorf("Modeler.AddIndices() = %v, want %v", got, tt.want)
+				return
+			}
+			if diff := deep.Equal(tt.m.Document, tt.wantDoc); diff != nil {
+				t.Errorf("Modeler.AddIndices() = %v", diff)
+				return
+			}
+		})
+	}
+}
+
+func TestModeler_AddColor(t *testing.T) {
+	type args struct {
+		bufferIndex uint32
+		data        interface{}
+	}
+	tests := []struct {
+		name    string
+		m       *Modeler
+		args    args
+		want    uint32
+		wantDoc *gltf.Document
+	}{
+		{"uint8", &Modeler{Document: &gltf.Document{
+			Accessors: []gltf.Accessor{{}},
+			Buffers:   []gltf.Buffer{{ByteLength: 10}},
+		}}, args{0, [][4]uint8{{1, 2, 3, 4}}}, 1, &gltf.Document{
+			Accessors: []gltf.Accessor{
+				{},
+				{BufferView: gltf.Index(0), Count: 1, Type: gltf.Vec4, ComponentType: gltf.UnsignedByte, Normalized: true},
+			},
+			BufferViews: []gltf.BufferView{
+				{ByteLength: 4, Target: gltf.ArrayBuffer, ByteStride: 4},
+			},
+			Buffers: []gltf.Buffer{
+				{ByteLength: 14, Data: []byte{1, 2, 3, 4}},
+			},
+		}},
+		{"uint16", &Modeler{Document: &gltf.Document{
+			Accessors: []gltf.Accessor{{}},
+			Buffers:   []gltf.Buffer{{ByteLength: 10}},
+		}}, args{0, [][4]uint16{{1, 2, 3, 4}}}, 1, &gltf.Document{
+			Accessors: []gltf.Accessor{
+				{},
+				{BufferView: gltf.Index(0), Count: 1, Type: gltf.Vec4, ComponentType: gltf.UnsignedShort, Normalized: true},
+			},
+			BufferViews: []gltf.BufferView{
+				{ByteLength: 8, Target: gltf.ArrayBuffer, ByteStride: 8},
+			},
+			Buffers: []gltf.Buffer{
+				{ByteLength: 18, Data: []byte{1, 0, 2, 0, 3, 0, 4, 0}},
+			},
+		}},
+		{"float", &Modeler{Document: &gltf.Document{
+			Accessors: []gltf.Accessor{{}},
+		}}, args{0, [][4]float32{{1, 2, 3, 4}, {}}}, 1, &gltf.Document{
+			Accessors: []gltf.Accessor{
+				{},
+				{BufferView: gltf.Index(0), Count: 2, Type: gltf.Vec4, ComponentType: gltf.Float},
+			},
+			BufferViews: []gltf.BufferView{
+				{ByteLength: 32, Target: gltf.ArrayBuffer, ByteStride: 16},
+			},
+			Buffers: []gltf.Buffer{
+				{ByteLength: 32, Data: []byte{0, 0, 128, 63, 0, 0, 0, 64, 0, 0, 64, 64, 0, 0, 128, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}},
+			},
+		}},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got := tt.m.AddColor(tt.args.bufferIndex, tt.args.data)
+			if tt.want != got {
+				t.Errorf("Modeler.AddColor() = %v, want %v", got, tt.want)
+				return
+			}
+			if diff := deep.Equal(tt.m.Document, tt.wantDoc); diff != nil {
+				t.Errorf("Modeler.AddColor() = %v", diff)
+				return
+			}
+		})
+	}
+}
+
+func TestModeler_AddImage(t *testing.T) {
+	type args struct {
+		bufferIndex uint32
+		name        string
+		mimeType    string
+		r           io.Reader
+	}
+	tests := []struct {
+		name    string
+		m       *Modeler
+		args    args
+		want    uint32
+		wantDoc *gltf.Document
+		wantErr bool
+	}{
+		{"base", &Modeler{Document: &gltf.Document{
+			Images:  []gltf.Image{{}},
+			Buffers: []gltf.Buffer{{ByteLength: 10}},
+		}}, args{0, "fake", "fake/type", bytes.NewReader([]byte{1, 2})}, 1, &gltf.Document{
+			Images: []gltf.Image{
+				{},
+				{BufferView: gltf.Index(0), Name: "fake", MimeType: "fake/type"},
+			},
+			BufferViews: []gltf.BufferView{
+				{ByteLength: 2, Target: gltf.None},
+			},
+			Buffers: []gltf.Buffer{
+				{ByteLength: 12, Data: []byte{1, 2}},
+			},
+		}, false},
+		{"buffer", &Modeler{Document: &gltf.Document{
+			Images:  []gltf.Image{{}},
+			Buffers: []gltf.Buffer{{ByteLength: 10}},
+		}}, args{0, "fake", "fake/type", bytes.NewBuffer([]byte{1, 2})}, 1, &gltf.Document{
+			Images: []gltf.Image{
+				{},
+				{BufferView: gltf.Index(0), Name: "fake", MimeType: "fake/type"},
+			},
+			BufferViews: []gltf.BufferView{
+				{ByteLength: 2, Target: gltf.None},
+			},
+			Buffers: []gltf.Buffer{
+				{ByteLength: 12, Data: []byte{1, 2}},
+			},
+		}, false},
+		{"err", &Modeler{Document: &gltf.Document{
+			Images:  []gltf.Image{{}},
+			Buffers: []gltf.Buffer{{ByteLength: 10}},
+		}}, args{0, "fake", "fake/type", &errReader{}}, 0, &gltf.Document{
+			Images: []gltf.Image{
+				{},
+			},
+			Buffers: []gltf.Buffer{
+				{ByteLength: 10},
+			},
+		}, true},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := tt.m.AddImage(tt.args.bufferIndex, tt.args.name, tt.args.mimeType, tt.args.r)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("Modeler.AddImage() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if tt.want != got {
+				t.Errorf("Modeler.AddImage() = %v, want %v", got, tt.want)
+				return
+			}
+			if diff := deep.Equal(tt.m.Document, tt.wantDoc); diff != nil {
+				t.Errorf("Modeler.AddImage() = %v", diff)
+				return
+			}
+		})
+	}
+}
+
+type errReader struct {
+	r io.Reader
+}
+
+func (r *errReader) Read(p []byte) (int, error) {
+	return 0, errors.New("")
+}