Repository: qmuntal/gltf
Pull request #16 (2019-11-10 10:39:53)

Lights puntual
- Support KHR_lights_punctual
- Improve extensions documentation
- Refactor the extension mechanism to support dynamic extension structs (breaking change)


Patch:
diff --git a/README.md b/README.md
index 668c1a1..2f458ec 100644
--- a/README.md
+++ b/README.md
@@ -12,32 +12,57 @@ A Go package for simple, efficient, and robust serialization/deserialization of
 
 ## Features
 
-* High parsing speed and moderate memory consumption.
+* High parsing speed and moderate memory consumption
 * glTF specification v2.0.0
-  * [x] ASCII glTF.
-  * [x] Binary glTF(GLB).
-  * [x] PBR material description.
+  * [x] ASCII glTF
+  * [x] Binary glTF(GLB)
+  * [x] PBR material description
 * glTF validaton
-  * [x] Validate against schemas.
-  * [ ] Validate coherence.
+  * [x] Validate against schemas
+  * [ ] Validate coherence
 * Buffers
-  * [x] Parse BASE64 encoded embedded buffer data(DataURI).
-  * [x] Load .bin file.
+  * [x] Parse BASE64 encoded embedded buffer data(DataURI)
+  * [x] Load .bin file
 * Read from io.Reader
-  * [x] Boilerplate for disk loading.
-  * [x] Custom callback handlers.
-  * [x] Automatic ASCII / glTF detection.
+  * [x] Boilerplate for disk loading
+  * [x] Custom callback handlers
+  * [x] Automatic ASCII / glTF detection
 * Write to io.Writer
-  * [x] Boilerplate for disk saving.
-  * [x] Custom callback handlers.
+  * [x] Boilerplate for disk saving
+  * [x] Custom callback handlers
   * [x] ASCII / Binary
 * Extensions
   * [ ] KHR_draco_mesh_compression
-  * [ ] KHR_lights_punctual
+  * [x] KHR_lights_punctual
   * [x] KHR_materials_pbrSpecularGlossiness
   * [x] KHR_materials_unlit
   * [ ] KHR_techniques_webgl
   * [x] KHR_texture_transform
+  * [x] Support custom extensions
+
+## Extensions
+
+This module is designed to support dynamic extensions. By default only the core specification is decoded and the data inside the extensions objects are stored as `json.RawMessage` so they can be decoded outside this package or automatically encoded when saving the document.
+
+To decode one of the supported extensions the only required action is to import the associated package, this way the extension will not be stored as `json.RawMessage` but as the type defined in the extension package:
+
+```go
+import (
+  "github.com/qmuntal/gltf"
+  "github.com/qmuntal/gltf/lightspuntual"
+)
+
+func ExampleExension() {
+  doc, _ := gltf.Open("...")
+  if v, ok := doc.Extensions[lightspuntual.ExtensionName]; ok {
+      for _, l := range v.(lightspuntual.Lights) {
+          fmt.Print(l.Type)
+      }
+  }
+}
+```
+
+To support custom extensions you should use the `gltf.RegisterExtension` method.
 
 ## Perfomance
 
