mirror of https://github.com/SolarLune/tetra3d.git
1696 lines
49 KiB
Go
1696 lines
49 KiB
Go
package tetra3d
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"image"
|
|
"io"
|
|
"io/fs"
|
|
"log"
|
|
"math"
|
|
"path"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/hajimehoshi/ebiten/v2"
|
|
"github.com/hajimehoshi/ebiten/v2/ebitenutil"
|
|
"github.com/qmuntal/gltf"
|
|
"github.com/qmuntal/gltf/ext/lightspuntual"
|
|
"github.com/qmuntal/gltf/modeler"
|
|
|
|
_ "image/png"
|
|
)
|
|
|
|
type GLTFLoadOptions struct {
|
|
// Width and height of loaded Cameras. Defaults to -1 each, which will then instead load the size from the camera's resolution as exported in the GLTF file;
|
|
// if the camera resolution isn't set there, cameras will load with a 1920x1080 camera size.
|
|
CameraWidth, CameraHeight int
|
|
CameraDepth bool // If cameras should render depth or not
|
|
DefaultToAutoTransparency bool // If DefaultToAutoTransparency is true, then opaque materials become Auto transparent materials in Tetra3D.
|
|
// DependentLibraryResolver is a function that takes a relative path (string) to the blend file representing the dependent Library that the loading
|
|
// Library requires. This function should return a reference to the dependent Library; if it returns nil, the linked objects from the dependent Library
|
|
// will not be instantiated in the loading Library.
|
|
// An example would be loading a level (level.gltf) composed of assets from another file (a GLTF file exported from assets.blend, which is a directory up).
|
|
// In this example, loading level.gltf would require the dependent library, found in assets.gltf. Loading level.gltf will refer to objects linked from the assets
|
|
// blend file, known as "../assets.blend".
|
|
// You could then simply load the assets library first and then code the DependentLibraryResolver function to take the assets library, or code the
|
|
// function to use the path to load the library on demand. You could then store the loaded result as necessary if multiple levels use this assets Library.
|
|
DependentLibraryResolver func(blendPath string) *Library
|
|
LoadExternalTextures bool // Whether any external textures should automatically be loaded if you load a GLTF file using LoadGLTFFile(). Defaults to true.
|
|
|
|
rootFilename string
|
|
externalBufferFileSystem fs.FS // The file system to use for loading external buffers; automatically set if you use LoadGLTFFile().
|
|
}
|
|
|
|
// DefaultGLTFLoadOptions creates an instance of GLTFLoadOptions with some sensible defaults.
|
|
func DefaultGLTFLoadOptions() *GLTFLoadOptions {
|
|
return &GLTFLoadOptions{
|
|
CameraWidth: -1,
|
|
CameraHeight: -1,
|
|
CameraDepth: true,
|
|
DefaultToAutoTransparency: true,
|
|
LoadExternalTextures: true,
|
|
}
|
|
}
|
|
|
|
// LoadGLTFFileSystem loads a .gltf or .glb file from the file system given using the filename provided.
|
|
// The provided GLTFLoadOptions struct alters how the file is loaded.
|
|
// Passing nil for gltfLoadOptions will load the file using default load options.
|
|
// LoadGLTFFileSystem properly handles .gltf + .bin file pairs.
|
|
// LoadGLTFFileSystem will return a Library, and an error if the process fails.
|
|
func LoadGLTFFileSystem(fileSystem fs.FS, filename string, gltfLoadOptions *GLTFLoadOptions) (*Library, error) {
|
|
|
|
file, err := fileSystem.Open(filename)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if gltfLoadOptions == nil {
|
|
gltfLoadOptions = DefaultGLTFLoadOptions()
|
|
}
|
|
|
|
gltfLoadOptions.externalBufferFileSystem = fileSystem
|
|
gltfLoadOptions.rootFilename = filename
|
|
|
|
return LoadGLTFData(file, gltfLoadOptions)
|
|
|
|
}
|
|
|
|
// LoadGLTFData loads a .gltf or .glb file from the byte data given, using a provided GLTFLoadOptions struct to alter how the file is loaded.
|
|
// Passing nil for loadOptions will load the file using default load options. Unlike with DAE files, Animations (including armature-based
|
|
// animations) and Cameras (assuming they are exported in the GLTF file) will be parsed properly.
|
|
// LoadGLTFFile will not work by default with external byte information buffers (i.e. .gltf and .glb file pairs)
|
|
// as the buffer is referenced in .gltf as just a filename. To handle this properly, load the .gltf file using LoadGLTFFile().
|
|
// LoadGLTFFile will return a Library, and an error if the process fails.
|
|
func LoadGLTFData(data io.Reader, gltfLoadOptions *GLTFLoadOptions) (*Library, error) {
|
|
|
|
doc := gltf.NewDocument()
|
|
|
|
decoder := gltf.NewDecoder(data)
|
|
|
|
err := decoder.Decode(doc)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// base directory when using a file system
|
|
baseDir := ""
|
|
|
|
if gltfLoadOptions == nil {
|
|
gltfLoadOptions = DefaultGLTFLoadOptions()
|
|
} else if gltfLoadOptions.externalBufferFileSystem != nil {
|
|
|
|
// We get the base directory for the loaded filename
|
|
dir, _ := path.Split(gltfLoadOptions.rootFilename)
|
|
baseDir = dir
|
|
|
|
for _, buffer := range doc.Buffers {
|
|
|
|
// We need to load this buffer
|
|
if buffer.URI != "" && len(buffer.Data) == 0 {
|
|
|
|
openedFile, err := gltfLoadOptions.externalBufferFileSystem.Open(path.Join(baseDir, buffer.URI))
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
bytesData, err := io.ReadAll(openedFile)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
buffer.Data = bytesData
|
|
continue
|
|
// decoder = gltf.NewDecoderFS(bytes.NewReader(data), gltfLoadOptions.externalBufferFileSystem)
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
library := NewLibrary()
|
|
|
|
var images []*ebiten.Image
|
|
|
|
exportedTextures := false
|
|
|
|
type Collection struct {
|
|
Objects []string
|
|
Offset []float64
|
|
Path string
|
|
}
|
|
|
|
collections := map[string]Collection{}
|
|
|
|
t3dExport := false
|
|
|
|
camWidth := gltfLoadOptions.CameraWidth
|
|
camHeight := gltfLoadOptions.CameraHeight
|
|
camDefaultSize := false
|
|
exportedCameras := []*Camera{}
|
|
sectorDetection := SectorDetectionTypeVertices
|
|
|
|
if gltfLoadOptions.CameraWidth <= 0 && gltfLoadOptions.CameraHeight <= 0 {
|
|
camWidth = 640
|
|
camHeight = 360
|
|
camDefaultSize = true
|
|
}
|
|
|
|
globalExporterSettings := doc.Scenes[0].Extras.(map[string]interface{})
|
|
|
|
if len(doc.Scenes) > 0 {
|
|
|
|
if doc.Scenes[0].Extras != nil {
|
|
|
|
if camDefaultSize {
|
|
|
|
if cr, exists := globalExporterSettings["t3dRenderResolutionW__"]; exists {
|
|
if w, ok := cr.(float64); ok {
|
|
camWidth = int(w)
|
|
}
|
|
}
|
|
|
|
if cr, exists := globalExporterSettings["t3dRenderResolutionH__"]; exists {
|
|
if h, ok := cr.(float64); ok {
|
|
camHeight = int(h)
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
exportFormat := 0 // 0 = GLB, 1 = GLTF Separate
|
|
|
|
if format, exists := globalExporterSettings["t3dExportFormat__"]; exists {
|
|
exportFormat = int(format.(float64))
|
|
}
|
|
|
|
if et, exists := globalExporterSettings["t3dPackTextures__"]; exists {
|
|
t3dExport = true
|
|
exportedTextures = et.(bool) && exportFormat == 0
|
|
}
|
|
|
|
if col, exists := globalExporterSettings["t3dCollections__"]; exists {
|
|
t3dExport = true
|
|
data := col.(map[string]interface{})
|
|
|
|
jsonData, err := json.Marshal(data)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
err = json.Unmarshal(jsonData, &collections)
|
|
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
}
|
|
|
|
if cameras, exists := globalExporterSettings["t3dView3DCameraData__"]; exists {
|
|
|
|
for _, cameraEntry := range cameras.([]interface{}) {
|
|
|
|
ce := cameraEntry.(map[string]interface{})
|
|
|
|
clipStart := ce["clip_start"].(float64)
|
|
clipEnd := ce["clip_end"].(float64)
|
|
fovY := ce["fovY"].(float64)
|
|
perspective := ce["perspective"].(bool)
|
|
|
|
rotation := ce["rotation"].([]interface{})
|
|
location := ce["location"].([]interface{})
|
|
rotationMatrix := NewMatrix4()
|
|
|
|
for i := 0; i < 3; i++ {
|
|
rotRow := rotation[i].([]interface{})
|
|
rotationMatrix.SetColumn(i, NewVector(rotRow[0].(float64), rotRow[1].(float64), rotRow[2].(float64)))
|
|
}
|
|
|
|
locVec := NewVector(location[0].(float64), location[1].(float64), location[2].(float64))
|
|
|
|
newCam := NewCamera(camWidth, camHeight)
|
|
|
|
newCam.SetLocalPositionVec(locVec)
|
|
newCam.SetLocalRotation(rotationMatrix)
|
|
|
|
newCam.near = clipStart
|
|
newCam.far = clipEnd
|
|
newCam.SetFieldOfView(fovY)
|
|
newCam.SetPerspective(perspective)
|
|
|
|
// newCam.SetOrthoScale(orthoZoom) // Ortho scale isn't implemented yet
|
|
|
|
exportedCameras = append(exportedCameras, newCam)
|
|
}
|
|
|
|
}
|
|
|
|
if value, exists := globalExporterSettings["t3dSectorDetectionType__"]; exists {
|
|
switch value.(string) {
|
|
case "AABB":
|
|
sectorDetection = SectorDetectionTypeAABB
|
|
case "VERTICES":
|
|
sectorDetection = SectorDetectionTypeVertices
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if exportedTextures {
|
|
images = make([]*ebiten.Image, len(doc.Images))
|
|
for i, gltfImage := range doc.Images {
|
|
|
|
imageData, err := modeler.ReadBufferView(doc, doc.BufferViews[*gltfImage.BufferView])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
byteReader := bytes.NewReader(imageData)
|
|
|
|
img, _, err := image.Decode(byteReader)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
images[i] = ebiten.NewImageFromImage(img)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
externalTextures := map[string]*ebiten.Image{}
|
|
|
|
for _, gltfMat := range doc.Materials {
|
|
|
|
newMat := NewMaterial(gltfMat.Name)
|
|
newMat.library = library
|
|
|
|
newMat.BackfaceCulling = !gltfMat.DoubleSided
|
|
|
|
if texture := gltfMat.PBRMetallicRoughness.BaseColorTexture; texture != nil {
|
|
if exportedTextures {
|
|
newMat.Texture = images[*doc.Textures[texture.Index].Source]
|
|
} else {
|
|
newMat.TexturePath = doc.Images[*doc.Textures[texture.Index].Source].URI
|
|
if gltfLoadOptions.LoadExternalTextures && gltfLoadOptions.externalBufferFileSystem != nil {
|
|
if texture, ok := externalTextures[newMat.TexturePath]; ok {
|
|
newMat.Texture = texture
|
|
} else {
|
|
texture, _, err := ebitenutil.NewImageFromFileSystem(gltfLoadOptions.externalBufferFileSystem, baseDir+newMat.TexturePath)
|
|
if err != nil {
|
|
log.Println(err)
|
|
} else {
|
|
newMat.Texture = texture
|
|
}
|
|
externalTextures[newMat.TexturePath] = texture
|
|
}
|
|
// newMat.Texture =
|
|
}
|
|
}
|
|
}
|
|
|
|
if gltfMat.Extras != nil {
|
|
if dataMap, isMap := gltfMat.Extras.(map[string]interface{}); isMap {
|
|
|
|
if c, exists := dataMap["t3dMaterialColor__"]; exists {
|
|
color := c.([]interface{})
|
|
newMat.Color.R = float32(color[0].(float64))
|
|
newMat.Color.G = float32(color[1].(float64))
|
|
newMat.Color.B = float32(color[2].(float64))
|
|
newMat.Color.A = float32(color[3].(float64))
|
|
}
|
|
|
|
if s, exists := dataMap["t3dMaterialShadeless__"]; exists {
|
|
newMat.Shadeless = s.(float64) > 0.5
|
|
}
|
|
|
|
if s, exists := dataMap["t3dMaterialFogless__"]; exists {
|
|
newMat.Fogless = s.(float64) > 0.5
|
|
}
|
|
|
|
if s, exists := dataMap["t3dBlendMode__"]; exists {
|
|
switch int(s.(float64)) {
|
|
case 0:
|
|
newMat.Blend = ebiten.BlendSourceOver
|
|
case 1:
|
|
newMat.Blend = ebiten.BlendLighter
|
|
case 2:
|
|
// Custom multiply blend mode
|
|
newMat.Blend = ebiten.Blend{
|
|
BlendFactorSourceRGB: ebiten.BlendFactorDestinationColor,
|
|
BlendFactorSourceAlpha: ebiten.BlendFactorDestinationColor,
|
|
BlendFactorDestinationRGB: ebiten.BlendFactorZero,
|
|
BlendFactorDestinationAlpha: ebiten.BlendFactorZero,
|
|
BlendOperationRGB: ebiten.BlendOperationAdd,
|
|
BlendOperationAlpha: ebiten.BlendOperationAdd,
|
|
}
|
|
case 3:
|
|
newMat.Blend = ebiten.BlendDestinationOut
|
|
}
|
|
|
|
}
|
|
|
|
if s, exists := dataMap["t3dBillboardMode__"]; exists {
|
|
switch int(s.(float64)) {
|
|
case 0:
|
|
newMat.BillboardMode = BillboardModeNone
|
|
case 1:
|
|
newMat.BillboardMode = BillboardModeFixedVertical
|
|
case 2:
|
|
newMat.BillboardMode = BillboardModeHorizontal
|
|
case 3:
|
|
newMat.BillboardMode = BillboardModeAll
|
|
}
|
|
|
|
}
|
|
|
|
if s, exists := dataMap["t3dCustomDepthOn__"]; exists {
|
|
newMat.CustomDepthOffsetOn = s.(float64) > 0
|
|
}
|
|
if s, exists := dataMap["t3dCustomDepthValue__"]; exists {
|
|
newMat.CustomDepthOffsetValue = s.(float64)
|
|
}
|
|
if s, exists := dataMap["t3dMaterialLightingMode__"]; exists {
|
|
// newMat.NormalsAlwaysFaceLights = s.(float64) > 0
|
|
switch int(s.(float64)) {
|
|
case 0:
|
|
newMat.LightingMode = LightingModeDefault
|
|
case 1:
|
|
newMat.LightingMode = LightingModeFixedNormals
|
|
case 2:
|
|
newMat.LightingMode = LightingModeDoubleSided
|
|
}
|
|
}
|
|
if s, exists := dataMap["t3dVisible__"]; exists {
|
|
newMat.Visible = s.(float64) > 0
|
|
}
|
|
|
|
// At this point, parenting should be set up.
|
|
|
|
if gameProps, exists := dataMap["t3dGameProperties__"]; exists {
|
|
|
|
for _, p := range gameProps.([]interface{}) {
|
|
|
|
name, value := handleGameProperties(p)
|
|
|
|
newMat.Properties().Add(name).Set(value)
|
|
|
|
}
|
|
}
|
|
|
|
// Non-Tetra3D custom data
|
|
for tagName, data := range dataMap {
|
|
if !strings.HasPrefix(tagName, "t3d") || !strings.HasSuffix(tagName, "__") {
|
|
newMat.Properties().Add(tagName).Set(data)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// If it's not exported through the Tetra addon, then just load the default GLTF material color value
|
|
if !t3dExport {
|
|
color := gltfMat.PBRMetallicRoughness.BaseColorFactor
|
|
newMat.Color.R = float32(color[0])
|
|
newMat.Color.G = float32(color[1])
|
|
newMat.Color.B = float32(color[2])
|
|
newMat.Color.A = float32(color[3])
|
|
}
|
|
|
|
newMat.Color.ConvertTosRGB()
|
|
|
|
if gltfMat.AlphaMode == gltf.AlphaOpaque {
|
|
if gltfLoadOptions.DefaultToAutoTransparency {
|
|
newMat.TransparencyMode = TransparencyModeAuto
|
|
} else {
|
|
newMat.TransparencyMode = TransparencyModeOpaque
|
|
}
|
|
} else if gltfMat.AlphaMode == gltf.AlphaBlend {
|
|
newMat.TransparencyMode = TransparencyModeTransparent
|
|
} else { //if gltfMat.AlphaMode == gltf.AlphaMask
|
|
newMat.TransparencyMode = TransparencyModeAlphaClip
|
|
}
|
|
|
|
library.Materials[gltfMat.Name] = newMat
|
|
|
|
}
|
|
|
|
for _, mesh := range doc.Meshes {
|
|
|
|
// If t3dGrid__ is set on a mesh, then it can be skipped for loading
|
|
if mesh.Extras != nil {
|
|
|
|
if dataMap, isMap := mesh.Extras.(map[string]interface{}); isMap {
|
|
|
|
if _, exists := dataMap["t3dGrid__"]; exists {
|
|
continue
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
newMesh := NewMesh(mesh.Name)
|
|
library.Meshes[mesh.Name] = newMesh
|
|
newMesh.library = library
|
|
|
|
colorChannelNames := []string{}
|
|
|
|
if mesh.Extras != nil {
|
|
|
|
if dataMap, isMap := mesh.Extras.(map[string]interface{}); isMap {
|
|
|
|
if vcNames, exists := dataMap["t3dVertexColorNames__"]; exists {
|
|
for index, name := range vcNames.([]interface{}) {
|
|
newMesh.VertexColorChannelNames[name.(string)] = index
|
|
colorChannelNames = append(colorChannelNames, name.(string))
|
|
}
|
|
}
|
|
|
|
if unique, exists := dataMap["t3dUniqueMesh__"]; exists && unique.(float64) > 0 {
|
|
if uniqueMats, exists := dataMap["t3dUniqueMaterials__"]; exists && uniqueMats.(float64) > 0 {
|
|
newMesh.Unique = MeshUniqueMeshAndMaterials
|
|
} else {
|
|
newMesh.Unique = MeshUniqueMesh
|
|
}
|
|
}
|
|
|
|
// Non-Tetra3D custom data
|
|
for tagName, data := range dataMap {
|
|
if !strings.HasPrefix(tagName, "t3d") || !strings.HasSuffix(tagName, "__") {
|
|
newMesh.properties.Add(tagName).Set(data)
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for _, v := range mesh.Primitives {
|
|
|
|
posBuffer := [][3]float32{}
|
|
vertPos, err := modeler.ReadPosition(doc, doc.Accessors[v.Attributes[gltf.POSITION]], posBuffer)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
vertexData := make([]VertexInfo, len(vertPos))
|
|
|
|
for i, v := range vertPos {
|
|
|
|
vertexData[i] = NewVertex(
|
|
float64(v[0]),
|
|
float64(v[1]),
|
|
float64(v[2]),
|
|
0, 0,
|
|
)
|
|
|
|
}
|
|
|
|
if texCoordAccessor, texCoordExists := v.Attributes[gltf.TEXCOORD_0]; texCoordExists {
|
|
|
|
uvBuffer := [][2]float32{}
|
|
|
|
texCoords, err := modeler.ReadTextureCoord(doc, doc.Accessors[texCoordAccessor], uvBuffer)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for i, v := range texCoords {
|
|
vertexData[i].U = float64(v[0])
|
|
vertexData[i].V = -(float64(v[1]) - 1)
|
|
}
|
|
|
|
}
|
|
|
|
if normalAccessor, normalExists := v.Attributes[gltf.NORMAL]; normalExists {
|
|
|
|
normalBuffer := [][3]float32{}
|
|
|
|
normals, err := modeler.ReadNormal(doc, doc.Accessors[normalAccessor], normalBuffer)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for i, v := range normals {
|
|
vertexData[i].NormalX = float64(v[0])
|
|
vertexData[i].NormalY = float64(v[1])
|
|
vertexData[i].NormalZ = float64(v[2])
|
|
}
|
|
|
|
}
|
|
|
|
// Blender's GLTF exporter changed - now the active vertex color channel
|
|
// is turned to "COLOR_0", while the other channels are turned into attributes
|
|
// accessible by their names like so: "_CHANNELNAMEINALLCAPS".
|
|
|
|
if len(colorChannelNames) > 0 {
|
|
|
|
dataMap, _ := mesh.Extras.(map[string]interface{})
|
|
activeChannelIndex := int(dataMap["t3dActiveVertexColorIndex__"].(float64))
|
|
|
|
for _, name := range colorChannelNames {
|
|
|
|
name = "_" + strings.ToUpper(name)
|
|
|
|
vertexColorAccessor, colorChannelExists := v.Attributes[name]
|
|
|
|
if !colorChannelExists {
|
|
vertexColorAccessor, colorChannelExists = v.Attributes["COLOR_0"]
|
|
}
|
|
|
|
if colorChannelExists {
|
|
|
|
vcBuffer := [][4]uint16{}
|
|
|
|
colors, err := modeler.ReadColor64(doc, doc.Accessors[vertexColorAccessor], vcBuffer)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for i, colorData := range colors {
|
|
|
|
// colors are exported from Blender as linear, but display as sRGB so we'll convert them here
|
|
color := NewColor(
|
|
float32(colorData[0])/math.MaxUint16,
|
|
float32(colorData[1])/math.MaxUint16,
|
|
float32(colorData[2])/math.MaxUint16,
|
|
float32(colorData[3])/math.MaxUint16,
|
|
).ConvertTosRGB()
|
|
vertexData[i].Colors = append(vertexData[i].Colors, color)
|
|
vertexData[i].ActiveColorChannel = activeChannelIndex
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if weightAccessor, weightExists := v.Attributes[gltf.WEIGHTS_0]; weightExists {
|
|
|
|
weightBuffer := [][4]float32{}
|
|
weights, err := modeler.ReadWeights(doc, doc.Accessors[weightAccessor], weightBuffer)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
boneBuffer := [][4]uint16{}
|
|
bones, err := modeler.ReadJoints(doc, doc.Accessors[v.Attributes[gltf.JOINTS_0]], boneBuffer)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Store weights and bones; we don't want to waste space and speed storing bones if their weights are 0
|
|
for w := range weights {
|
|
vWeights := weights[w]
|
|
for i := range vWeights {
|
|
if vWeights[i] > 0 {
|
|
vertexData[w].Weights = append(vertexData[w].Weights, vWeights[i])
|
|
vertexData[w].Bones = append(vertexData[w].Bones, bones[w][i])
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
newMesh.AddVertices(vertexData...)
|
|
|
|
indexBuffer := []uint32{}
|
|
|
|
indices, err := modeler.ReadIndices(doc, doc.Accessors[*v.Indices], indexBuffer)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var mat *Material
|
|
|
|
if v.Material != nil {
|
|
gltfMat := doc.Materials[*v.Material]
|
|
mat = library.Materials[gltfMat.Name]
|
|
}
|
|
|
|
newIndices := make([]int, len(indices))
|
|
|
|
for i, j := range indices {
|
|
newIndices[i] = int(j)
|
|
}
|
|
|
|
newMesh.AddMeshPart(mat, newIndices...)
|
|
|
|
newMesh.UpdateBounds()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for _, gltfAnim := range doc.Animations {
|
|
anim := NewAnimation(gltfAnim.Name)
|
|
anim.library = library
|
|
library.Animations[gltfAnim.Name] = anim
|
|
|
|
animLength := 0.0
|
|
|
|
for _, channel := range gltfAnim.Channels {
|
|
|
|
sampler := gltfAnim.Samplers[*channel.Sampler]
|
|
|
|
channelName := "root"
|
|
if channel.Target.Node != nil {
|
|
channelName = doc.Nodes[*channel.Target.Node].Name
|
|
}
|
|
|
|
animChannel := anim.Channels[channelName]
|
|
if animChannel == nil {
|
|
animChannel = anim.AddChannel(channelName)
|
|
}
|
|
|
|
if channel.Target.Path == gltf.TRSTranslation {
|
|
|
|
id, err := modeler.ReadAccessor(doc, doc.Accessors[sampler.Input], nil)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
inputData := id.([]float32)
|
|
|
|
od, err := modeler.ReadAccessor(doc, doc.Accessors[sampler.Output], nil)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
outputData := od.([][3]float32)
|
|
|
|
track := animChannel.AddTrack(TrackTypePosition)
|
|
track.Interpolation = int(sampler.Interpolation)
|
|
for i := 0; i < len(inputData); i++ {
|
|
t := inputData[i]
|
|
p := outputData[i]
|
|
track.AddKeyframe(float64(t), Vector{float64(p[0]), float64(p[1]), float64(p[2]), 0})
|
|
if float64(t) > animLength {
|
|
animLength = float64(t)
|
|
}
|
|
}
|
|
|
|
} else if channel.Target.Path == gltf.TRSScale {
|
|
|
|
id, err := modeler.ReadAccessor(doc, doc.Accessors[sampler.Input], nil)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
inputData := id.([]float32)
|
|
|
|
od, err := modeler.ReadAccessor(doc, doc.Accessors[sampler.Output], nil)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
outputData := od.([][3]float32)
|
|
|
|
track := animChannel.AddTrack(TrackTypeScale)
|
|
track.Interpolation = int(sampler.Interpolation)
|
|
for i := 0; i < len(inputData); i++ {
|
|
t := inputData[i]
|
|
p := outputData[i]
|
|
track.AddKeyframe(float64(t), Vector{float64(p[0]), float64(p[1]), float64(p[2]), 0})
|
|
if float64(t) > animLength {
|
|
animLength = float64(t)
|
|
}
|
|
}
|
|
|
|
} else if channel.Target.Path == gltf.TRSRotation {
|
|
|
|
id, err := modeler.ReadAccessor(doc, doc.Accessors[sampler.Input], nil)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
inputData := id.([]float32)
|
|
|
|
od, err := modeler.ReadAccessor(doc, doc.Accessors[sampler.Output], nil)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
outputData := od.([][4]float32)
|
|
|
|
track := animChannel.AddTrack(TrackTypeRotation)
|
|
track.Interpolation = int(sampler.Interpolation)
|
|
|
|
for i := 0; i < len(inputData); i++ {
|
|
t := inputData[i]
|
|
p := outputData[i]
|
|
track.AddKeyframe(float64(t), NewQuaternion(float64(p[0]), float64(p[1]), float64(p[2]), float64(p[3])))
|
|
if float64(t) > animLength {
|
|
animLength = float64(t)
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if gltfAnim.Extras != nil {
|
|
m := gltfAnim.Extras.(map[string]interface{})
|
|
if markerData, exists := m["t3dMarkers__"]; exists {
|
|
for _, mData := range markerData.([]interface{}) {
|
|
|
|
marker := mData.(map[string]interface{})
|
|
|
|
anim.Markers = append(anim.Markers, Marker{
|
|
Name: marker["name"].(string),
|
|
Time: marker["time"].(float64),
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
anim.Length = animLength
|
|
|
|
}
|
|
|
|
// skins := []*Skin{}
|
|
|
|
// for _, skin := range doc.Skins {
|
|
|
|
// skins = append(skins, skin.)
|
|
|
|
// }
|
|
|
|
objects := []INode{}
|
|
|
|
objToNode := map[INode]*gltf.Node{}
|
|
|
|
nodeHasProp := func(node *gltf.Node, propName string) bool {
|
|
|
|
if node.Extras == nil {
|
|
return false
|
|
}
|
|
|
|
if _, exists := node.Extras.(map[string]interface{})[propName]; exists {
|
|
return true
|
|
}
|
|
return false
|
|
|
|
}
|
|
|
|
nodeGetProp := func(node *gltf.Node, propName string) interface{} {
|
|
|
|
if node.Extras == nil {
|
|
return nil
|
|
}
|
|
|
|
if value, exists := node.Extras.(map[string]interface{})[propName]; exists {
|
|
return value
|
|
}
|
|
return nil
|
|
|
|
}
|
|
|
|
// Node / Object creation
|
|
for _, node := range doc.Nodes {
|
|
|
|
var obj INode
|
|
|
|
var mesh *Mesh
|
|
|
|
if node.Mesh != nil {
|
|
mesh = library.Meshes[doc.Meshes[*node.Mesh].Name]
|
|
}
|
|
|
|
if mesh != nil {
|
|
|
|
if mesh.Unique != MeshUniqueFalse {
|
|
mesh = mesh.Clone()
|
|
}
|
|
|
|
obj = NewModel(node.Name, mesh)
|
|
|
|
if node.Extras != nil && nodeHasProp(node, "t3dAutoBatch__") {
|
|
s := node.Extras.(map[string]interface{})["t3dAutoBatch__"].(float64)
|
|
obj.(*Model).AutoBatchMode = int(s)
|
|
}
|
|
|
|
} else if node.Camera != nil {
|
|
|
|
gltfCam := doc.Cameras[*node.Camera]
|
|
|
|
newCam := NewCamera(camWidth, camHeight)
|
|
newCam.name = node.Name
|
|
newCam.RenderDepth = gltfLoadOptions.CameraDepth
|
|
newCam.updateProjectionMatrix = true
|
|
|
|
if gltfCam.Perspective != nil {
|
|
newCam.near = float64(gltfCam.Perspective.Znear)
|
|
newCam.far = float64(*gltfCam.Perspective.Zfar)
|
|
newCam.fieldOfView = ToDegrees(float64(gltfCam.Perspective.Yfov))
|
|
newCam.perspective = true
|
|
} else if gltfCam.Orthographic != nil {
|
|
newCam.near = float64(gltfCam.Orthographic.Znear)
|
|
newCam.far = float64(gltfCam.Orthographic.Zfar)
|
|
newCam.orthoScale = float64(gltfCam.Orthographic.Xmag * 2)
|
|
newCam.perspective = false
|
|
}
|
|
|
|
camProp := func(cam *gltf.Camera, propName string) any {
|
|
|
|
if cam.Extras == nil {
|
|
return nil
|
|
}
|
|
|
|
if value, exists := cam.Extras.(map[string]interface{})[propName]; exists {
|
|
return value
|
|
}
|
|
return nil
|
|
|
|
}
|
|
|
|
if v := camProp(gltfCam, "t3dSectorRendering__"); v != nil {
|
|
newCam.SectorRendering = v.(float64) > 0
|
|
}
|
|
if v := camProp(gltfCam, "t3dMaxLightCount__"); v != nil {
|
|
newCam.MaxLightCount = int(v.(float64))
|
|
}
|
|
if v := camProp(gltfCam, "t3dSectorRenderDepth__"); v != nil {
|
|
newCam.SectorRenderDepth = int(v.(float64))
|
|
}
|
|
if v := camProp(gltfCam, "t3dPerspectiveCorrectedTextureMapping__"); v != nil {
|
|
newCam.PerspectiveCorrectedTextureMapping = v.(float64) > 0
|
|
}
|
|
|
|
obj = newCam
|
|
|
|
} else if lighting := node.Extensions["KHR_lights_punctual"]; lighting != nil {
|
|
lights := doc.Extensions["KHR_lights_punctual"].(lightspuntual.Lights)
|
|
lightData := lights[lighting.(lightspuntual.LightIndex)]
|
|
|
|
if lightData.Type == lightspuntual.TypeDirectional {
|
|
directionalLight := NewDirectionalLight(node.Name, lightData.Color[0], lightData.Color[1], lightData.Color[2], *lightData.Intensity) // Sun is in "energy"
|
|
obj = directionalLight
|
|
} else if lightData.Type == lightspuntual.TypePoint {
|
|
pointLight := NewPointLight(node.Name, lightData.Color[0], lightData.Color[1], lightData.Color[2], *lightData.Intensity/80) // Point lights have wattage energy
|
|
if !math.IsInf(float64(*lightData.Range), 0) {
|
|
pointLight.Range = float64(*lightData.Range)
|
|
}
|
|
obj = pointLight
|
|
} else {
|
|
// Any unsupported light type just gets turned into an ambient light
|
|
pointLight := NewAmbientLight(node.Name, lightData.Color[0], lightData.Color[1], lightData.Color[2], *lightData.Intensity/80)
|
|
obj = pointLight
|
|
}
|
|
|
|
} else if node.Extras != nil && nodeHasProp(node, "t3dPathPoints__") {
|
|
|
|
points := []Vector{}
|
|
extraMap := node.Extras.(map[string]interface{})
|
|
|
|
for _, p := range extraMap["t3dPathPoints__"].([]interface{}) {
|
|
pointData := p.([]interface{})
|
|
points = append(points, Vector{pointData[0].(float64), pointData[2].(float64), -pointData[1].(float64), 0})
|
|
}
|
|
|
|
path := NewPath(node.Name, points...)
|
|
|
|
if nodeHasProp(node, "t3dPathCyclic__") {
|
|
path.Closed = extraMap["t3dPathCyclic__"].(bool)
|
|
}
|
|
|
|
obj = path
|
|
|
|
} else if node.Extras != nil && nodeHasProp(node, "t3dGridConnections__") {
|
|
|
|
obj = NewGrid(node.Name)
|
|
|
|
extraMap := node.Extras.(map[string]interface{})
|
|
|
|
type gridPointPosition struct {
|
|
X, Y, Z float64
|
|
}
|
|
|
|
creationOrder := []*GridPoint{}
|
|
|
|
parsePosition := func(gpString string) gridPointPosition {
|
|
trimmed := strings.Trim(gpString, "()")
|
|
components := strings.Split(trimmed, ", ")
|
|
x, _ := strconv.ParseFloat(components[0], 64)
|
|
y, _ := strconv.ParseFloat(components[1], 64)
|
|
z, _ := strconv.ParseFloat(components[2], 64)
|
|
|
|
return gridPointPosition{
|
|
X: x,
|
|
Y: y,
|
|
Z: z,
|
|
}
|
|
}
|
|
|
|
for _, k := range extraMap["t3dGridEntries__"].([]interface{}) {
|
|
|
|
key := parsePosition(k.(string))
|
|
gp := NewGridPoint("Grid Point")
|
|
obj.AddChildren(gp)
|
|
gp.SetWorldPosition(key.X, key.Z, -key.Y)
|
|
creationOrder = append(creationOrder, gp)
|
|
|
|
}
|
|
|
|
for k, array := range extraMap["t3dGridConnections__"].(map[string]interface{}) {
|
|
|
|
key, _ := strconv.Atoi(k)
|
|
for _, v := range array.([]interface{}) {
|
|
connectedID, _ := strconv.Atoi(v.(string))
|
|
creationOrder[key].Connect(creationOrder[connectedID])
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
obj = NewNode(node.Name)
|
|
}
|
|
|
|
objToNode[obj] = node
|
|
|
|
obj.setLibrary(library)
|
|
|
|
for _, child := range node.Children {
|
|
obj.AddChildren(objects[int(child)])
|
|
}
|
|
|
|
if node.Extras != nil {
|
|
if dataMap, isMap := node.Extras.(map[string]interface{}); isMap {
|
|
|
|
getOrDefaultBool := func(path string, defaultValue bool) bool {
|
|
if value, exists := dataMap[path]; exists {
|
|
return value.(float64) > 0.5
|
|
}
|
|
return defaultValue
|
|
}
|
|
|
|
getOrDefaultFloat := func(path string, defaultValue float64) float64 {
|
|
if value, exists := dataMap[path]; exists {
|
|
return value.(float64)
|
|
}
|
|
return defaultValue
|
|
}
|
|
|
|
getOrDefaultFloatSlice := func(path string, defaultValues []float64) []float64 {
|
|
if value, exists := dataMap[path]; exists {
|
|
floats := []float64{}
|
|
for _, v := range value.([]interface{}) {
|
|
floats = append(floats, v.(float64))
|
|
}
|
|
return floats
|
|
}
|
|
return defaultValues
|
|
}
|
|
|
|
obj.SetVisible(getOrDefaultBool("t3dVisible__", true), false)
|
|
|
|
if bt, exists := dataMap["t3dBoundsType__"]; exists {
|
|
|
|
boundsType := int(bt.(float64))
|
|
|
|
switch boundsType {
|
|
// case 0: // NONE
|
|
case 1: // AABB
|
|
|
|
var aabb *BoundingAABB
|
|
|
|
if aabbCustomEnabled := getOrDefaultBool("t3dAABBCustomEnabled__", false); aabbCustomEnabled {
|
|
|
|
boundsSize := getOrDefaultFloatSlice("t3dAABBCustomSize__", []float64{2, 2, 2})
|
|
aabb = NewBoundingAABB("BoundingAABB", boundsSize[0], boundsSize[1], boundsSize[2])
|
|
|
|
} else if obj.Type().Is(NodeTypeModel) && obj.(*Model).Mesh != nil {
|
|
mesh := obj.(*Model).Mesh
|
|
dim := mesh.Dimensions
|
|
aabb = NewBoundingAABB("BoundingAABB", dim.Width(), dim.Height(), dim.Depth())
|
|
}
|
|
|
|
if aabb != nil {
|
|
|
|
if obj.Type().Is(NodeTypeModel) && obj.(*Model).Mesh != nil {
|
|
aabb.SetLocalPositionVec(obj.(*Model).Mesh.Dimensions.Center())
|
|
}
|
|
|
|
obj.AddChildren(aabb)
|
|
|
|
} else {
|
|
log.Println("Warning: object " + obj.Name() + " has bounds type BoundingAABB with no size and is not a Model")
|
|
}
|
|
|
|
case 2: // Capsule
|
|
|
|
var capsule *BoundingCapsule
|
|
|
|
if capsuleCustomEnabled := getOrDefaultBool("t3dCapsuleCustomEnabled__", false); capsuleCustomEnabled {
|
|
height := getOrDefaultFloat("t3dCapsuleCustomHeight__", 2)
|
|
radius := getOrDefaultFloat("t3dCapsuleCustomRadius__", 0.5)
|
|
capsule = NewBoundingCapsule("BoundingCapsule", height, radius)
|
|
} else if obj.Type().Is(NodeTypeModel) && obj.(*Model).Mesh != nil {
|
|
mesh := obj.(*Model).Mesh
|
|
dim := mesh.Dimensions
|
|
capsule = NewBoundingCapsule("BoundingCapsule", dim.Height(), math.Max(dim.Width(), dim.Depth())/2)
|
|
}
|
|
|
|
if capsule != nil {
|
|
|
|
if obj.Type().Is(NodeTypeModel) && obj.(*Model).Mesh != nil {
|
|
capsule.SetLocalPositionVec(obj.(*Model).Mesh.Dimensions.Center())
|
|
}
|
|
|
|
obj.AddChildren(capsule)
|
|
|
|
} else {
|
|
log.Println("Warning: object " + obj.Name() + " has bounds type BoundingCapsule with no size and is not a Model")
|
|
}
|
|
|
|
case 3: // Sphere
|
|
|
|
var sphere *BoundingSphere
|
|
|
|
if sphereCustomEnabled := getOrDefaultBool("t3dSphereCustomEnabled__", false); sphereCustomEnabled {
|
|
radius := getOrDefaultFloat("t3dSphereCustomRadius__", 1)
|
|
sphere = NewBoundingSphere("BoundingSphere", radius)
|
|
} else if obj.Type().Is(NodeTypeModel) && obj.(*Model).Mesh != nil {
|
|
|
|
model := obj.(*Model)
|
|
dim := model.Mesh.Dimensions
|
|
scale := model.WorldScale()
|
|
|
|
dim.Min = dim.Min.Mult(scale)
|
|
dim.Max = dim.Max.Mult(scale)
|
|
|
|
sphere = NewBoundingSphere("BoundingSphere", dim.MaxDimension()/2)
|
|
}
|
|
|
|
if sphere != nil {
|
|
|
|
if obj.Type().Is(NodeTypeModel) && obj.(*Model).Mesh != nil {
|
|
sphere.SetLocalPositionVec(obj.(*Model).Mesh.Dimensions.Center())
|
|
}
|
|
|
|
obj.AddChildren(sphere)
|
|
|
|
} else {
|
|
log.Println("Warning: object " + obj.Name() + " has bounds type BoundingSphere with no size and is not a Model")
|
|
}
|
|
|
|
case 4: // Triangles
|
|
|
|
if obj.Type().Is(NodeTypeModel) && obj.(*Model).Mesh != nil {
|
|
|
|
gridSize := 20.0
|
|
|
|
if getOrDefaultBool("t3dTrianglesCustomBroadphaseEnabled__", false) {
|
|
gridSize = getOrDefaultFloat("t3dTrianglesCustomBroadphaseGridSize__", 20)
|
|
}
|
|
|
|
triangles := NewBoundingTriangles("BoundingTriangles", obj.(*Model).Mesh, gridSize)
|
|
|
|
obj.AddChildren(triangles)
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
// Non-Tetra3D custom data
|
|
for tagName, data := range dataMap {
|
|
if !strings.HasPrefix(tagName, "t3d") || !strings.HasSuffix(tagName, "__") {
|
|
obj.Properties().Add(tagName).Set(data)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
mtData := node.Matrix
|
|
|
|
matrix := NewMatrix4()
|
|
matrix.SetRow(0, Vector{float64(mtData[0]), float64(mtData[1]), float64(mtData[2]), float64(mtData[3])})
|
|
matrix.SetRow(1, Vector{float64(mtData[4]), float64(mtData[5]), float64(mtData[6]), float64(mtData[7])})
|
|
matrix.SetRow(2, Vector{float64(mtData[8]), float64(mtData[9]), float64(mtData[10]), float64(mtData[11])})
|
|
matrix.SetRow(3, Vector{float64(mtData[12]), float64(mtData[13]), float64(mtData[14]), float64(mtData[15])})
|
|
|
|
if !matrix.IsIdentity() {
|
|
|
|
p, s, r := matrix.Decompose()
|
|
|
|
obj.SetLocalPositionVec(p)
|
|
obj.SetLocalScaleVec(s)
|
|
obj.SetLocalRotation(r)
|
|
|
|
} else {
|
|
|
|
obj.SetLocalPositionVec(Vector{float64(node.Translation[0]), float64(node.Translation[1]), float64(node.Translation[2]), 0})
|
|
obj.SetLocalScaleVec(Vector{float64(node.Scale[0]), float64(node.Scale[1]), float64(node.Scale[2]), 0})
|
|
obj.SetLocalRotation(NewQuaternion(float64(node.Rotation[0]), float64(node.Rotation[1]), float64(node.Rotation[2]), float64(node.Rotation[3])).ToMatrix4())
|
|
|
|
}
|
|
|
|
// if value := nodeGetProp(node, "t3dSector__"); value != nil && value.(float64) > 0 {
|
|
// sec := NewSector(obj.(*Model))
|
|
// sec.SectorDetectionType = SectorDetectionType(sectorDetection)
|
|
// obj.(*Model).sector = sec
|
|
// }
|
|
|
|
if value := nodeGetProp(node, "t3dSectorType__"); value != nil {
|
|
obj.SetSectorType(SectorType(value.(float64)))
|
|
switch value.(float64) {
|
|
// case 0: // SectorTypeObject
|
|
case 1: // SectorTypeSector
|
|
sec := NewSector(obj.(*Model))
|
|
sec.SectorDetectionType = SectorDetectionType(sectorDetection)
|
|
obj.(*Model).sector = sec
|
|
// case 2: // SectorTypeStandalone
|
|
}
|
|
}
|
|
|
|
objects = append(objects, obj)
|
|
|
|
}
|
|
|
|
// We do this again here so we can be sure that all of the nodes can be created first
|
|
for i, node := range doc.Nodes {
|
|
|
|
// Set up skin for skinning animations
|
|
if node.Skin != nil {
|
|
|
|
model := objects[i].(*Model)
|
|
|
|
skin := doc.Skins[*node.Skin]
|
|
|
|
// Unsure of if this is necessary.
|
|
// if skin.Skeleton != nil {
|
|
// skeletonRoot := objects[*skin.Skeleton]
|
|
// model.SetWorldPositionVec(skeletonRoot.WorldPosition())
|
|
// model.SetWorldScale(skeletonRoot.WorldScale())
|
|
// model.SetWorldRotation(skeletonRoot.WorldRotation())
|
|
// }
|
|
|
|
model.skinned = true
|
|
|
|
// We should keep a local slice of bones because we can't simply loop through all bones with the matrix index, as
|
|
// the matrix index resets at 0 for each armature, naturally.
|
|
localBones := []*Node{}
|
|
|
|
for _, b := range skin.Joints {
|
|
bone := objects[b].(*Node)
|
|
// This is incorrect, but it gives us a link to any bone in the armature to establish
|
|
// the true root after parenting is set below
|
|
model.SkinRoot = bone
|
|
localBones = append(localBones, bone)
|
|
}
|
|
|
|
matrices, err := modeler.ReadAccessor(doc, doc.Accessors[*skin.InverseBindMatrices], nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for matIndex, matrix := range matrices.([][4][4]float32) {
|
|
|
|
newMat := NewMatrix4()
|
|
for rowIndex, row := range matrix {
|
|
newMat.SetColumn(rowIndex, Vector{float64(row[0]), float64(row[1]), float64(row[2]), float64(row[3])})
|
|
}
|
|
|
|
localBones[matIndex].inverseBindMatrix = newMat
|
|
localBones[matIndex].isBone = true
|
|
|
|
}
|
|
|
|
for vertIndex, boneIndices := range model.Mesh.VertexBones {
|
|
model.bones = append(model.bones, []*Node{})
|
|
|
|
for _, boneID := range boneIndices {
|
|
model.bones[vertIndex] = append(model.bones[vertIndex], localBones[boneID])
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// Set up parenting
|
|
for _, childIndex := range node.Children {
|
|
objects[i].AddChildren(objects[int(childIndex)])
|
|
}
|
|
|
|
}
|
|
|
|
findNode := func(objName string) INode {
|
|
for _, obj := range objects {
|
|
if obj.Name() == objName {
|
|
return obj
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// At this point, parenting should be set up.
|
|
for obj, node := range objToNode {
|
|
|
|
if node.Extras != nil {
|
|
if dataMap, isMap := node.Extras.(map[string]interface{}); isMap {
|
|
|
|
if gameProps, exists := dataMap["t3dGameProperties__"]; exists {
|
|
|
|
for _, p := range gameProps.([]interface{}) {
|
|
|
|
name, value := handleGameProperties(p)
|
|
|
|
obj.Properties().Add(name).Set(value)
|
|
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
// Set up SkinRoot for skinned Models; this should be the root node of a hierarchy of bone Nodes.
|
|
for _, n := range objects {
|
|
|
|
if model, isModel := n.(*Model); isModel && model.skinned {
|
|
|
|
parent := model.SkinRoot
|
|
|
|
for parent.IsBone() && parent.Parent() != nil {
|
|
parent = parent.Parent()
|
|
}
|
|
|
|
model.SkinRoot = parent
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for obj, node := range objToNode {
|
|
|
|
if node.Extras != nil {
|
|
if dataMap, isMap := node.Extras.(map[string]interface{}); isMap {
|
|
|
|
if c, exists := dataMap["t3dInstanceCollection__"]; exists {
|
|
|
|
n := obj.(*Node)
|
|
n.collectionObjects = []INode{}
|
|
|
|
collection := collections[c.(string)]
|
|
|
|
offset := Vector{-collection.Offset[0], -collection.Offset[2], collection.Offset[1], 0}
|
|
|
|
for _, cloneName := range collection.Objects {
|
|
|
|
var clone INode
|
|
|
|
path := collection.Path
|
|
|
|
if path == "" {
|
|
clone = findNode(cloneName).Clone()
|
|
} else {
|
|
path = convertBlenderPath(path)
|
|
if gltfLoadOptions.DependentLibraryResolver == nil {
|
|
log.Printf("Warning: No dependent library resolver defined to resolve dependent library %s for object %s.\n", path, cloneName)
|
|
} else {
|
|
|
|
if library := gltfLoadOptions.DependentLibraryResolver(path); library != nil {
|
|
if foundNode := library.FindNode(cloneName); foundNode != nil {
|
|
clone = foundNode.Clone()
|
|
} else {
|
|
panic("Error in instantiating linked element: " + cloneName + " as there is no such object in the returned library.")
|
|
}
|
|
} else {
|
|
log.Printf("Warning: No library returned in resolving dependent library %s for object %s.\n", path, cloneName)
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if clone != nil {
|
|
|
|
clone.MoveVec(offset)
|
|
obj.AddChildren(clone)
|
|
|
|
n.collectionObjects = append(n.collectionObjects, clone)
|
|
|
|
// Share properties to top-level clones
|
|
for k, v := range obj.Properties() {
|
|
clone.Properties().Add(k).Set(v.Value)
|
|
}
|
|
|
|
} else {
|
|
log.Println("Error in instantiating linked element:", cloneName, "from:", path, "; did you pass the Library as a dependent Library in the GLTFLoadOptions struct?")
|
|
}
|
|
|
|
}
|
|
|
|
if c, exists := dataMap["t3dSectorTypeOverride__"]; exists && c.(float64) > 0 {
|
|
n.SearchTree().ForEach(func(node INode) bool {
|
|
node.SetSectorType(obj.SectorType())
|
|
return true
|
|
})
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
// Set up worlds
|
|
if len(doc.Scenes) > 0 && doc.Scenes[0].Extras != nil {
|
|
|
|
dataMap := doc.Scenes[0].Extras.(map[string]interface{})
|
|
|
|
if wd, exists := dataMap["t3dWorlds__"]; exists {
|
|
|
|
worldData := wd.(map[string]interface{})
|
|
|
|
for worldName, p := range worldData {
|
|
|
|
world := NewWorld(worldName)
|
|
|
|
props := p.(map[string]interface{})
|
|
|
|
if wc, exists := props["ambient color"]; exists {
|
|
wcc := wc.([]interface{})
|
|
worldColor := NewColor(float32(wcc[0].(float64)), float32(wcc[1].(float64)), float32(wcc[2].(float64)), 1).ConvertTosRGB()
|
|
world.AmbientLight.color = worldColor
|
|
}
|
|
|
|
if wc, exists := props["ambient energy"]; exists {
|
|
world.AmbientLight.energy = float32(wc.(float64))
|
|
}
|
|
|
|
if cc, exists := props["clear color"]; exists {
|
|
wcc := cc.([]interface{})
|
|
clearColor := NewColor(float32(wcc[0].(float64)), float32(wcc[1].(float64)), float32(wcc[2].(float64)), float32(wcc[3].(float64))).ConvertTosRGB()
|
|
world.ClearColor = clearColor
|
|
}
|
|
|
|
if v, exists := props["fog mode"]; exists {
|
|
world.FogOn = true
|
|
fm := v.(string)
|
|
switch fm {
|
|
case "OFF":
|
|
world.FogOn = false
|
|
case "ADDITIVE":
|
|
world.FogMode = FogAdd
|
|
case "SUBTRACT":
|
|
world.FogMode = FogSub
|
|
case "OVERWRITE":
|
|
world.FogMode = FogOverwrite
|
|
case "TRANSPARENT":
|
|
world.FogMode = FogTransparent
|
|
}
|
|
}
|
|
|
|
if v, exists := props["fog curve"]; exists {
|
|
fc := v.(string)
|
|
switch fc {
|
|
case "LINEAR":
|
|
world.FogCurve = FogCurveLinear
|
|
case "OUTCIRC":
|
|
world.FogCurve = FogCurveOutCirc
|
|
case "INCIRC":
|
|
world.FogCurve = FogCurveInCirc
|
|
}
|
|
}
|
|
|
|
if v, exists := props["dithered transparency"]; exists {
|
|
world.DitheredFogSize = float32(v.(float64))
|
|
}
|
|
|
|
if v, exists := props["fog color"]; exists {
|
|
wcc := v.([]interface{})
|
|
fogColor := NewColor(float32(wcc[0].(float64)), float32(wcc[1].(float64)), float32(wcc[2].(float64)), float32(wcc[3].(float64))).ConvertTosRGB()
|
|
world.FogColor = fogColor
|
|
}
|
|
|
|
if v, exists := props["fog range start"]; exists {
|
|
fogStart := v.(float64)
|
|
world.FogRange[0] = float32(fogStart)
|
|
}
|
|
|
|
if v, exists := props["fog range end"]; exists {
|
|
fogEnd := v.(float64)
|
|
world.FogRange[1] = float32(fogEnd)
|
|
}
|
|
|
|
library.Worlds[world.Name] = world
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// Set up scene roots
|
|
|
|
for _, s := range doc.Scenes {
|
|
|
|
scene := library.AddScene(s.Name)
|
|
|
|
// Parent all parentless objects to the scene root to be visible.
|
|
for _, n := range s.Nodes {
|
|
scene.Root.AddChildren(objects[n])
|
|
}
|
|
|
|
if s.Extras != nil {
|
|
extras := s.Extras.(map[string]interface{})
|
|
if wn, exists := extras["t3dCurrentWorld__"]; exists {
|
|
scene.World = library.Worlds[wn.(string)]
|
|
}
|
|
|
|
if gameProps, exists := extras["t3dGameProperties__"]; exists {
|
|
|
|
for _, p := range gameProps.([]interface{}) {
|
|
|
|
name, value := handleGameProperties(p)
|
|
|
|
scene.Properties().Add(name).Set(value)
|
|
|
|
}
|
|
}
|
|
|
|
// Non-Tetra3D custom data
|
|
for tagName, data := range extras {
|
|
if !strings.HasPrefix(tagName, "t3d") || !strings.HasSuffix(tagName, "__") {
|
|
scene.Properties().Add(tagName).Set(data)
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
/*
|
|
Below, we handle collection instances specially, as objects in the collection should be instantiated
|
|
instead in the same relative locations, rather than parenting them to their collection instance objects.
|
|
Otherwise, there would be nodes in the middle - the collection instance. For example, a collection composed
|
|
of a chair mesh parented to a collision box would become a hierarchy like this when placed as a collection
|
|
instance in a Blender scene:
|
|
|
|
* Root
|
|
|- * Chair (Collection Instance, named, say, "Chair.001")
|
|
|- * Chair Collision Box (named "ChairCol")
|
|
|- * Chair Mesh
|
|
|
|
Instead, we can substitute the chair collision box in the collection instance's location:
|
|
|
|
* Root
|
|
|- * Chair Collision Box (named "ChairCol", or optionally, after the collection instance object, "Chair.001")
|
|
|- * Chair Mesh
|
|
|
|
*/
|
|
|
|
for _, n := range scene.Root.SearchTree().INodes() {
|
|
|
|
n.setOriginalTransform()
|
|
|
|
if node, ok := n.(*Node); ok && len(node.collectionObjects) > 0 {
|
|
|
|
for _, child := range node.collectionObjects {
|
|
transform := child.Transform()
|
|
node.parent.AddChildren(child)
|
|
if value, exists := globalExporterSettings["t3dRenameInstancedObjects__"]; exists && value.(bool) {
|
|
child.SetName(node.name)
|
|
}
|
|
child.SetWorldTransform(transform)
|
|
}
|
|
|
|
// TODO: For now, this code doesn't do anything because objects parented to instance collections
|
|
// in Blender aren't exported (for whatever reason).
|
|
for _, child := range node.children {
|
|
node.collectionObjects[0].AddChildren(child)
|
|
}
|
|
|
|
node.Unparent()
|
|
|
|
}
|
|
|
|
models := scene.Root.SearchTree().Models()
|
|
if model, ok := n.(*Model); ok && model.sector != nil {
|
|
model.sector.UpdateNeighbors(models...)
|
|
}
|
|
|
|
}
|
|
|
|
for _, cam := range exportedCameras {
|
|
scene.View3DCameras = append(scene.View3DCameras, cam.Clone().(*Camera))
|
|
}
|
|
|
|
}
|
|
|
|
// Cameras exported through GLTF become nodes + a camera child with the correct orientation for some reason???
|
|
// So here we basically cut the empty nodes out of the equation, leaving just the cameras with the correct orientation.
|
|
|
|
// EDIT: This is no longer done, as the camera direction in Blender and the camera direction in GLTF aren't the same, whoops.
|
|
// See: https://github.com/KhronosGroup/glTF-Blender-Exporter/issues/113
|
|
// Cutting out the inserted correction Node breaks relative transforms (i.e. camera parented to another object for positioning).
|
|
|
|
// for _, n := range objects {
|
|
|
|
// if camera, isCamera := n.(*Camera); isCamera {
|
|
// oldParent := camera.Parent()
|
|
// root := oldParent.Parent()
|
|
|
|
// camera.name = oldParent.Name()
|
|
|
|
// for _, child := range oldParent.Children() {
|
|
// if child == camera {
|
|
// continue
|
|
// }
|
|
// camera.AddChildren(child)
|
|
// }
|
|
|
|
// root.RemoveChildren(camera.parent)
|
|
// root.AddChildren(camera)
|
|
// }
|
|
|
|
// }
|
|
|
|
library.ExportedScene = library.Scenes[*doc.Scene]
|
|
|
|
return library, nil
|
|
|
|
}
|
|
|
|
func handleGameProperties(p interface{}) (string, interface{}) {
|
|
|
|
getOrDefaultInt := func(propMap map[string]interface{}, key string, defaultValue int) int {
|
|
if value, keyExists := propMap[key]; keyExists {
|
|
return int(value.(float64))
|
|
}
|
|
return defaultValue
|
|
}
|
|
|
|
getOrDefaultString := func(propMap map[string]interface{}, key string, defaultValue string) string {
|
|
if value, keyExists := propMap[key]; keyExists {
|
|
return value.(string)
|
|
}
|
|
return defaultValue
|
|
}
|
|
|
|
getOrDefaultFloat := func(propMap map[string]interface{}, key string, defaultValue float64) float64 {
|
|
if value, keyExists := propMap[key]; keyExists {
|
|
return value.(float64)
|
|
}
|
|
return defaultValue
|
|
}
|
|
|
|
getOrDefaultBool := func(propMap map[string]interface{}, key string, defaultValue bool) bool {
|
|
if value, keyExists := propMap[key]; keyExists {
|
|
return value.(float64) > 0
|
|
}
|
|
return defaultValue
|
|
}
|
|
|
|
getIfExistingMap := func(propMap map[string]interface{}, key string) map[string]interface{} {
|
|
if value, keyExists := propMap[key]; keyExists && value != nil {
|
|
return value.(map[string]interface{})
|
|
}
|
|
return nil
|
|
}
|
|
|
|
getOrDefaultFloatArray := func(propMap map[string]interface{}, key string, defaultValue []float64) []float64 {
|
|
if value, keyExists := propMap[key]; keyExists {
|
|
values := make([]float64, 0, len(value.([]interface{})))
|
|
for _, v := range value.([]interface{}) {
|
|
values = append(values, v.(float64))
|
|
}
|
|
return values
|
|
}
|
|
return defaultValue
|
|
}
|
|
|
|
property := p.(map[string]interface{})
|
|
|
|
propType := getOrDefaultInt(property, "valueType", 0)
|
|
|
|
// Property types:
|
|
|
|
// bool, int, float, string, reference (string)
|
|
|
|
name := getOrDefaultString(property, "name", "New Property")
|
|
var value interface{}
|
|
|
|
if propType == 0 {
|
|
value = getOrDefaultBool(property, "valueBool", false)
|
|
} else if propType == 1 {
|
|
value = getOrDefaultInt(property, "valueInt", 0)
|
|
} else if propType == 2 {
|
|
value = getOrDefaultFloat(property, "valueFloat", 0)
|
|
} else if propType == 3 {
|
|
value = getOrDefaultString(property, "valueString", "")
|
|
} else if propType == 4 {
|
|
scene := ""
|
|
// Can be nil if it was set to something and then set to nothing
|
|
if ref := getIfExistingMap(property, "valueReferenceScene"); ref != nil {
|
|
scene = getOrDefaultString(ref, "name", "")
|
|
}
|
|
if ref := getIfExistingMap(property, "valueReference"); ref != nil {
|
|
value = scene + ":" + getOrDefaultString(ref, "name", "")
|
|
}
|
|
} else if propType == 5 {
|
|
colorValues := getOrDefaultFloatArray(property, "valueColor", []float64{1, 1, 1, 1})
|
|
color := NewColor(float32(colorValues[0]), float32(colorValues[1]), float32(colorValues[2]), float32(colorValues[3])).ConvertTosRGB()
|
|
value = color
|
|
} else if propType == 6 {
|
|
vecValues := getOrDefaultFloatArray(property, "valueVector3D", []float64{0, 0, 0})
|
|
value = Vector{vecValues[0], vecValues[2], -vecValues[1], 0}
|
|
} else if propType == 7 {
|
|
value = getOrDefaultString(property, "valueFilepath", "")
|
|
if value != "" {
|
|
value = convertBlenderPath(value.(string))
|
|
}
|
|
} else if propType == 8 {
|
|
value = getOrDefaultString(property, "valueDirpath", "")
|
|
if value != "" {
|
|
value = convertBlenderPath(value.(string))
|
|
}
|
|
}
|
|
|
|
return name, value
|
|
|
|
}
|
|
|
|
func convertBlenderPath(path string) string {
|
|
path = strings.ReplaceAll(path, "//", "") // Blender relative paths have double-slashes; we don't need them to
|
|
return path
|
|
}
|