From 145f1aab06f69cfce58aef08693b6632a13002c0 Mon Sep 17 00:00:00 2001 From: raa0121 Date: Thu, 21 May 2026 22:36:40 +0900 Subject: [PATCH] loader/vrm: add VRM/GLB loader --- loader/vrm/loader.go | 1212 +++++++++++++++++++++++++++++++++ loader/vrm/logger.go | 12 + loader/vrm/material_common.go | 140 ++++ loader/vrm/material_pbr.go | 135 ++++ loader/vrm/vrm.go | 460 +++++++++++++ 5 files changed, 1959 insertions(+) create mode 100644 loader/vrm/loader.go create mode 100644 loader/vrm/logger.go create mode 100644 loader/vrm/material_common.go create mode 100644 loader/vrm/material_pbr.go create mode 100644 loader/vrm/vrm.go diff --git a/loader/vrm/loader.go b/loader/vrm/loader.go new file mode 100644 index 00000000..89b8a56b --- /dev/null +++ b/loader/vrm/loader.go @@ -0,0 +1,1212 @@ +// Copyright 2016 The G3N Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package vrm + +import ( + "bytes" + "encoding/base64" + "encoding/binary" + "encoding/json" + "fmt" + "image" + "image/draw" + "io" + "io/ioutil" + "os" + "path/filepath" + "strings" + "unsafe" + + "github.com/g3n/engine/animation" + "github.com/g3n/engine/camera" + "github.com/g3n/engine/core" + "github.com/g3n/engine/geometry" + "github.com/g3n/engine/gls" + "github.com/g3n/engine/graphic" + "github.com/g3n/engine/material" + "github.com/g3n/engine/math32" + "github.com/g3n/engine/texture" +) + +// ParseJSON parses the glTF data from the specified JSON file +// and returns a pointer to the parsed structure. +func ParseJSON(filename string) (*GLTF, error) { + + // Open file + f, err := os.Open(filename) + if err != nil { + return nil, err + } + // Extract path from file + path := filepath.Dir(filename) + defer f.Close() + return ParseJSONReader(f, path) +} + +// ParseJSONReader parses the glTF JSON data from the specified reader +// and returns a pointer to the parsed structure +func ParseJSONReader(r io.Reader, path string) (*GLTF, error) { + + g := new(GLTF) + g.path = path + + dec := json.NewDecoder(r) + err := dec.Decode(g) + if err != nil { + return nil, err + } + + // TODO Check for extensions used and extensions required + + return g, nil +} + +// ParseBin parses the glTF data from the specified binary file +// and returns a pointer to the parsed structure. +func ParseBin(filename string) (*GLTF, error) { + + // Open file + f, err := os.Open(filename) + if err != nil { + return nil, err + } + // Extract path from file + path := filepath.Dir(filename) + defer f.Close() + return ParseBinReader(f, path) +} + +// ParseBinReader parses the glTF data from the specified binary reader +// and returns a pointer to the parsed structure +func ParseBinReader(r io.Reader, path string) (*GLTF, error) { + + // Read header + var header GLBHeader + err := binary.Read(r, binary.LittleEndian, &header) + if err != nil { + return nil, err + } + + // Check magic and version + if header.Magic != GLBMagic { + return nil, fmt.Errorf("invalid GLB Magic field") + } + if header.Version < 2 { + return nil, fmt.Errorf("GLB version:%v not supported", header.Version) + } + + // Read first chunk (JSON) + buf, err := readChunk(r, GLBJson) + if err != nil { + return nil, err + } + + // Parse JSON into gltf object + bb := bytes.NewBuffer(buf) + gltf, err := ParseJSONReader(bb, path) + if err != nil { + return nil, err + } + + // Check for and read second chunk (binary, optional) + data, err := readChunk(r, GLBBin) + if err != nil { + return nil, err + } + + gltf.data = data + + return gltf, nil +} + +// readChunk reads a GLB chunk with the specified type and returns the data in a byte array. +func readChunk(r io.Reader, chunkType uint32) ([]byte, error) { + + // Read chunk header + var chunk GLBChunk + err := binary.Read(r, binary.LittleEndian, &chunk) + if err != nil { + if err == io.EOF { + return nil, nil + } + return nil, err + } + + // Check chunk type + if chunk.Type != chunkType { + return nil, fmt.Errorf("expected GLB chunk type [%v] but found [%v]", chunkType, chunk.Type) + } + + // Read chunk data + data := make([]byte, chunk.Length) + err = binary.Read(r, binary.LittleEndian, &data) + if err != nil { + return nil, err + } + + return data, nil +} + +// LoadScene creates a parent Node which contains all nodes contained by +// the specified scene index from the GLTF Scenes array. +func (g *GLTF) LoadScene(sceneIdx int) (core.INode, error) { + + // Check if provided scene index is valid + if sceneIdx < 0 || sceneIdx >= len(g.Scenes) { + return nil, fmt.Errorf("invalid scene index") + } + log.Debug("Loading Scene %d", sceneIdx) + sceneData := g.Scenes[sceneIdx] + + scene := core.NewNode() + scene.SetName(sceneData.Name) + + // Load all nodes + for _, ni := range sceneData.Nodes { + child, err := g.LoadNode(ni) + if err != nil { + return nil, err + } + scene.Add(child) + } + return scene, nil +} + +// LoadNode creates and returns a new Node described by the specified index +// in the decoded GLTF Nodes array. +func (g *GLTF) LoadNode(nodeIdx int) (core.INode, error) { + + // Check if provided node index is valid + if nodeIdx < 0 || nodeIdx >= len(g.Nodes) { + return nil, fmt.Errorf("invalid node index") + } + nodeData := g.Nodes[nodeIdx] + // Return cached if available + if nodeData.cache != nil { + log.Debug("Fetching Node %d (cached)", nodeIdx) + return nodeData.cache, nil + } + log.Debug("Loading Node %d", nodeIdx) + + var in core.INode + var err error + // Check if the node is a Mesh (triangles, lines, etc...) + if nodeData.Mesh != nil { + in, err = g.LoadMesh(*nodeData.Mesh) + if err != nil { + return nil, err + } + + if nodeData.Skin != nil { + skeleton, err := g.LoadSkin(*nodeData.Skin) + if err != nil { + return nil, err + } + children := in.GetNode().Children() + if len(children) == 1 { + rm := graphic.NewRiggedMesh(children[0].(*graphic.Mesh)) + rm.SetSkeleton(skeleton) + in = rm + } else { + // Multiple primitives: give each its own RiggedMesh sharing the same skeleton + container := core.NewNode() + for _, child := range children { + rm := graphic.NewRiggedMesh(child.(*graphic.Mesh)) + rm.SetSkeleton(skeleton) + container.Add(rm) + } + in = container + } + } + + // Check if the node is Camera + } else if nodeData.Camera != nil { + in, err = g.LoadCamera(*nodeData.Camera) + if err != nil { + return nil, err + } + // Other cases, return empty node + } else { + log.Debug("Empty Node") + in = core.NewNode() + } + + // Get *core.Node from core.INode + node := in.GetNode() + node.SetName(nodeData.Name) + + // If defined, set node local transformation matrix + if nodeData.Matrix != nil { + node.SetMatrix((*math32.Matrix4)(nodeData.Matrix)) + // Otherwise, check rotation, scale and translation fields + } else { + // Rotation quaternion + if nodeData.Rotation != nil { + node.SetQuaternion(nodeData.Rotation[0], nodeData.Rotation[1], nodeData.Rotation[2], nodeData.Rotation[3]) + } + // Scale + if nodeData.Scale != nil { + node.SetScale(nodeData.Scale[0], nodeData.Scale[1], nodeData.Scale[2]) + } + // Translation + if nodeData.Translation != nil { + node.SetPosition(nodeData.Translation[0], nodeData.Translation[1], nodeData.Translation[2]) + } + } + + // Cache node + g.Nodes[nodeIdx].cache = in + + // Recursively load node children and add them to the parent + for _, ci := range nodeData.Children { + child, err := g.LoadNode(ci) + if err != nil { + return nil, err + } + node.Add(child) + } + + return in, nil +} + +// LoadSkin loads the skin with specified index. +func (g *GLTF) LoadSkin(skinIdx int) (*graphic.Skeleton, error) { + + // Check if provided skin index is valid + if skinIdx < 0 || skinIdx >= len(g.Skins) { + return nil, fmt.Errorf("invalid skin index") + } + skinData := g.Skins[skinIdx] + // Return cached if available + if skinData.cache != nil { + log.Debug("Fetching Skin %d (cached)", skinIdx) + return skinData.cache, nil + } + log.Debug("Loading Skin %d", skinIdx) + + // Create Skeleton and set it on Rigged mesh + skeleton := graphic.NewSkeleton() + + // Load inverseBindMatrices + ibmData, err := g.loadAccessorF32(skinData.InverseBindMatrices, "ibm", []string{MAT4}, []int{FLOAT}) + if err != nil { + return nil, err + } + + // Add bones + for i := range skinData.Joints { + jointNode, err := g.LoadNode(skinData.Joints[i]) + if err != nil { + return nil, err + } + var ibm math32.Matrix4 + ibmData.GetMatrix4(16*i, &ibm) + skeleton.AddBone(jointNode.GetNode(), &ibm) + } + + // Cache skin + g.Skins[skinIdx].cache = skeleton + + return skeleton, nil +} + +// LoadAnimationByName loads the animations with specified name. +// If there are multiple animations with the same name it loads the first occurrence. +func (g *GLTF) LoadAnimationByName(animName string) (*animation.Animation, error) { + + for i := range g.Animations { + if g.Animations[i].Name == animName { + return g.LoadAnimation(i) + } + } + return nil, fmt.Errorf("could not find animation named %v", animName) +} + +// LoadAnimation creates an Animation for the specified +// animation index from the GLTF Animations array. +func (g *GLTF) LoadAnimation(animIdx int) (*animation.Animation, error) { + + // Check if provided animation index is valid + if animIdx < 0 || animIdx >= len(g.Animations) { + return nil, fmt.Errorf("invalid animation index") + } + log.Debug("Loading Animation %d", animIdx) + animData := g.Animations[animIdx] + + anim := animation.NewAnimation() + anim.SetName(animData.Name) + for i := 0; i < len(animData.Channels); i++ { + + chData := animData.Channels[i] + target := chData.Target + sampler := animData.Samplers[chData.Sampler] + node, err := g.LoadNode(target.Node) + if err != nil { + return nil, err + } + + var validTypes []string + var validComponentTypes []int + + var ch animation.IChannel + if target.Path == "translation" { + validTypes = []string{VEC3} + validComponentTypes = []int{FLOAT} + ch = animation.NewPositionChannel(node) + } else if target.Path == "rotation" { + validTypes = []string{VEC4} + validComponentTypes = []int{FLOAT, BYTE, UNSIGNED_BYTE, SHORT, UNSIGNED_SHORT} + ch = animation.NewRotationChannel(node) + } else if target.Path == "scale" { + validTypes = []string{VEC3} + validComponentTypes = []int{FLOAT} + ch = animation.NewScaleChannel(node) + } else if target.Path == "weights" { + validTypes = []string{SCALAR} + validComponentTypes = []int{FLOAT, BYTE, UNSIGNED_BYTE, SHORT, UNSIGNED_SHORT} + children := node.GetNode().Children() + if len(children) > 1 { + return nil, fmt.Errorf("animating meshes with more than a single primitive is not supported") + } + morphGeom := children[0].(graphic.IGraphic).IGeometry().(*geometry.MorphGeometry) + ch = animation.NewMorphChannel(morphGeom) + } + + // TODO what if Input and Output accessors are interleaved? probably de-interleave in these 2 cases + + keyframes, err := g.loadAccessorF32(sampler.Input, "Input", []string{SCALAR}, []int{FLOAT}) + if err != nil { + return nil, err + } + values, err := g.loadAccessorF32(sampler.Output, "Output", validTypes, validComponentTypes) + if err != nil { + return nil, err + } + ch.SetBuffers(keyframes, values) + ch.SetInterpolationType(animation.InterpolationType(sampler.Interpolation)) + anim.AddChannel(ch) + } + return anim, nil +} + +// LoadCamera creates and returns a Camera Node +// from the specified GLTF.Cameras index. +func (g *GLTF) LoadCamera(camIdx int) (core.INode, error) { + + // Check if provided camera index is valid + if camIdx < 0 || camIdx >= len(g.Cameras) { + return nil, fmt.Errorf("invalid camera index") + } + log.Debug("Loading Camera %d", camIdx) + camData := g.Cameras[camIdx] + + if camData.Type == "perspective" { + desc := camData.Perspective + fov := 360 * (desc.Yfov) / 2 * math32.Pi + aspect := float32(2) // TODO how to get the current aspect ratio of the viewport from here ? + if desc.AspectRatio != nil { + aspect = *desc.AspectRatio + } + far := float32(2E6) + if desc.Zfar != nil { + far = *desc.Zfar + } + cam := camera.NewPerspective(aspect, desc.Znear, far, fov, camera.Vertical) + return cam, nil + } + + if camData.Type == "orthographic" { + desc := camData.Orthographic + cam := camera.NewOrthographic(desc.Xmag/desc.Ymag, desc.Znear, desc.Zfar, desc.Ymag, camera.Vertical) + return cam, nil + + } + + return nil, fmt.Errorf("unsupported camera type: %s", camData.Type) +} + +// LoadMesh creates and returns a Graphic Node (graphic.Mesh, graphic.Lines, graphic.Points, etc) +// from the specified GLTF.Meshes index. +func (g *GLTF) LoadMesh(meshIdx int) (core.INode, error) { + + // Check if provided mesh index is valid + if meshIdx < 0 || meshIdx >= len(g.Meshes) { + return nil, fmt.Errorf("invalid mesh index") + } + meshData := g.Meshes[meshIdx] + // Return cached if available + if meshData.cache != nil { + // TODO CLONE/REINSTANCE INSTEAD + //log.Debug("Instancing Mesh %d (from cached)", meshIdx) + //return meshData.cache, nil + } + log.Debug("Loading Mesh %d", meshIdx) + + var err error + + // Create container node + meshNode := core.NewNode() + + for i := 0; i < len(meshData.Primitives); i++ { + + // Get primitive information + p := meshData.Primitives[i] + + // Indexed Geometry + indices := math32.NewArrayU32(0, 0) + if p.Indices != nil { + pidx, err := g.loadIndices(*p.Indices) + if err != nil { + return nil, err + } + indices = append(indices, pidx...) + } else { + // Non-indexed primitive + // indices array stay empty + } + + // Load primitive material + var grMat material.IMaterial + if p.Material != nil { + grMat, err = g.LoadMaterial(*p.Material) + if err != nil { + return nil, err + } + } else { + grMat = g.newDefaultMaterial() + } + + // Create geometry + var igeom geometry.IGeometry + igeom = geometry.NewGeometry() + geom := igeom.GetGeometry() + + err = g.loadAttributes(geom, p.Attributes, indices) + if err != nil { + return nil, err + } + + // If primitive has targets then the geometry should be a morph geometry + if len(p.Targets) > 0 { + morphGeom := geometry.NewMorphGeometry(geom) + + // TODO Load morph target names if present in extras under "targetNames" + // TODO Update morph target weights if present in Mesh.Weights + + // Load targets + for i := range p.Targets { + tGeom := geometry.NewGeometry() + attributes := p.Targets[i] + err = g.loadAttributes(tGeom, attributes, indices) + if err != nil { + return nil, err + } + morphGeom.AddMorphTargetDeltas(tGeom) + } + + igeom = morphGeom + } + + // Default mode is 4 (TRIANGLES) + mode := TRIANGLES + if p.Mode != nil { + mode = *p.Mode + } + + // Create Mesh + // TODO materials for LINES, etc need to be different... + if mode == TRIANGLES { + meshNode.Add(graphic.NewMesh(igeom, grMat)) + } else if mode == LINES { + meshNode.Add(graphic.NewLines(igeom, grMat)) + } else if mode == LINE_STRIP { + meshNode.Add(graphic.NewLineStrip(igeom, grMat)) + } else if mode == POINTS { + meshNode.Add(graphic.NewPoints(igeom, grMat)) + } else { + return nil, fmt.Errorf("unsupported primitive:%v", mode) + } + } + + // Cache mesh + g.Meshes[meshIdx].cache = meshNode + + return meshNode, nil +} + +// loadAttributes loads the provided list of vertex attributes as VBO(s) into the specified geometry. +func (g *GLTF) loadAttributes(geom *geometry.Geometry, attributes map[string]int, indices math32.ArrayU32) error { + + // Indices of buffer views + interleavedVBOs := make(map[int]*gls.VBO, 0) + + // Load primitive attributes + for name, aci := range attributes { + accessor := g.Accessors[aci] + + // Validate that accessor is compatible with attribute + err := g.validateAccessorAttribute(accessor, name) + if err != nil { + return err + } + + // Load data and add it to geometry's VBO + if g.isInterleaved(accessor) { + bvIdx := *accessor.BufferView + // Check if we already loaded this buffer view + vbo, ok := interleavedVBOs[bvIdx] + if ok { + // Already created VBO for this buffer view + // Add attribute with correct byteOffset + g.addAttributeToVBO(vbo, name, uint32(*accessor.ByteOffset)) + } else { + // Load data and create vbo + buf, err := g.loadBufferView(bvIdx) + if err != nil { + return err + } + data, err := g.bytesToArrayF32(buf, accessor.ComponentType, accessor.Count*TypeSizes[accessor.Type]) + if err != nil { + return err + } + vbo := gls.NewVBO(data) + g.addAttributeToVBO(vbo, name, 0) + // Save reference to VBO keyed by index of the buffer view + interleavedVBOs[bvIdx] = vbo + // Add VBO to geometry + geom.AddVBO(vbo) + } + } else { + buf, err := g.loadAccessorBytes(accessor) + if err != nil { + return err + } + data, err := g.bytesToArrayF32(buf, accessor.ComponentType, accessor.Count*TypeSizes[accessor.Type]) + if err != nil { + return err + } + vbo := gls.NewVBO(data) + g.addAttributeToVBO(vbo, name, 0) + // Add VBO to geometry + geom.AddVBO(vbo) + } + } + + // Set indices + if len(indices) > 0 { + geom.SetIndices(indices) + } + + return nil +} + +// loadIndices loads the indices stored in the specified accessor. +func (g *GLTF) loadIndices(ai int) (math32.ArrayU32, error) { + + return g.loadAccessorU32(ai, "indices", []string{SCALAR}, []int{UNSIGNED_BYTE, UNSIGNED_SHORT, UNSIGNED_INT}) // TODO verify that it's ELEMENT_ARRAY_BUFFER +} + +// addAttributeToVBO adds the appropriate attribute to the provided vbo based on the glTF attribute name. +func (g *GLTF) addAttributeToVBO(vbo *gls.VBO, attribName string, byteOffset uint32) { + + aType, ok := AttributeName[attribName] + if !ok { + log.Warn(fmt.Sprintf("Attribute %v is not supported!", attribName)) + return + } + vbo.AddAttribOffset(aType, byteOffset) +} + +// validateAccessorAttribute validates the specified accessor for the given attribute name. +func (g *GLTF) validateAccessorAttribute(ac Accessor, attribName string) error { + + parts := strings.Split(attribName, "_") + semantic := parts[0] + + usage := "attribute " + attribName + + if attribName == "POSITION" { + return g.validateAccessor(ac, usage, []string{VEC3}, []int{FLOAT}) + } else if attribName == "NORMAL" { + return g.validateAccessor(ac, usage, []string{VEC3}, []int{FLOAT}) + } else if attribName == "TANGENT" { + // Note that morph targets only support VEC3 whereas normal attributes only support VEC4. + return g.validateAccessor(ac, usage, []string{VEC3, VEC4}, []int{FLOAT}) + } else if semantic == "TEXCOORD" { + return g.validateAccessor(ac, usage, []string{VEC2}, []int{FLOAT, UNSIGNED_BYTE, UNSIGNED_SHORT}) + } else if semantic == "COLOR" { + return g.validateAccessor(ac, usage, []string{VEC3, VEC4}, []int{FLOAT, UNSIGNED_BYTE, UNSIGNED_SHORT}) + } else if semantic == "JOINTS" { + return g.validateAccessor(ac, usage, []string{VEC4}, []int{UNSIGNED_BYTE, UNSIGNED_SHORT}) + } else if semantic == "WEIGHTS" { + return g.validateAccessor(ac, usage, []string{VEC4}, []int{FLOAT, UNSIGNED_BYTE, UNSIGNED_SHORT}) + } else { + return fmt.Errorf("attribute %v is not supported", attribName) + } +} + +// validateAccessor validates the specified attribute accessor with the specified allowed types and component types. +func (g *GLTF) validateAccessor(ac Accessor, usage string, validTypes []string, validComponentTypes []int) error { + + // Validate accessor type + validType := false + for _, vType := range validTypes { + if ac.Type == vType { + validType = true + break + } + } + if !validType { + return fmt.Errorf("invalid Accessor.Type %v for %s", ac.Type, usage) + } + + // Validate accessor component type + validComponentType := false + for _, vComponentType := range validComponentTypes { + if ac.ComponentType == vComponentType { + validComponentType = true + break + } + } + if !validComponentType { + return fmt.Errorf("invalid Accessor.ComponentType %v for %s", ac.ComponentType, usage) + } + + return nil +} + +// newDefaultMaterial creates and returns the default material. +func (g *GLTF) newDefaultMaterial() material.IMaterial { + + return material.NewStandard(&math32.Color{R: 0.5, G: 0.5, B: 0.5}) +} + +// LoadMaterial creates and returns a new material based on the material data with the specified index. +func (g *GLTF) LoadMaterial(matIdx int) (material.IMaterial, error) { + + // Check if provided material index is valid + if matIdx < 0 || matIdx >= len(g.Materials) { + return nil, fmt.Errorf("invalid material index") + } + matData := g.Materials[matIdx] + // Return cached if available + if matData.cache != nil { + log.Debug("Fetching Material %d (cached)", matIdx) + return matData.cache, nil + } + log.Debug("Loading Material %d", matIdx) + + var err error + var imat material.IMaterial + + // Check for material extensions + hasCommon := false + if matData.Extensions != nil { + for ext, extData := range matData.Extensions { + if ext == KhrMaterialsCommon { + imat, err = g.loadMaterialCommon(extData) + hasCommon = true + } else if ext == KhrMaterialsUnlit { + // Fall through to PBR below; the unlit flag is advisory only. + } else if ext == VRM { + // VRM root-level material extension; ignore and use PBR data. + } else { + log.Warn("unknown material extension %s, falling back to PBR", ext) + } + } + } + // Load PBR if no other material type was resolved + if !hasCommon && imat == nil { + imat, err = g.loadMaterialPBR(&matData) + } + + // Cache material + g.Materials[matIdx].cache = imat + + return imat, err +} + +// LoadTexture loads the texture specified by its index. +func (g *GLTF) LoadTexture(texIdx int) (*texture.Texture2D, error) { + + // Check if provided texture index is valid + if texIdx < 0 || texIdx >= len(g.Textures) { + return nil, fmt.Errorf("invalid texture index") + } + texData := g.Textures[texIdx] + // NOTE: Textures can't be cached because they have their own uniforms + log.Debug("Loading Texture %d", texIdx) + + // Load texture image + img, err := g.LoadImage(texData.Source) + if err != nil { + return nil, err + } + tex := texture.NewTexture2DFromRGBA(img) + + // Get sampler and apply texture parameters + if texData.Sampler != nil { + err = g.applySampler(*texData.Sampler, tex) + if err != nil { + return nil, err + } + } + + return tex, nil +} + +// applySamplers applies the specified Sampler to the provided texture. +func (g *GLTF) applySampler(samplerIdx int, tex *texture.Texture2D) error { + + log.Debug("Applying Sampler %d", samplerIdx) + // Check if provided sampler index is valid + if samplerIdx < 0 || samplerIdx >= len(g.Samplers) { + return fmt.Errorf("invalid sampler index") + } + sampler := g.Samplers[samplerIdx] + + // Magnification filter + magFilter := gls.LINEAR + if sampler.MagFilter != nil { + magFilter = *sampler.MagFilter + } + tex.SetMagFilter(uint32(magFilter)) + + // Minification filter + minFilter := gls.LINEAR_MIPMAP_LINEAR + if sampler.MinFilter != nil { + minFilter = *sampler.MinFilter + } + tex.SetMinFilter(uint32(minFilter)) + + // S coordinate wrapping mode + wrapS := gls.REPEAT + if sampler.WrapS != nil { + wrapS = *sampler.WrapS + } + tex.SetWrapS(uint32(wrapS)) + + // T coordinate wrapping mode + wrapT := gls.REPEAT + if sampler.WrapT != nil { + wrapT = *sampler.WrapT + } + tex.SetWrapT(uint32(wrapT)) + + return nil +} + +// LoadImage loads the image specified by the index of GLTF.Images. +// Image can be loaded from binary chunk file or data URI or external file.. +func (g *GLTF) LoadImage(imgIdx int) (*image.RGBA, error) { + + // Check if provided image index is valid + if imgIdx < 0 || imgIdx >= len(g.Images) { + return nil, fmt.Errorf("invalid image index") + } + imgData := g.Images[imgIdx] + // Return cached if available + if imgData.cache != nil { + log.Debug("Fetching Image %d (cached)", imgIdx) + return imgData.cache, nil + } + log.Debug("Loading Image %d", imgIdx) + + var data []byte + var err error + // If Uri is empty, load image from GLB binary chunk + if imgData.Uri == "" { + if imgData.BufferView == nil { + return nil, fmt.Errorf("image has empty URI and no BufferView") + } + data, err = g.loadBufferView(*imgData.BufferView) + } else if isDataURL(imgData.Uri) { + // Checks if image URI is data URL + data, err = loadDataURL(imgData.Uri) + } else { + // Load image data from file + data, err = g.loadFileBytes(imgData.Uri) + } + + if err != nil { + return nil, err + } + + // Decodes image data + bb := bytes.NewBuffer(data) + img, _, err := image.Decode(bb) + if err != nil { + return nil, err + } + + // Converts image to RGBA format + rgba := image.NewRGBA(img.Bounds()) + if rgba.Stride != rgba.Rect.Size().X*4 { + return nil, fmt.Errorf("unsupported stride") + } + draw.Draw(rgba, rgba.Bounds(), img, image.Point{0, 0}, draw.Src) + + // Cache image + g.Images[imgIdx].cache = rgba + + return rgba, nil +} + +// bytesToArrayU32 converts a byte array to ArrayU32. +func (g *GLTF) bytesToArrayU32(data []byte, componentType, count int) (math32.ArrayU32, error) { + + // If component is UNSIGNED_INT nothing to do + if componentType == UNSIGNED_INT { + arr := (*[1 << 30]uint32)(unsafe.Pointer(&data[0]))[:count] + return math32.ArrayU32(arr), nil + } + + // Converts UNSIGNED_SHORT to UNSIGNED_INT + if componentType == UNSIGNED_SHORT { + out := math32.NewArrayU32(count, count) + for i := 0; i < count; i++ { + out[i] = uint32(data[i*2]) + uint32(data[i*2+1])*256 + } + return out, nil + } + + // Converts UNSIGNED_BYTE indices to UNSIGNED_INT + if componentType == UNSIGNED_BYTE { + out := math32.NewArrayU32(count, count) + for i := 0; i < count; i++ { + out[i] = uint32(data[i]) + } + return out, nil + } + + return nil, fmt.Errorf("unsupported Accessor ComponentType:%v", componentType) +} + +// bytesToArrayF32 converts a byte array to ArrayF32. +func (g *GLTF) bytesToArrayF32(data []byte, componentType, count int) (math32.ArrayF32, error) { + + // If component is UNSIGNED_INT nothing to do + if componentType == UNSIGNED_INT { + arr := (*[1 << 30]float32)(unsafe.Pointer(&data[0]))[:count] + return math32.ArrayF32(arr), nil + } + + // Converts UNSIGNED_SHORT to UNSIGNED_INT + if componentType == UNSIGNED_SHORT { + out := math32.NewArrayF32(count, count) + for i := 0; i < count; i++ { + out[i] = float32(data[i*2]) + float32(data[i*2+1])*256 + } + return out, nil + } + + // Converts UNSIGNED_BYTE indices to UNSIGNED_INT + if componentType == UNSIGNED_BYTE { + out := math32.NewArrayF32(count, count) + for i := 0; i < count; i++ { + out[i] = float32(data[i]) + } + return out, nil + } + + return (*[1 << 30]float32)(unsafe.Pointer(&data[0]))[:count], nil +} + +// loadAccessorU32 loads data from the specified accessor and performs validation of the Type and ComponentType. +func (g *GLTF) loadAccessorU32(ai int, usage string, validTypes []string, validComponentTypes []int) (math32.ArrayU32, error) { + + // Get Accessor for the specified index + ac := g.Accessors[ai] + if ac.BufferView == nil { + return nil, fmt.Errorf("accessor.BufferView == nil NOT SUPPORTED YET") // TODO + } + + // Validate type and component type + err := g.validateAccessor(ac, usage, validTypes, validComponentTypes) + if err != nil { + return nil, err + } + + // Load bytes + data, err := g.loadAccessorBytes(ac) + if err != nil { + return nil, err + } + + return g.bytesToArrayU32(data, ac.ComponentType, ac.Count*TypeSizes[ac.Type]) +} + +// loadAccessorF32 loads data from the specified accessor and performs validation of the Type and ComponentType. +func (g *GLTF) loadAccessorF32(ai int, usage string, validTypes []string, validComponentTypes []int) (math32.ArrayF32, error) { + + // Get Accessor for the specified index + ac := g.Accessors[ai] + if ac.BufferView == nil { + return nil, fmt.Errorf("accessor.BufferView == nil NOT SUPPORTED YET") // TODO + } + + // Validate type and component type + err := g.validateAccessor(ac, usage, validTypes, validComponentTypes) + if err != nil { + return nil, err + } + + // Load bytes + data, err := g.loadAccessorBytes(ac) + if err != nil { + return nil, err + } + + return g.bytesToArrayF32(data, ac.ComponentType, ac.Count*TypeSizes[ac.Type]) +} + +// loadAccessorBytes returns the base byte array used by an accessor. +func (g *GLTF) loadAccessorBytes(ac Accessor) ([]byte, error) { + + // Get the Accessor's BufferView + if ac.BufferView == nil { + return nil, fmt.Errorf("accessor.BufferView == nil NOT SUPPORTED YET") // TODO + } + bv := g.BufferViews[*ac.BufferView] + + // Loads data from associated BufferView + data, err := g.loadBufferView(*ac.BufferView) + if err != nil { + return nil, err + } + + // Accessor offset into BufferView + offset := 0 + if ac.ByteOffset != nil { + offset = *ac.ByteOffset + } + data = data[offset:] + + // TODO check if interleaved and de-interleave if necessary? + + // Calculate the size in bytes of a complete attribute + itemSize := TypeSizes[ac.Type] + itemBytes := int(gls.FloatSize) * itemSize + + // If the BufferView stride is equal to the item size, the buffer is not interleaved + if (bv.ByteStride != nil) && (*bv.ByteStride != itemBytes) { + // BufferView data is interleaved, de-interleave + // TODO + return nil, fmt.Errorf("data is interleaved - not supported for animation yet") + } + + // TODO Sparse accessor + + return data, nil +} + +// isInterleaves returns whether the BufferView used by the provided accessor is interleaved. +func (g *GLTF) isInterleaved(accessor Accessor) bool { + + // Get the Accessor's BufferView + if accessor.BufferView == nil { + return false + } + bv := g.BufferViews[*accessor.BufferView] + + // Calculates the size in bytes of a complete attribute + itemSize := TypeSizes[accessor.Type] + itemBytes := int(gls.FloatSize) * itemSize + + // If the BufferView stride is equal to the item size, the buffer is not interleaved + if bv.ByteStride == nil { + return false + } + if *bv.ByteStride == itemBytes { + return false + } + return true +} + +// loadBufferView loads and returns a byte slice with data from the specified BufferView. +func (g *GLTF) loadBufferView(bvIdx int) ([]byte, error) { + + // Check if provided buffer view index is valid + if bvIdx < 0 || bvIdx >= len(g.BufferViews) { + return nil, fmt.Errorf("invalid buffer view index") + } + bvData := g.BufferViews[bvIdx] + // Return cached if available + if bvData.cache != nil { + log.Debug("Fetching BufferView %d (cached)", bvIdx) + return bvData.cache, nil + } + log.Debug("Loading BufferView %d", bvIdx) + + // Load buffer view buffer + buf, err := g.loadBuffer(bvData.Buffer) + if err != nil { + return nil, err + } + + // Establish offset + offset := 0 + if bvData.ByteOffset != nil { + offset = *bvData.ByteOffset + } + + // Compute and return offset slice + bvBytes := buf[offset : offset+bvData.ByteLength] + + // Cache buffer view + g.BufferViews[bvIdx].cache = bvBytes + + return bvBytes, nil +} + +// loadBuffer loads and returns the data from the specified GLTF Buffer index +func (g *GLTF) loadBuffer(bufIdx int) ([]byte, error) { + + // Check if provided buffer index is valid + if bufIdx < 0 || bufIdx >= len(g.Buffers) { + return nil, fmt.Errorf("invalid buffer index") + } + bufData := &g.Buffers[bufIdx] + // Return cached if available + if bufData.cache != nil { + log.Debug("Fetching Buffer %d (cached)", bufIdx) + return bufData.cache, nil + } + log.Debug("Loading Buffer %d", bufIdx) + + // If buffer URI use the chunk data field + if bufData.Uri == "" { + return g.data, nil + } + + // Checks if buffer URI is a data URI + var data []byte + var err error + if isDataURL(bufData.Uri) { + data, err = loadDataURL(bufData.Uri) + } else { + // Try to load buffer from file + data, err = g.loadFileBytes(bufData.Uri) + } + if err != nil { + return nil, err + } + + // Checks data length + if len(data) != bufData.ByteLength { + return nil, fmt.Errorf("buffer:%d read data length:%d expected:%d", bufIdx, len(data), bufData.ByteLength) + } + // Cache buffer data + g.Buffers[bufIdx].cache = data + log.Debug("cache data:%v", len(bufData.cache)) + return data, nil +} + +// loadFileBytes loads the file with specified path as a byte array. +func (g *GLTF) loadFileBytes(uri string) ([]byte, error) { + + log.Debug("Loading File: %v", uri) + + fpath := filepath.Join(g.path, uri) + f, err := os.Open(fpath) + if err != nil { + return nil, err + } + defer f.Close() + data, err := ioutil.ReadAll(f) + if err != nil { + return nil, err + } + return data, nil +} + +// dataURL describes a decoded data url string. +type dataURL struct { + MediaType string + Encoding string + Data string +} + +const ( + dataURLprefix = "data:" + mimeBIN = "application/octet-stream" + mimePNG = "image/png" + mimeJPEG = "image/jpeg" +) + +var validMediaTypes = []string{mimeBIN, mimePNG, mimeJPEG} + +// isDataURL checks if the specified string has the prefix of data URL. +func isDataURL(url string) bool { + + if strings.HasPrefix(url, dataURLprefix) { + return true + } + return false +} + +// loadDataURL decodes the specified data URI string (base64). +func loadDataURL(url string) ([]byte, error) { + + var du dataURL + err := parseDataURL(url, &du) + if err != nil { + return nil, err + } + + // Checks for valid media type + found := false + for i := 0; i < len(validMediaTypes); i++ { + if validMediaTypes[i] == du.MediaType { + found = true + break + } + } + if !found { + return nil, fmt.Errorf("data URI media type:%s not supported", du.MediaType) + } + + // Checks encoding + if du.Encoding != "base64" { + return nil, fmt.Errorf("data URI encoding:%s not supported", du.Encoding) + } + + // Decodes data from BASE64 + data, err := base64.StdEncoding.DecodeString(du.Data) + if err != nil { + return nil, err + } + return data, nil +} + +// parseDataURL tries to parse the specified string as a data URL with the format: +// data:[][;base64], +// and if successfull returns true and updates the specified pointer with the parsed fields. +func parseDataURL(url string, du *dataURL) error { + + // Check prefix + if !isDataURL(url) { + return fmt.Errorf("specified string is not a data URL") + } + + // Separate header from data + body := url[len(dataURLprefix):] + parts := strings.Split(body, ",") + if len(parts) != 2 { + return fmt.Errorf("data URI contains more than one ','") + } + du.Data = parts[1] + + // Separate media type from optional encoding + res := strings.Split(parts[0], ";") + du.MediaType = res[0] + if len(res) < 2 { + return nil + } + if len(res) >= 2 { + du.Encoding = res[1] + } + return nil +} diff --git a/loader/vrm/logger.go b/loader/vrm/logger.go new file mode 100644 index 00000000..d1e9c2c4 --- /dev/null +++ b/loader/vrm/logger.go @@ -0,0 +1,12 @@ +// Copyright 2016 The G3N Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package vrm + +import ( + "github.com/g3n/engine/util/logger" +) + +// Package logger +var log = logger.New("VRM", logger.Default) diff --git a/loader/vrm/material_common.go b/loader/vrm/material_common.go new file mode 100644 index 00000000..37514c30 --- /dev/null +++ b/loader/vrm/material_common.go @@ -0,0 +1,140 @@ +package vrm + +import ( + "github.com/g3n/engine/material" + "github.com/g3n/engine/math32" + "github.com/g3n/engine/texture" +) + +// loadMaterialCommon receives an interface value describing a KHR_materials_common extension, +// decodes it and returns a Material closest to the specified description +// The specification of this extension is at: +// https://github.com/KhronosGroup/glTF/tree/master/extensions/Khronos/KHR_materials_common +func (g *GLTF) loadMaterialCommon(ext interface{}) (material.IMaterial, error) { + + // The extension must be an object + m := ext.(map[string]interface{}) + + // Double sided + doubleSided := false + val, ok := m["doubleSided"] + if ok { + doubleSided = val.(bool) + } + + // Transparent + transparent := false + val, ok = m["transparent"] + if ok { + transparent = val.(bool) + } + + // Defaul values + ambient := []float32{0, 0, 0, 1} + diffuse := []float32{0, 0, 0, 1} + emission := []float32{0, 0, 0, 1} + specular := []float32{0, 0, 0, 1} + shininess := float32(0) + transparency := float32(1) + var texDiffuse *texture.Texture2D + + // Converts a slice of interface values which should be float64 + // to a slice of float32 + convIF32 := func(v interface{}) []float32 { + + si := v.([]interface{}) + res := make([]float32, 0) + for i := 0; i < len(si); i++ { + res = append(res, float32(si[i].(float64))) + } + return res + } + + // Values + values, ok := m["values"].(map[string]interface{}) + if ok { + + // Ambient light + val, ok = values["ambient"] + if ok { + ambient = convIF32(val) + } + + // Diffuse light + val, ok = values["diffuse"] + if ok { + v := convIF32(val) + // Checks for texture index + if len(v) == 1 { + var err error + texDiffuse, err = g.LoadTexture(int(v[0])) + if err != nil { + return nil, err + } + diffuse = []float32{1, 1, 1, 1} + } + } + + // Emission light + val, ok = values["emission"] + if ok { + emission = convIF32(val) + } + + // Specular light + val, ok = values["specular"] + if ok { + specular = convIF32(val) + } + + // Shininess + val, ok = values["shininess"] + if ok { + s := convIF32(val) + shininess = s[0] + } + + // Transparency + val, ok = values["transparency"] + if ok { + s := convIF32(val) + transparency = s[0] + } + } + + //log.Error("doubleSided:%v", doubleSided) + //log.Error("technique:%v", technique) + //log.Error("transparent:%v", transparent) + //log.Error("values:%v", values) + //log.Error("ambient:%v", ambient) + //log.Error("diffuse:%v", diffuse) + //log.Error("emission:%v", emission) + //log.Error("specular:%v", specular) + //log.Error("shininess:%v", shininess) + //log.Error("transparency:%v", transparency) + + mat := material.NewStandard(&math32.Color{R: diffuse[0], G: diffuse[1], B: diffuse[2]}) + mat.SetAmbientColor(&math32.Color{R: ambient[0], G: ambient[1], B: ambient[2]}) + mat.SetEmissiveColor(&math32.Color{R: emission[0], G: emission[1], B: emission[2]}) + mat.SetSpecularColor(&math32.Color{R: specular[0], G: specular[1], B: specular[2]}) + mat.SetShininess(shininess) + mat.SetOpacity(transparency) + if texDiffuse != nil { + mat.AddTexture(texDiffuse) + } + + // Double Sided + if doubleSided { + mat.SetSide(material.SideDouble) + } else { + mat.SetSide(material.SideFront) + } + + // Transparency + if transparent { + mat.SetDepthMask(true) + } else { + mat.SetDepthMask(false) + } + return mat, nil +} diff --git a/loader/vrm/material_pbr.go b/loader/vrm/material_pbr.go new file mode 100644 index 00000000..f8d02b95 --- /dev/null +++ b/loader/vrm/material_pbr.go @@ -0,0 +1,135 @@ +package vrm + +import ( + "fmt" + + "github.com/g3n/engine/material" + "github.com/g3n/engine/math32" +) + +func (g *GLTF) loadMaterialPBR(m *Material) (material.IMaterial, error) { + + // Get pbr information + pbr := m.PbrMetallicRoughness + if pbr == nil { + return nil, fmt.Errorf("PbrMetallicRoughness not supplied") + } + + // Create new physically based material + pm := material.NewPhysical() + + // Double sided + if m.DoubleSided { + pm.SetSide(material.SideDouble) + } else { + pm.SetSide(material.SideFront) + } + + var alphaMode string + if len(m.AlphaMode) > 0{ + alphaMode = m.AlphaMode + } else { + alphaMode = "OPAQUE" + } + + if alphaMode == "BLEND" { + pm.SetTransparent(true) + } else { + pm.SetTransparent(false) + if alphaMode == "MASK" { + // TODO m.AlphaCutoff + // pm.SetAlphaCutoff + } + } + + // BaseColorFactor + var baseColorFactor math32.Color4 + if pbr.BaseColorFactor != nil { + baseColorFactor = math32.Color4{R: pbr.BaseColorFactor[0], G: pbr.BaseColorFactor[1], B: pbr.BaseColorFactor[2], A: pbr.BaseColorFactor[3]} + } else { + baseColorFactor = math32.Color4{R: 1, G: 1, B: 1, A: 1} + } + pm.SetBaseColorFactor(&baseColorFactor) + + // MetallicFactor + var metallicFactor float32 + if pbr.MetallicFactor != nil { + metallicFactor = *pbr.MetallicFactor + } else { + if pbr.MetallicRoughnessTexture != nil { + metallicFactor = 1 + } else { + metallicFactor = 0 + } + } + pm.SetMetallicFactor(metallicFactor) + + // RoughnessFactor + var roughnessFactor float32 + if pbr.RoughnessFactor != nil { + roughnessFactor = *pbr.RoughnessFactor + } else { + roughnessFactor = 1 + } + pm.SetRoughnessFactor(roughnessFactor) + + // EmissiveFactor + var emissiveFactor math32.Color + if m.EmissiveFactor != nil { + emissiveFactor = math32.Color{R: m.EmissiveFactor[0], G: m.EmissiveFactor[1], B: m.EmissiveFactor[2]} + } else { + if m.EmissiveTexture != nil { + emissiveFactor = math32.Color{R: 1, G: 1, B: 1} + } else { + emissiveFactor = math32.Color{R: 0, G: 0, B: 0} + } + } + pm.SetEmissiveFactor(&emissiveFactor) + + // BaseColorTexture + if pbr.BaseColorTexture != nil { + tex, err := g.LoadTexture(pbr.BaseColorTexture.Index) + if err != nil { + return nil, err + } + pm.SetBaseColorMap(tex) + } + + // MetallicRoughnessTexture + if pbr.MetallicRoughnessTexture != nil { + tex, err := g.LoadTexture(pbr.MetallicRoughnessTexture.Index) + if err != nil { + return nil, err + } + pm.SetMetallicRoughnessMap(tex) + } + + // NormalTexture + if m.NormalTexture != nil { + tex, err := g.LoadTexture(m.NormalTexture.Index) + if err != nil { + return nil, err + } + pm.SetNormalMap(tex) + } + + // OcclusionTexture + if m.OcclusionTexture != nil { + tex, err := g.LoadTexture(m.OcclusionTexture.Index) + if err != nil { + return nil, err + } + pm.SetOcclusionMap(tex) + } + + // EmissiveTexture + if m.EmissiveTexture != nil { + tex, err := g.LoadTexture(m.EmissiveTexture.Index) + if err != nil { + return nil, err + } + pm.SetEmissiveMap(tex) + } + + return pm, nil +} diff --git a/loader/vrm/vrm.go b/loader/vrm/vrm.go new file mode 100644 index 00000000..cedbdca8 --- /dev/null +++ b/loader/vrm/vrm.go @@ -0,0 +1,460 @@ +// Copyright 2016 The G3N Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package vrm +package vrm + +import ( + "github.com/g3n/engine/animation" + "github.com/g3n/engine/camera" + "github.com/g3n/engine/core" + "github.com/g3n/engine/gls" + "github.com/g3n/engine/graphic" + "github.com/g3n/engine/material" + "github.com/g3n/engine/math32" + "image" +) + +// glTF Extensions. +const ( + KhrDracoMeshCompression = "KHR_draco_mesh_compression" + KhrMaterialsUnlit = "KHR_materials_unlit" + KhrMaterialsCommon = "KHR_materials_common" // TODO this is officially part of glTF 1.0 (remove?) + KhrMaterialsPbrSpecularGlossiness = "KHR_materials_pbrSpecularGlossiness" + VRM = "VRM" +) + +// GLTF is the root object for a glTF asset. +type GLTF struct { + ExtensionsUsed []string // Names of glTF extensions used somewhere in this asset. Not required. + ExtensionsRequired []string // Names of glTF extensions required to properly load this asset. Not required. + Accessors []Accessor // An array of accessors. Not required. + Animations []Animation // An array of keyframe animations. Not required. + Asset Asset // Metadata about the glTF asset. Required. + Buffers []Buffer // An array of buffers. Not required. + BufferViews []BufferView // An array of bufferViews. Not required. + Cameras []Camera // An array of cameras. Not required. + Images []Image // An array of images. Not required. + Materials []Material // An array of materials. Not required. + Meshes []Mesh // An array of meshes. Not required. + Nodes []Node // An array of nodes. Not required. + Samplers []Sampler // An array of samplers. Not required. + Scene *int // The index of the default scene. Not required. + Scenes []Scene // An array of scenes. Not required. + Skins []Skin // An array of skins. Not required. + Textures []Texture // An array of textures. Not required. + Extensions map[string]interface{} // Dictionary object with extension-specific objects. Not required. + Extras interface{} // Application-specific data. Not required. + + path string // File path for resources. + data []byte // Binary file Chunk 1 data. +} + +// Accessor is a typed view into a BufferView. +type Accessor struct { + BufferView *int // The index of the buffer view. Not required. + ByteOffset *int // The offset relative to the start of the BufferView in bytes. Not required. Default is 0. + ComponentType int // The data type of components in the attribute. Required. + Normalized bool // Specifies whether integer data values should be normalized. Not required. Default is false. + Count int // The number of attributes referenced by this accessor. Required. + Type string // Specifies if the attribute is a scalar, vector or matrix. Required. + Max []float32 // Maximum value of each component in this attribute. Not required. + Min []float32 // Minimum value of each component in this attribute. Not required. + Sparse *Sparse // Sparse storage attribute that deviates from their initialization value. Not required. + Name string // The user-defined name of this object. Not required. + Extensions map[string]interface{} // Dictionary object with extension specific objects. Not required. + Extras interface{} // Application-specific data. Not required. + + cache math32.ArrayF32 // TODO implement caching +} + +// Animation is a keyframe animation. +type Animation struct { + Channels []Channel // An array of channels, each of which targets an animation's sampler at a node's property. Different channels of the same animation can't have equal targets. Required. + Samplers []AnimationSampler // An array of samplers that combines input and output accessors with an interpolation algorithm to define a keyframe graph (but not its target). Required. + Name string // The user-defined name of this object. Not required. + Extensions map[string]interface{} // Dictionary object with extension specific objects. Not required. + Extras interface{} // Application-specific data. Not required. + + cache *animation.Animation // Cached Animation. // TODO +} + +// AnimationSample combines input and output accessors with an interpolation algorithm to define a keyframe graph (but not its target). +type AnimationSampler struct { + Input int // The index of an accessor containing keyframe input values, e.g., time. Required. + Interpolation string // Interpolation algorithm. Not required. Default is "LINEAR". + Output int // The index of an accessor, containing keyframe output values. Required. + Extensions map[string]interface{} // Dictionary object with extension-specific objects. Not required. + Extras interface{} // Application-specific data. Not required. +} + +// Asset contains metadata about the glTF asset. +type Asset struct { + Copyright string // A copyright message suitable for display to credit the content creator. Not required. + Generator string // Tool that generated this glTF model. Useful for debugging. Not required. + Version string // The glTF version that this asset targets. Required. + MinVersion string // The minimum glTF version that this asset targets. Not required. + Extensions map[string]interface{} // Dictionary object with extension-specific objects. Not required. + Extras interface{} // Application-specific data. Not required. +} + +// Buffer points to binary geometry, animation, or skins. +type Buffer struct { + Uri string // The URI of the buffer. Not required. + ByteLength int // The length of the buffer in bytes. Required. + Name string // The user-defined name of this object. Not required. + Extensions map[string]interface{} // Dictionary object with extension-specific objects. Not required. + Extras interface{} // Application-specific data. Not required. + + cache []byte // Cached buffer data. +} + +// BufferView is a view into a buffer generally representing a subset of the buffer. +type BufferView struct { + Buffer int // The index of the buffer. Required. + ByteOffset *int // The offset into the buffer, in bytes. Not required. Default is 0. + ByteLength int // The length of the buffer view, in bytes. Required. + ByteStride *int // The stride, in bytes. Not required. + Target *int // The target that the GPU buffer should be bound to. Not required. + Name string // The user-defined name of this object. Not required. + Extensions map[string]interface{} // Dictionary object with extension specific objects. Not required. + Extras interface{} // Application-specific data. Not required. + + cache []byte // Cached buffer view data. +} + +// Camera is a camera's projection. +// A node can reference a camera to apply a transform to place the camera in the scene. +type Camera struct { + Orthographic *Orthographic // An orthographic camera containing properties to create an orthographic projection matrix. Not required. + Perspective *Perspective // A perspective camera containing properties to create a perspective projection matrix. Not required. + Type string // Specifies if the camera uses a perspective or orthographic projection. Required. + Name string // The user-defined name of this object. Not required. + Extensions map[string]interface{} // Dictionary object with extension-specific objects. Not required. + Extras interface{} // Application-specific data. Not required. + + cache camera.ICamera // Cached ICamera. // TODO +} + +// Channel targets an animation's sampler at a node's property. +type Channel struct { + Sampler int // The index of a sampler in this animation used to compute the value for the target. Required. + Target Target // The index of the node and TRS property to target. Required. + Extensions map[string]interface{} // Dictionary object with extension-specific objects. Not required. + Extras interface{} // Application-specific data. Not required. +} + +// Image data used to create a texture. +// Image can be referenced by URI or bufferView index. mimeType is required in the latter case. +type Image struct { + Uri string // The URI of the image. Not required. + MimeType string // The image's MIME type. Not required. + BufferView *int // The index of the bufferView that contains the image. Use this instead of the image's uri property. Not required. + Name string // The user-defined name of this object. Not required. + Extensions map[string]interface{} // Dictionary object with extension-specific objects. Not required. + Extras interface{} // Application-specific data. Not required. + + cache *image.RGBA // Cached image. +} + +// Indices of those attributes that deviate from their initialization value. +type Indices struct { + BufferView int // The index of the bufferView with sparse indices. Referenced bufferView can't have ARRAY_BUFFER or ELEMENT_ARRAY_BUFFER target. Required. + ByteOffset int // The offset relative to the start of the bufferView in bytes. Must be aligned. Not required. Default is 0. + ComponentType int // The indices data type. Required. + Extensions map[string]interface{} // Dictionary object with extension-specific objects. Not required. + Extras interface{} // Application-specific data. Not required. +} + +// Material describes the material appearance of a primitive. +type Material struct { + Name string // The user-defined name of this object. Not required. + PbrMetallicRoughness *PbrMetallicRoughness // A set of parameter values that are used to define the metallic-roughness material model from Physically-Based Rendering (PBR) methodology. When not specified, all the default values of pbrMetallicRoughness apply. Not required. + NormalTexture *NormalTextureInfo // The normal map texture. Not required. + OcclusionTexture *OcclusionTextureInfo // The occlusion map texture. Not required. + EmissiveTexture *TextureInfo // The emissive map texture. Not required. + EmissiveFactor *[3]float32 // The emissive color of the material. Not required. Default is [0,0,0] + AlphaMode string // The alpha rendering mode of the material. Not required. Default is OPAQUE. + AlphaCutoff float32 // The alpha cutoff value of the material. Not required. Default is 0.5. + DoubleSided bool // Specifies whether the material is double sided. Not required. Default is false. + Extensions map[string]interface{} // Dictionary object with extension-specific objects. Not required. + Extras interface{} // Application-specific data. Not required. + + cache material.IMaterial // Cached IMaterial. +} + +// Mesh is a set of primitives to be rendered. +// A node can contain one mesh. A node's transform places the mesh in the scene. +type Mesh struct { + Primitives []Primitive // An array of primitives, each defining geometry to be rendered with a material. Required. + Weights []float32 // Array of weights to be applied to the Morph Targets. Not required. + Name string // The user-defined name of this object. Not required. + Extensions map[string]interface{} // Dictionary object with extension-specific objects. Not required. + Extras interface{} // Application-specific data. Not required. + + cache core.INode // Cached INode. We don't cache an IGraphic here because a glTFL mesh can contain multiple primitive IGraphics. +} + +// Node is a node in the node hierarchy. +// When the node contains skin, all mesh.primitives must contain JOINTS_0 and WEIGHTS_0 attributes. +// A node can have either a matrix or any combination of translation/rotation/scale (TRS) properties. +// TRS properties are converted to matrices and postmultiplied in the T * R * S order to compose the transformation matrix; first the scale is applied to the vertices, then the rotation, and then the translation. +// If none are provided, the transform is the identity. +// When a node is targeted for animation (referenced by an animation.channel.target), only TRS properties may be present; matrix will not be present. +type Node struct { + Camera *int // Index of the camera referenced by this node. Not required. + Children []int // The indices of this node's children. Not required. + Skin *int // The index of the skin referenced by this node. Not required. + Matrix *[16]float32 // Floating point 4x4 transformation matrix in column-major order. Not required. Default is the identity matrix. + Mesh *int // The index of the mesh in this node. Not required. + Rotation *[4]float32 // The node's unit quaternion rotation in the order (x, y, z, w), where w is the scalar. Not required. Default is [0,0,0,1]. + Scale *[3]float32 // The node's non-uniform scale, given as the scaling factors along the x, y, and z axes. Not required. Default is [1,1,1]. + Translation *[3]float32 // The node's translation along the x, y, and z axes. Not required. Default is [0,0,0]. + Weights []float32 // The weights of the instantiated Morph Target. Number of elements must match number of Morph Targets of used mesh. Not required. + Name string // The user-defined name of this object. Not required. + Extensions map[string]interface{} // Dictionary object with extension-specific objects. Not required. + Extras interface{} // Application-specific data. Not required. + + cache core.INode // Cached INode. +} + +// TODO Why not combine NormalTextureInfo and OcclusionTextureInfo ? Or simply add Scale to TextureInfo and use only TextureInfo? +// Propose this change to the official specification! + +// NormalTextureInfo is a reference to a texture. +type NormalTextureInfo struct { + Index int // The index of the texture. Required. + TexCoord int // The set index of texture's TEXCOORD attribute used for texture coordinate mapping. Not required. Default is 0. + Scale float32 // The scalar multiplier applied to each normal vector of the normal texture. Not required. Default is 1. + Extensions map[string]interface{} // Dictionary object with extension-specific objects. Not required. + Extras interface{} // Application-specific data. Not required. +} + +// OcclusionTextureInfo is a reference to a texture. +type OcclusionTextureInfo struct { + Index int // The index of the texture. Required. + TexCoord int // The set index of texture's TEXCOORD attribute used for texture coordinate mapping. Not required. Default is 0. + Strength float32 // The scalar multiplier controlling the amount of occlusion applied. Not required. Default is 1. + Extensions map[string]interface{} // Dictionary object with extension-specific objects. Not required. + Extras interface{} // Application-specific data. Not required. +} + +// Orthographic is an orthographic camera containing properties to create an orthographic projection matrix. +type Orthographic struct { + Xmag float32 // The floating-point horizontal magnification of the view. Required. + Ymag float32 // The floating-point vertical magnification of the view. Required. + Zfar float32 // The floating-point distance to the far clipping plane. Zfar must be greater than Znear. Required. + Znear float32 // The floating-point distance to the near clipping plane. Required. + Extensions map[string]interface{} // Dictionary object with extension-specific objects. Not required. + Extras interface{} // Application-specific data. Not required. +} + +// PbrMetallicRoughness is a set of parameter values that are used to define the metallic-roughness material model from Physically-Based Rendering (PBR) methodology. +type PbrMetallicRoughness struct { + BaseColorFactor *[4]float32 // The material's base color factor. Not required. Default is [1,1,1,1] + BaseColorTexture *TextureInfo // The base color texture. Not required. + MetallicFactor *float32 // The metalness of the material. Not required. Default is 1. + RoughnessFactor *float32 // The roughness of the material. Not required. Default is 1. + MetallicRoughnessTexture *TextureInfo // The metallic-roughness texture. Not required. + Extensions map[string]interface{} // Dictionary object with extension-specific objects. Not required. + Extras interface{} // Application-specific data. Not required. +} + +// Perspective is a perspective camera containing properties to create a perspective projection matrix. +type Perspective struct { + AspectRatio *float32 // The floating-point aspect ratio of the field of view. Not required. + Yfov float32 // The floating-point vertical field of view in radians. Required. + Zfar *float32 // The floating-point distance to the far clipping plane. Not required. + Znear float32 // The floating-point distance to the near clipping plane. Required. + Extensions map[string]interface{} // Dictionary object with extension-specific objects. Not required. + Extras interface{} // Application-specific data. Not required. +} + +// Primitive represents geometry to be rendered with the given material. +type Primitive struct { + Attributes map[string]int // A dictionary object, where each key corresponds to mesh attribute semantic and each value is the index of the accessor containing attribute's data. Required. + Indices *int // The index of the accessor that contains the indices. Not required. + Material *int // The index of the material to apply to this primitive when rendering. Not required. + Mode *int // The type of primitives to render. Not required. Default is 4 (TRIANGLES). + Targets []map[string]int // An array of Morph Targets. Each Morph Target is a dictionary mapping attributes (only POSITION, NORMAL, and TANGENT supported) to their deviations in the Morph Target. + Extensions map[string]interface{} // Dictionary object with extension-specific objects. Not required. + Extras interface{} // Application-specific data. Not required. +} + +// Sampler represents a texture sampler with properties for filtering and wrapping modes. +type Sampler struct { + MagFilter *int // Magnification filter. Not required. + MinFilter *int // Minification filter. Not required. + WrapS *int // s coordinate wrapping mode. Not required. Default is 10497 (REPEAT). + WrapT *int // t coordinate wrapping mode. Not required. Default is 10497 (REPEAT). + Name string // The user-defined name of this object. Not required. + Extensions map[string]interface{} // Dictionary object with extension-specific objects. Not required. + Extras interface{} // Application-specific data. Not required. +} + +// Scene contains root nodes. +type Scene struct { + Nodes []int // The indices of the root nodes. Not required. + Name string // The user-defined name of this object. Not required. + Extensions map[string]interface{} // Dictionary object with extension-specific objects. Not required. Not required. + Extras interface{} // Application-specific data. Not required. Not required. +} + +// Joints and matrices defining a skin. +type Skin struct { + InverseBindMatrices int // The index of the accessor containing the floating-point 4x4 inverse-bind matrices. The default is that each matrix is a 4x4 identity matrix, which implies that inverse-bind matrices were pre-applied. Not required. + Skeleton *int // The index of the node used as a skeleton root. When undefined, joints transforms resolve to scene root. Not required. + Joints []int // Indices of skeleton nodes, used as joints in this skin. Required. + Name string // The user-define named of this object. Not required. + Extensions map[string]interface{} // Dictionary object with extension-specific objects. Not required. + Extras interface{} // Application-specific data. Not required. + + cache *graphic.Skeleton // Cached skin. +} + +// Sparse storage of attributes that deviate from their initialization value. +type Sparse struct { + Count int // Number of entries stored in the sparse array. Required. + Indices []int // Index array of size count that points to those accessor attributes that deviate from their initialization value. Indices must strictly increase. Required. + Values []int // Array of size count times number of components, storing the displaced accessor attributes pointed by indices. Substituted values must have the same componentType and number of components as the base accessor. Required. + Extensions map[string]interface{} // Dictionary object with extension-specific objects. Not required. + Extras interface{} // Application-specific data. Not required. +} + +// Target represents the index of the node and TRS property than an animation channel targets. +type Target struct { + Node int // The index of the node to target. Not required. + Path string // The name of the node's TRS property to modify, or the "weights" of the Morph Targets it instantiates. Required. + // For the "translation" property, the values that are provided by the sampler are the translation along the x, y, and z axes. + // For the "rotation" property, the values are a quaternion in the order (x, y, z, w), where w is the scalar. + // For the "scale" property, the values are the scaling factors along the x, y, and z axes. + Extensions map[string]interface{} // Dictionary object with extension-specific objects. Not required. + Extras interface{} // Application-specific data. Not required. +} + +// Texture represents a texture and its sampler. +type Texture struct { + Sampler *int // The index of the sampler used by this texture. When undefined, a sampler with REPEAT wrapping and AUTO filtering should be used. Not required. + Source int // The index of the image used by this texture. Not required. + Name string // The user-defined name of this object. Not required. + Extensions map[string]interface{} // Dictionary object with extension-specific objects. Not required. Not required. + Extras interface{} // Application-specific data. Not required. Not required. +} + +// TextureInfo is a reference to a texture. +type TextureInfo struct { + Index int // The index of the texture. Required. + TexCoord int // The set index of texture's TEXCOORD attribute used for texture coordinate mapping. Not required. Default is 0. + Extensions map[string]interface{} // Dictionary object with extension-specific objects. Not required. + Extras interface{} // Application-specific data. Not required. +} + +// Values is an array of size accessor.sparse.count times number of components storing the displaced accessor attributes pointed by accessor.sparse.indices. +type Values struct { + BufferView int // The index of the bufferView with sparse values. Referenced bufferView can't have ARRAY_BUFFER or ELEMENT_ARRAY_BUFFER target. Required. + ByteOffset int // The offset relative to the start of the bufferView in bytes. Must be aligned. Not required. Default is 0. + Extensions map[string]interface{} // Dictionary object with extension-specific objects. Not required. + Extras interface{} // Application-specific data. Not required. +} + +// Primitive types. +const ( + POINTS = 0 + LINES = 1 + LINE_LOOP = 2 + LINE_STRIP = 3 + TRIANGLES = 4 + TRIANGLE_STRIP = 5 + TRIANGLE_FAN = 6 +) + +// OpenGL array types. +const ( + ARRAY_BUFFER = 34962 // For vertex attributes + ELEMENT_ARRAY_BUFFER = 34963 // For indices +) + +// Texture filtering modes. +const ( + NEAREST = 9728 + LINEAR = 9729 + NEAREST_MIPMAP_NEAREST = 9984 + LINEAR_MIPMAP_NEAREST = 9985 + NEAREST_MIPMAP_LINEAR = 9986 + LINEAR_MIPMAP_LINEAR = 9987 +) + +// Texture sampling modes. +const ( + CLAMP_TO_EDGE = 33071 + MIRRORED_REPEAT = 33648 + REPEAT = 10497 +) + +// Possible componentType values. +const ( + BYTE = 5120 + UNSIGNED_BYTE = 5121 + SHORT = 5122 + UNSIGNED_SHORT = 5123 + UNSIGNED_INT = 5125 + FLOAT = 5126 +) + +// Attribute element types. +const ( + SCALAR = "SCALAR" + VEC2 = "VEC2" + VEC3 = "VEC3" + VEC4 = "VEC4" + MAT2 = "MAT2" + MAT3 = "MAT3" + MAT4 = "MAT4" +) + +// TypeSizes maps an attribute element type to the number of components it contains. +var TypeSizes = map[string]int{ + SCALAR: 1, + VEC2: 2, + VEC3: 3, + VEC4: 4, + MAT2: 4, + MAT3: 9, + MAT4: 16, +} + +// AttributeName maps the glTF attribute name to the internal g3n attribute type. +var AttributeName = map[string]gls.AttribType{ + "POSITION": gls.VertexPosition, + "NORMAL": gls.VertexNormal, + "TANGENT": gls.VertexTangent, + "TEXCOORD_0": gls.VertexTexcoord, + "TEXCOORD_1": gls.VertexTexcoord2, + "COLOR_0": gls.VertexColor, + "JOINTS_0": gls.SkinIndex, + "WEIGHTS_0": gls.SkinWeight, +} + +type GLB struct { + Header GLBHeader + JSON GLBChunk + Data GLBChunk +} + +type GLBHeader struct { + Magic uint32 + Version uint32 + Length uint32 // Not used directly +} + +type GLBChunk struct { + Length uint32 + Type uint32 +} + +const ( + GLBMagic = 0x46546C67 + GLBJson = 0x4E4F534A + GLBBin = 0x004E4942 +)