diff --git a/lightspuntual/lightspuntual.go b/lightspuntual/lightspuntual.go
new file mode 100644
index 0000000..7d2fb77
--- /dev/null
+++ b/lightspuntual/lightspuntual.go
@@ -0,0 +1,115 @@
+// This extension defines three "punctual" light types: directional, point and spot.
+// Punctual lights are defined as parameterized, infinitely small points
+// that emit light in well-defined directions and intensities.
+package lightspuntual
+
+import (
+	"encoding/json"
+	"math"
+
+	"github.com/qmuntal/gltf"
+)
+
+const (
+	// ExtensionName defines the KHR_lights_punctual unique key.
+	ExtensionName = "KHR_lights_punctual"
+)
+
+func init() {
+	gltf.RegisterExtension(ExtensionName, Unmarshal)
+}
+
+type envelop struct {
+	Lights Lights
+	Light  *LightIndex
+}
+
+// Unmarshal decodes the json data into the correct type.
+func Unmarshal(data []byte) (interface{}, error) {
+	var env envelop
+	if err := json.Unmarshal([]byte(data), &env); err != nil {
+		return nil, err
+	}
+	if env.Light != nil {
+		return *env.Light, nil
+	}
+	return env.Lights, nil
+}
+
+const (
+	// TypeDirectional lights act as though they are infinitely far away and emit light in the direction of the local -z axis.
+	TypeDirectional = "directional"
+	// TypePoint lights emit light in all directions from their position in space.
+	TypePoint = "point"
+	// TypeSpot lights emit light in a cone in the direction of the local -z axis.
+	TypeSpot = "spot"
+)
+
+// LightIndex is the id of the light referenced by this node.
+type LightIndex uint32
+
+// Spot defines the spot cone.
+type Spot struct {
+	InnerConeAngle float64  `json:"innerConeAngle,omitempty"`
+	OuterConeAngle *float64 `json:"outerConeAngle,omitempty"`
+}
+
+// OuterConeAngleOrDefault returns the OuterConeAngle if it is not nil, else return the default one.
+func (s *Spot) OuterConeAngleOrDefault() float64 {
+	if s.OuterConeAngle == nil {
+		return math.Pi / 4
+	}
+	return *s.OuterConeAngle
+}
+
+// UnmarshalJSON unmarshal the spot with the correct default values.
+func (s *Spot) UnmarshalJSON(data []byte) error {
+	type alias Spot
+	tmp := alias(Spot{OuterConeAngle: gltf.Float64(math.Pi / 4)})
+	err := json.Unmarshal(data, &tmp)
+	if err == nil {
+		*s = Spot(tmp)
+	}
+	return err
+}
+
+// Lights defines a list of Lights.
+type Lights []*Light
+
+// Light defines a directional, point, or spot light.
+// When a light's type is spot, the spot property on the light is required.
+type Light struct {
+	Type      string    `json:"type"`
+	Name      string    `json:"name,omitempty"`
+	Color     *gltf.RGB `json:"color,omitempty"`
+	Intensity *float64  `json:"intensity,omitempty"`
+	Range     *float64  `json:"range,omitempty"`
+	Spot      *Spot     `json:"spot,omitempty"`
+}
+
+// IntensityOrDefault returns the itensity if it is not nil, else return the default one.
+func (l *Light) IntensityOrDefault() float64 {
+	if l.Intensity == nil {
+		return 1
+	}
+	return *l.Intensity
+}
+
+// ColorOrDefault returns the color if it is not nil, else return the default one.
+func (l *Light) ColorOrDefault() gltf.RGB {
+	if l.Color == nil {
+		return *gltf.NewRGB()
+	}
+	return *l.Color
+}
+
+// UnmarshalJSON unmarshal the light with the correct default values.
+func (l *Light) UnmarshalJSON(data []byte) error {
+	type alias Light
+	tmp := alias(Light{Color: gltf.NewRGB(), Intensity: gltf.Float64(1), Range: gltf.Float64(math.Inf(0))})
+	err := json.Unmarshal(data, &tmp)
+	if err == nil {
+		*l = Light(tmp)
+	}
+	return err
+}
diff --git a/marshal.go b/marshal.go
index ee9c1f4..4b577f3 100644
--- a/marshal.go
+++ b/marshal.go
@@ -224,8 +224,7 @@ func (ext *Extensions) UnmarshalJSON(data []byte) error {
 	if err == nil {
 		for key, value := range raw {
 			if extFactory, ok := extensions[key]; ok {
-				n := extFactory()
-				err := json.Unmarshal(value, n)
+				n, err := extFactory(value)
 				if err != nil {
 					(*ext)[key] = value
 				} else {
diff --git a/specular/specular.go b/specular/specular.go
index 7eb8374..d3ba46c 100644
--- a/specular/specular.go
+++ b/specular/specular.go
@@ -8,17 +8,19 @@ import (
 )
 
 const (
-	// ExtPBRSpecularGlossiness defines the PBRSpecularGlossiness unique key.
-	ExtPBRSpecularGlossiness = "KHR_materials_pbrSpecularGlossiness"
+	// ExtensionName defines the PBRSpecularGlossiness unique key.
+	ExtensionName = "KHR_materials_pbrSpecularGlossiness"
 )
 
-// New returns a new specular.PBRSpecularGlossiness.
-func New() json.Unmarshaler {
-	return new(PBRSpecularGlossiness)
+// Unmarshal decodes the json data into the correct type.
+func Unmarshal(data []byte) (interface{}, error) {
+	pbr := new(PBRSpecularGlossiness)
+	err := json.Unmarshal(data, pbr)
+	return pbr, err
 }
 
 func init() {
-	gltf.RegisterExtension(ExtPBRSpecularGlossiness, New)
+	gltf.RegisterExtension(ExtensionName, Unmarshal)
 }
 
 // PBRSpecularGlossiness defines a specular-glossiness material model.
diff --git a/struct.go b/struct.go
index 105020e..13b9ddb 100644
--- a/struct.go
+++ b/struct.go
@@ -2,7 +2,6 @@ package gltf
 
 import (
 	"encoding/base64"
-	"encoding/json"
 	"fmt"
 	"strings"
 )
@@ -467,11 +466,11 @@ type ChannelTarget struct {
 // If a key does not match with any of the supported extensions the value will be a json.RawMessage so its decoding can be delayed.
 type Extensions map[string]interface{}
 
-var extensions = make(map[string]func() json.Unmarshaler)
+var extensions = make(map[string]func([]byte) (interface{}, error))
 
 // RegisterExtension registers a function that returns a new extension of the given
 // byte array. This is intended to be called from the init function in
 // packages that implement extensions.
-func RegisterExtension(key string, f func() json.Unmarshaler) {
+func RegisterExtension(key string, f func([]byte) (interface{}, error)) {
 	extensions[key] = f
 }
diff --git a/texturetransform/transform.go b/texturetransform/transform.go
index b9804df..53f384b 100644
--- a/texturetransform/transform.go
+++ b/texturetransform/transform.go
@@ -15,17 +15,19 @@ var (
 )
 
 const (
-	// ExtTextureTransform defines the ExtTextureTransform unique key.
-	ExtTextureTransform = "KHR_texture_transform"
+	// ExtensionName defines the ExtTextureTransform unique key.
+	ExtensionName = "KHR_texture_transform"
 )
 
-// New returns a new TextureTranform.
-func New() json.Unmarshaler {
-	return new(TextureTranform)
+// Unmarshal decodes the json data into the correct type.
+func Unmarshal(data []byte) (interface{}, error) {
+	t := new(TextureTranform)
+	err := json.Unmarshal(data, t)
+	return t, err
 }
 
 func init() {
-	gltf.RegisterExtension(ExtTextureTransform, New)
+	gltf.RegisterExtension(ExtensionName, Unmarshal)
 }
 
 // TextureTranform can be used in textureInfo to pack many low-res texture into a single large texture atlas.
diff --git a/unlit/unlit.go b/unlit/unlit.go
index a399014..385c2b9 100644
--- a/unlit/unlit.go
+++ b/unlit/unlit.go
@@ -7,17 +7,19 @@ import (
 )
 
 const (
-	// ExtUnlit defines the Unlit unique key.
-	ExtUnlit = "KHR_materials_unlit"
+	// ExtensionName defines the Unlit unique key.
+	ExtensionName = "KHR_materials_unlit"
 )
 
-// New returns a new unlit.Unlit.
-func New() json.Unmarshaler {
-	return new(Unlit)
+// Unmarshal decodes the json data into the correct type.
+func Unmarshal(data []byte) (interface{}, error) {
+	u := new(Unlit)
+	err := json.Unmarshal(data, u)
+	return u, err
 }
 
 func init() {
-	gltf.RegisterExtension(ExtUnlit, New)
+	gltf.RegisterExtension(ExtensionName, Unmarshal)
 }
 
 // Unlit defines an unlit shading model.


Test patch:
diff --git a/lightspuntual/lightspuntual_test.go b/lightspuntual/lightspuntual_test.go
new file mode 100644
index 0000000..a635568
--- /dev/null
+++ b/lightspuntual/lightspuntual_test.go
@@ -0,0 +1,152 @@
+package lightspuntual
+
+import (
+	"math"
+	"reflect"
+	"testing"
+
+	"github.com/go-test/deep"
+	"github.com/qmuntal/gltf"
+)
+
+func TestLight_IntensityOrDefault(t *testing.T) {
+	tests := []struct {
+		name string
+		l    *Light
+		want float64
+	}{
+		{"empty", &Light{}, 1},
+		{"other", &Light{Intensity: gltf.Float64(0.5)}, 0.5},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := tt.l.IntensityOrDefault(); got != tt.want {
+				t.Errorf("Light.IntensityOrDefault() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func TestLight_ColorOrDefault(t *testing.T) {
+	tests := []struct {
+		name string
+		l    *Light
+		want gltf.RGB
+	}{
+		{"empty", &Light{}, *gltf.NewRGB()},
+		{"other", &Light{Color: &gltf.RGB{R: 0.8, G: 0.8, B: 0.8}}, gltf.RGB{R: 0.8, G: 0.8, B: 0.8}},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := tt.l.ColorOrDefault(); !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("Light.ColorOrDefault() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func TestSpot_OuterConeAngleOrDefault(t *testing.T) {
+	tests := []struct {
+		name string
+		s    *Spot
+		want float64
+	}{
+		{"empty", &Spot{}, math.Pi / 4},
+		{"other", &Spot{OuterConeAngle: gltf.Float64(0.5)}, 0.5},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := tt.s.OuterConeAngleOrDefault(); got != tt.want {
+				t.Errorf("Spot.OuterConeAngleOrDefault() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func TestLight_UnmarshalJSON(t *testing.T) {
+	type args struct {
+		data []byte
+	}
+	tests := []struct {
+		name    string
+		l       *Light
+		args    args
+		want    *Light
+		wantErr bool
+	}{
+		{"default", new(Light), args{[]byte("{}")}, &Light{
+			Color: gltf.NewRGB(), Intensity: gltf.Float64(1), Range: gltf.Float64(math.Inf(0)),
+		}, false},
+		{"nodefault", new(Light), args{[]byte(`{
+			"color": [0.3, 0.7, 1.0],
+			"name": "AAA",
+			"intensity": 40.0,
+			"type": "spot",
+			"range": 10.0,
+			"spot": {
+			  "innerConeAngle": 1.0,
+			  "outerConeAngle": 2.0
+			}
+		  }`)}, &Light{
+			Name: "AAA", Type: "spot", Color: &gltf.RGB{R: 0.3, G: 0.7, B: 1.0}, Intensity: gltf.Float64(40), Range: gltf.Float64(10),
+			Spot: &Spot{
+				InnerConeAngle: 1.0,
+				OuterConeAngle: gltf.Float64(2.0),
+			},
+		}, false},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if err := tt.l.UnmarshalJSON(tt.args.data); (err != nil) != tt.wantErr {
+				t.Errorf("Light.UnmarshalJSON() error = %v, wantErr %v", err, tt.wantErr)
+			}
+			if !reflect.DeepEqual(tt.l, tt.want) {
+				t.Errorf("Light.UnmarshalJSON() = %+v, want %+v", tt.l, tt.want)
+			}
+		})
+	}
+}
+
+func TestUnmarshal(t *testing.T) {
+	type args struct {
+		data []byte
+	}
+	tests := []struct {
+		name    string
+		args    args
+		want    interface{}
+		wantErr bool
+	}{
+		{"error", args{[]byte(`{"light: 1}`)}, nil, true},
+		{"index", args{[]byte(`{"light": 1}`)}, LightIndex(1), false},
+		{"lights", args{[]byte(`{"lights": [
+			{
+			  "color": [1.0, 0.9, 0.7],
+			  "name": "Directional",
+			  "intensity": 3.0,
+			  "type": "directional"
+			},
+			{
+			  "color": [1.0, 0.0, 0.0],
+			  "name": "Point",
+			  "intensity": 20.0,
+			  "type": "point"
+			}
+		  ]}`)}, Lights{
+			{Color: &gltf.RGB{R: 1.0, G: 0.9, B: 0.7}, Name: "Directional", Intensity: gltf.Float64(3.0), Type: "directional", Range: gltf.Float64(math.Inf(0))},
+			{Color: &gltf.RGB{R: 1.0}, Name: "Point", Intensity: gltf.Float64(20.0), Type: "point", Range: gltf.Float64(math.Inf(0))},
+		}, false},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := Unmarshal(tt.args.data)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("Unmarshal() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if diff := deep.Equal(got, tt.want); diff != nil {
+				t.Errorf("Unmarshal() = %v", diff)
+			}
+		})
+	}
+}
diff --git a/marshal_test.go b/marshal_test.go
index 392a432..3a3c202 100644
--- a/marshal_test.go
+++ b/marshal_test.go
@@ -379,7 +379,11 @@ func (f *fakeExt) UnmarshalJSON(data []byte) error {
 }
 
 func TestExtensions_UnmarshalJSON(t *testing.T) {
-	RegisterExtension("fake_ext", func() json.Unmarshaler { return new(fakeExt) })
+	RegisterExtension("fake_ext", func(data []byte) (interface{}, error) {
+		e := new(fakeExt)
+		err := json.Unmarshal(data, e)
+		return e, err
+	})
 	type args struct {
 		data []byte
 	}
diff --git a/specular/specular_test.go b/specular/specular_test.go
index 1dd0a12..bcb9521 100644
--- a/specular/specular_test.go
+++ b/specular/specular_test.go
@@ -1,7 +1,6 @@
 package specular
 
 import (
-	"encoding/json"
 	"reflect"
 	"testing"
 
@@ -62,17 +61,27 @@ func TestPBRSpecularGlossiness_MarshalJSON(t *testing.T) {
 	}
 }
 
-func TestNew(t *testing.T) {
+func TestUnmarshal(t *testing.T) {
+	type args struct {
+		data []byte
+	}
 	tests := []struct {
-		name string
-		want json.Unmarshaler
+		name    string
+		args    args
+		want    interface{}
+		wantErr bool
 	}{
-		{"base", new(PBRSpecularGlossiness)},
+		{"base", args{[]byte("{}")}, &PBRSpecularGlossiness{DiffuseFactor: gltf.NewRGBA(), SpecularFactor: gltf.NewRGB(), GlossinessFactor: gltf.Float64(1)}, false},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			if got := New(); !reflect.DeepEqual(got, tt.want) {
-				t.Errorf("New() = %v, want %v", got, tt.want)
+			got, err := Unmarshal(tt.args.data)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("Unmarshal() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("Unmarshal() = %v, want %v", got, tt.want)
 			}
 		})
 	}
diff --git a/texturetransform/transform_test.go b/texturetransform/transform_test.go
index e067090..42bef41 100644
--- a/texturetransform/transform_test.go
+++ b/texturetransform/transform_test.go
@@ -1,29 +1,12 @@
 package texturetransform
 
 import (
-	"encoding/json"
 	"reflect"
 	"testing"
 
 	"github.com/qmuntal/gltf"
 )
 
-func TestNew(t *testing.T) {
-	tests := []struct {
-		name string
-		want json.Unmarshaler
-	}{
-		{"base", new(TextureTranform)},
-	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			if got := New(); !reflect.DeepEqual(got, tt.want) {
-				t.Errorf("New() = %v, want %v", got, tt.want)
-			}
-		})
-	}
-}
-
 func TestTextureTranform_ScaleOrDefault(t *testing.T) {
 	tests := []struct {
 		name string
@@ -96,3 +79,29 @@ func TestTextureTranform_MarshalJSON(t *testing.T) {
 		})
 	}
 }
+
+func TestUnmarshal(t *testing.T) {
+	type args struct {
+		data []byte
+	}
+	tests := []struct {
+		name    string
+		args    args
+		want    interface{}
+		wantErr bool
+	}{
+		{"base", args{[]byte("{}")}, &TextureTranform{Scale: DefaultScale}, false},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := Unmarshal(tt.args.data)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("Unmarshal() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("Unmarshal() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
diff --git a/unlit/unlit_test.go b/unlit/unlit_test.go
index 14a9ab0..f5ea30e 100644
--- a/unlit/unlit_test.go
+++ b/unlit/unlit_test.go
@@ -6,22 +6,6 @@ import (
 	"testing"
 )
 
-func TestNew(t *testing.T) {
-	tests := []struct {
-		name string
-		want json.Unmarshaler
-	}{
-		{"base", new(Unlit)},
-	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			if got := New(); !reflect.DeepEqual(got, tt.want) {
-				t.Errorf("New() = %v, want %v", got, tt.want)
-			}
-		})
-	}
-}
-
 func TestUnlit_UnmarshalJSON(t *testing.T) {
 	type args struct {
 		data []byte
@@ -51,3 +35,29 @@ func TestUnlit_UnmarshalJSON(t *testing.T) {
 		})
 	}
 }
+
+func TestUnmarshal(t *testing.T) {
+	type args struct {
+		data []byte
+	}
+	tests := []struct {
+		name    string
+		args    args
+		want    interface{}
+		wantErr bool
+	}{
+		{"base", args{[]byte("{}")}, &Unlit{}, false},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := Unmarshal(tt.args.data)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("Unmarshal() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("Unmarshal() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}