2338 lines
59 KiB
C#
2338 lines
59 KiB
C#
|
using System;
|
||
|
using System.Collections.Generic;
|
||
|
using System.IO;
|
||
|
using GLTF.Schema;
|
||
|
using UnityEngine;
|
||
|
using UnityGLTF.Extensions;
|
||
|
using UnityEditor;
|
||
|
using System.Text.RegularExpressions;
|
||
|
|
||
|
namespace UnityGLTF
|
||
|
{
|
||
|
public class GLTFEditorExporter
|
||
|
{
|
||
|
private Transform[] _rootTransforms;
|
||
|
private GLTFRoot _root;
|
||
|
private BufferId _bufferId;
|
||
|
private GLTF.Schema.Buffer _buffer;
|
||
|
private BinaryWriter _bufferWriter;
|
||
|
private List<Texture2D> _images;
|
||
|
private List<UnityEngine.Texture> _textures;
|
||
|
private List<UnityEngine.Material> _materials;
|
||
|
private Dictionary<int, int> _exportedTransforms;
|
||
|
private List<Transform> _animatedNodes;
|
||
|
private List<Transform> _skinnedNodes;
|
||
|
private Dictionary<SkinnedMeshRenderer, UnityEngine.Mesh> _bakedMeshes;
|
||
|
|
||
|
// Temporary setting to avoid validation issue 'not multiple of 4' in bufferView.offset
|
||
|
private bool _forceIndicesUint = true;
|
||
|
|
||
|
private bool _exportAnimation = true;
|
||
|
private bool _bakeSkinnedMeshes = false;
|
||
|
private Dictionary<string, string> _exportedFiles;
|
||
|
|
||
|
protected struct PrimKey
|
||
|
{
|
||
|
public UnityEngine.Mesh Mesh;
|
||
|
public UnityEngine.Material Material;
|
||
|
}
|
||
|
private readonly Dictionary<PrimKey, MeshId> _primOwner = new Dictionary<PrimKey, MeshId>();
|
||
|
private readonly Dictionary<UnityEngine.Mesh, MeshPrimitive[]> _meshToPrims = new Dictionary<UnityEngine.Mesh, MeshPrimitive[]>();
|
||
|
|
||
|
private GLTFTextureUtilsCache _textureCache;
|
||
|
public bool ExportNames = true;
|
||
|
|
||
|
public enum MaterialDoubleSidedExport {
|
||
|
Default,
|
||
|
FrontSide,
|
||
|
DoubleSide
|
||
|
}
|
||
|
|
||
|
public static MaterialDoubleSidedExport materialExportOption = MaterialDoubleSidedExport.Default;
|
||
|
|
||
|
// Progress
|
||
|
public enum EXPORT_STEP
|
||
|
{
|
||
|
NODES,
|
||
|
ANIMATIONS,
|
||
|
SKINNING,
|
||
|
IMAGES
|
||
|
}
|
||
|
public delegate void ProgressCallback(EXPORT_STEP step, float current, float total);
|
||
|
public delegate void FinishCallback();
|
||
|
ProgressCallback _progressCallback;
|
||
|
FinishCallback _finishCallback;
|
||
|
|
||
|
/// <summary>
|
||
|
/// Create a GLTFExporter that exports out a transform
|
||
|
/// </summary>
|
||
|
public GLTFEditorExporter()
|
||
|
{
|
||
|
initializeStructures();
|
||
|
}
|
||
|
|
||
|
public void setProgressCallback(ProgressCallback cb)
|
||
|
{
|
||
|
_progressCallback = cb;
|
||
|
}
|
||
|
|
||
|
private void updateProgress(EXPORT_STEP step, float current, float total)
|
||
|
{
|
||
|
if(_progressCallback != null)
|
||
|
{
|
||
|
_progressCallback(step, current, total);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public void setExportFinishCallback(FinishCallback cb)
|
||
|
{
|
||
|
_finishCallback = cb;
|
||
|
}
|
||
|
|
||
|
public void triggerFinishCallback()
|
||
|
{
|
||
|
if (_finishCallback != null)
|
||
|
_finishCallback();
|
||
|
|
||
|
}
|
||
|
/// <summary>
|
||
|
/// Create a GLTFExporter that exports out a transform
|
||
|
/// </summary>
|
||
|
public GLTFEditorExporter(string generator)
|
||
|
{
|
||
|
initializeStructures(generator);
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Create a GLTFExporter that exports out a transform
|
||
|
/// </summary>
|
||
|
/// <param name="rootTransforms">Root transform of object to export</param>
|
||
|
public GLTFEditorExporter(Transform[] rootTransforms, string generator = "")
|
||
|
{
|
||
|
initializeStructures(generator);
|
||
|
_rootTransforms = rootTransforms;
|
||
|
}
|
||
|
|
||
|
public void setTransforms(Transform[] rootTransforms)
|
||
|
{
|
||
|
_rootTransforms = rootTransforms;
|
||
|
}
|
||
|
|
||
|
public void enableAnimation(bool enable)
|
||
|
{
|
||
|
_exportAnimation = enable;
|
||
|
}
|
||
|
|
||
|
private void initializeStructures(string generator = "")
|
||
|
{
|
||
|
_exportedTransforms = new Dictionary<int, int>();
|
||
|
_animatedNodes = new List<Transform>();
|
||
|
_skinnedNodes = new List<Transform>();
|
||
|
_exportedFiles = new Dictionary<string, string>();
|
||
|
_textureCache = new GLTFTextureUtilsCache();
|
||
|
_bakedMeshes = new Dictionary<SkinnedMeshRenderer, UnityEngine.Mesh>();
|
||
|
|
||
|
_root = new GLTFRoot
|
||
|
{
|
||
|
Accessors = new List<Accessor>(),
|
||
|
Animations = new List<GLTF.Schema.Animation>(),
|
||
|
Asset = new Asset
|
||
|
{
|
||
|
Version = "2.0",
|
||
|
Generator = generator.Length > 0 ? generator : "Plattar UnityGLTF (" + Application.unityVersion + ")"
|
||
|
},
|
||
|
Buffers = new List<GLTF.Schema.Buffer>(),
|
||
|
BufferViews = new List<BufferView>(),
|
||
|
ExtensionsUsed = new List<string>(),
|
||
|
ExtensionsRequired = new List<string>(),
|
||
|
Images = new List<Image>(),
|
||
|
Materials = new List<GLTF.Schema.Material>(),
|
||
|
Meshes = new List<GLTF.Schema.Mesh>(),
|
||
|
Nodes = new List<Node>(),
|
||
|
Samplers = new List<Sampler>(),
|
||
|
Scenes = new List<Scene>(),
|
||
|
Skins = new List<Skin>(),
|
||
|
Textures = new List<GLTF.Schema.Texture>(),
|
||
|
};
|
||
|
|
||
|
_images = new List<Texture2D>();
|
||
|
_materials = new List<UnityEngine.Material>();
|
||
|
_textures = new List<UnityEngine.Texture>();
|
||
|
|
||
|
_buffer = new GLTF.Schema.Buffer();
|
||
|
_bufferId = new BufferId
|
||
|
{
|
||
|
Id = _root.Buffers.Count,
|
||
|
Root = _root
|
||
|
};
|
||
|
_root.Buffers.Add(_buffer);
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Gets the root object of the exported GLTF
|
||
|
/// </summary>
|
||
|
/// <returns>Root parsed GLTF Json</returns>
|
||
|
public GLTFRoot GetRoot() {
|
||
|
return _root;
|
||
|
}
|
||
|
|
||
|
public Dictionary<string, string> getExportedFilesList()
|
||
|
{
|
||
|
return _exportedFiles;
|
||
|
}
|
||
|
|
||
|
public void clear()
|
||
|
{
|
||
|
initializeStructures();
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Specifies the path and filename for the GLTF Json and binary
|
||
|
/// </summary>
|
||
|
/// <param name="path">File path for saving the GLTF and binary files</param>
|
||
|
/// <param name="fileName">The name of the GLTF file</param>
|
||
|
public void SaveGLTFandBin(string path, string fileName)
|
||
|
{
|
||
|
string binPath = Path.Combine(path, fileName + ".bin");
|
||
|
var binFile = File.Create(binPath);
|
||
|
_bufferWriter = new BinaryWriter(binFile);
|
||
|
|
||
|
_root.Scene = ExportScene(fileName, _rootTransforms);
|
||
|
if (_exportAnimation)
|
||
|
{
|
||
|
exportAnimation();
|
||
|
// Export skins
|
||
|
for (int i = 0; i < _skinnedNodes.Count; ++i)
|
||
|
{
|
||
|
Transform t = _skinnedNodes[i];
|
||
|
exportSkinFromNode(t);
|
||
|
|
||
|
updateProgress(EXPORT_STEP.SKINNING, i, _skinnedNodes.Count);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
_buffer.Uri = fileName + ".bin";
|
||
|
_buffer.ByteLength = (int)_bufferWriter.BaseStream.Length;
|
||
|
|
||
|
_exportedFiles.Add(binPath, "");
|
||
|
|
||
|
string gltfPath = Path.Combine(path, fileName + ".gltf");
|
||
|
var gltfFile = File.CreateText(gltfPath);
|
||
|
_root.Serialize(gltfFile);
|
||
|
|
||
|
#if WINDOWS_UWP
|
||
|
gltfFile.Dispose();
|
||
|
binFile.Dispose();
|
||
|
#else
|
||
|
gltfFile.Close();
|
||
|
binFile.Close();
|
||
|
#endif
|
||
|
_exportedFiles.Add(gltfPath, "");
|
||
|
bool backup = GL.sRGBWrite;
|
||
|
GL.sRGBWrite = true;
|
||
|
foreach (var image in _images)
|
||
|
{
|
||
|
//Should filter regarding channel that use it
|
||
|
string outputPath = Path.Combine(path, GLTFUtils.buildImageName(image)); // Png by default, but will be changed in write function
|
||
|
string finalOutputPath = GLTFTextureUtils.writeTextureOnDisk(_textureCache.flipTexture(image), outputPath, true);
|
||
|
try {
|
||
|
_exportedFiles.Add(finalOutputPath, "");
|
||
|
}
|
||
|
catch (Exception e) {
|
||
|
throw new Exception("The File " + outputPath + " has alredy been used. Ensure your file names are unique.");
|
||
|
}
|
||
|
|
||
|
updateProgress(EXPORT_STEP.IMAGES, _images.IndexOf(image), _images.Count);
|
||
|
}
|
||
|
GL.sRGBWrite = backup;
|
||
|
triggerFinishCallback();
|
||
|
}
|
||
|
|
||
|
private void exportAnimation()
|
||
|
{
|
||
|
for (int i = 0; i < _animatedNodes.Count; ++i)
|
||
|
{
|
||
|
Transform t = _animatedNodes[i];
|
||
|
exportAnimationFromNode(ref t);
|
||
|
|
||
|
updateProgress(EXPORT_STEP.ANIMATIONS, i, _animatedNodes.Count);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private UnityEngine.Mesh getMesh(GameObject gameObject)
|
||
|
{
|
||
|
if(gameObject.GetComponent<MeshFilter>())
|
||
|
{
|
||
|
return gameObject.GetComponent<MeshFilter>().sharedMesh;
|
||
|
}
|
||
|
|
||
|
SkinnedMeshRenderer skinMesh = gameObject.GetComponent<SkinnedMeshRenderer>();
|
||
|
if (skinMesh)
|
||
|
{
|
||
|
if(!_exportAnimation && _bakeSkinnedMeshes)
|
||
|
{
|
||
|
if(!_bakedMeshes.ContainsKey(skinMesh))
|
||
|
{
|
||
|
UnityEngine.Mesh bakedMesh = new UnityEngine.Mesh();
|
||
|
skinMesh.BakeMesh(bakedMesh);
|
||
|
_bakedMeshes.Add(skinMesh, bakedMesh);
|
||
|
}
|
||
|
|
||
|
return _bakedMeshes[skinMesh];
|
||
|
}
|
||
|
|
||
|
return gameObject.GetComponent<SkinnedMeshRenderer>().sharedMesh;
|
||
|
}
|
||
|
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
private UnityEngine.Material getMaterial(GameObject gameObject)
|
||
|
{
|
||
|
if (gameObject.GetComponent<MeshRenderer>())
|
||
|
{
|
||
|
return gameObject.GetComponent<MeshRenderer>().sharedMaterial;
|
||
|
}
|
||
|
|
||
|
if (gameObject.GetComponent<SkinnedMeshRenderer>())
|
||
|
{
|
||
|
return gameObject.GetComponent<SkinnedMeshRenderer>().sharedMaterial;
|
||
|
}
|
||
|
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
private UnityEngine.Material[] getMaterials(GameObject gameObject)
|
||
|
{
|
||
|
if (gameObject.GetComponent<MeshRenderer>())
|
||
|
{
|
||
|
return gameObject.GetComponent<MeshRenderer>().sharedMaterials;
|
||
|
}
|
||
|
|
||
|
if (gameObject.GetComponent<SkinnedMeshRenderer>())
|
||
|
{
|
||
|
return gameObject.GetComponent<SkinnedMeshRenderer>().sharedMaterials;
|
||
|
}
|
||
|
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
private int countNodes(Transform[] trs)
|
||
|
{
|
||
|
int ct = 0;
|
||
|
foreach (Transform tr in trs)
|
||
|
{
|
||
|
countRecursive(tr, ref ct);
|
||
|
}
|
||
|
|
||
|
return ct;
|
||
|
}
|
||
|
|
||
|
private void countRecursive(Transform tr, ref int count)
|
||
|
{
|
||
|
count += 1;
|
||
|
if (tr.childCount > 0)
|
||
|
{
|
||
|
for (int i = 0; i < tr.childCount; ++i)
|
||
|
{
|
||
|
countRecursive(tr.GetChild(i), ref count);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private SceneId ExportScene(string name, Transform[] rootObjTransforms)
|
||
|
{
|
||
|
var scene = new Scene();
|
||
|
|
||
|
if (ExportNames)
|
||
|
{
|
||
|
scene.Name = name;
|
||
|
}
|
||
|
int nodeCount = countNodes(rootObjTransforms);
|
||
|
scene.Nodes = new List<NodeId>(rootObjTransforms.Length);
|
||
|
foreach (var transform in rootObjTransforms)
|
||
|
{
|
||
|
scene.Nodes.Add(ExportNode(transform, nodeCount));
|
||
|
}
|
||
|
|
||
|
_root.Scenes.Add(scene);
|
||
|
|
||
|
return new SceneId {
|
||
|
Id = _root.Scenes.Count - 1,
|
||
|
Root = _root
|
||
|
};
|
||
|
}
|
||
|
|
||
|
private NodeId ExportNode(Transform nodeTransform, int nodeCount)
|
||
|
{
|
||
|
var node = new Node();
|
||
|
|
||
|
if (ExportNames)
|
||
|
{
|
||
|
node.Name = nodeTransform.name;
|
||
|
}
|
||
|
if(nodeTransform.GetComponent<UnityEngine.Animation>() || nodeTransform.GetComponent<UnityEngine.Animator>())
|
||
|
{
|
||
|
_animatedNodes.Add(nodeTransform);
|
||
|
}
|
||
|
if(nodeTransform.GetComponent<SkinnedMeshRenderer>())
|
||
|
{
|
||
|
_skinnedNodes.Add(nodeTransform);
|
||
|
}
|
||
|
// If object is on top of the selection, use global transform
|
||
|
bool useLocal = !Array.Exists(_rootTransforms, element => element == nodeTransform);
|
||
|
node.SetUnityTransform(nodeTransform, useLocal);
|
||
|
|
||
|
var id = new NodeId {
|
||
|
Id = _root.Nodes.Count,
|
||
|
Root = _root
|
||
|
};
|
||
|
|
||
|
// Register nodes for animation parsing (could be disabled is animation is disables)
|
||
|
_exportedTransforms.Add(nodeTransform.GetInstanceID(), _root.Nodes.Count);
|
||
|
|
||
|
_root.Nodes.Add(node);
|
||
|
// Update progress
|
||
|
updateProgress(EXPORT_STEP.NODES, _root.Nodes.Count, nodeCount);
|
||
|
|
||
|
// children that are primitives get put in a mesh
|
||
|
GameObject[] primitives, nonPrimitives;
|
||
|
FilterPrimitives(nodeTransform, out primitives, out nonPrimitives);
|
||
|
if (primitives.Length > 0)
|
||
|
{
|
||
|
node.Mesh = ExportMesh(nodeTransform.name, primitives);
|
||
|
|
||
|
// associate unity meshes with gltf mesh id
|
||
|
foreach (var prim in primitives)
|
||
|
{
|
||
|
_primOwner[new PrimKey { Mesh = getMesh(prim), Material = getMaterial(prim) }] = node.Mesh;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// children that are not primitives get added as child nodes
|
||
|
if (nonPrimitives.Length > 0)
|
||
|
{
|
||
|
node.Children = new List<NodeId>(nonPrimitives.Length);
|
||
|
foreach(var child in nonPrimitives)
|
||
|
{
|
||
|
node.Children.Add(ExportNode(child.transform, nodeCount));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return id;
|
||
|
}
|
||
|
|
||
|
private void FilterPrimitives(Transform transform, out GameObject[] primitives, out GameObject[] nonPrimitives)
|
||
|
{
|
||
|
var childCount = transform.childCount;
|
||
|
var prims = new List<GameObject>(childCount+1);
|
||
|
var nonPrims = new List<GameObject>(childCount);
|
||
|
|
||
|
// add another primitive if the root object also has a mesh
|
||
|
if (GLTFUtils.isValidMeshObject(transform.gameObject))
|
||
|
{
|
||
|
prims.Add(transform.gameObject);
|
||
|
}
|
||
|
|
||
|
for (var i = 0; i < childCount; i++)
|
||
|
{
|
||
|
var go = transform.GetChild(i).gameObject;
|
||
|
nonPrims.Add(go);
|
||
|
}
|
||
|
|
||
|
primitives = prims.ToArray();
|
||
|
nonPrimitives = nonPrims.ToArray();
|
||
|
}
|
||
|
|
||
|
private static bool IsPrimitive(GameObject gameObject)
|
||
|
{
|
||
|
/*
|
||
|
* Primitives have the following properties:
|
||
|
* - have no children
|
||
|
* - have no non-default local transform properties
|
||
|
* - have MeshFilter and MeshRenderer components
|
||
|
*/
|
||
|
return gameObject.transform.childCount == 0
|
||
|
&& gameObject.transform.localPosition == Vector3.zero
|
||
|
&& gameObject.transform.localRotation == Quaternion.identity
|
||
|
&& gameObject.transform.localScale == Vector3.one
|
||
|
&& GLTFUtils.isValidMeshObject(gameObject);
|
||
|
}
|
||
|
|
||
|
private MeshId ExportMesh(string name, GameObject[] primitives)
|
||
|
{
|
||
|
// check if this set of primitives is already a mesh
|
||
|
MeshId existingMeshId = null;
|
||
|
var key = new PrimKey();
|
||
|
foreach (var prim in primitives)
|
||
|
{
|
||
|
key.Mesh = getMesh(prim);
|
||
|
key.Material = getMaterial(prim);
|
||
|
|
||
|
MeshId tempMeshId;
|
||
|
if (_primOwner.TryGetValue(key, out tempMeshId) && (existingMeshId == null || tempMeshId == existingMeshId))
|
||
|
{
|
||
|
existingMeshId = tempMeshId;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
existingMeshId = null;
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// if so, return that mesh id
|
||
|
if(existingMeshId != null)
|
||
|
return existingMeshId;
|
||
|
|
||
|
// if not, create new mesh and return its id
|
||
|
var mesh = new GLTF.Schema.Mesh();
|
||
|
|
||
|
if (ExportNames)
|
||
|
{
|
||
|
mesh.Name = name;
|
||
|
}
|
||
|
|
||
|
mesh.Primitives = new List<MeshPrimitive>(primitives.Length);
|
||
|
foreach (var prim in primitives)
|
||
|
{
|
||
|
mesh.Primitives.AddRange(ExportPrimitive(prim));
|
||
|
}
|
||
|
|
||
|
var id = new MeshId
|
||
|
{
|
||
|
Id = _root.Meshes.Count,
|
||
|
Root = _root
|
||
|
};
|
||
|
_root.Meshes.Add(mesh);
|
||
|
|
||
|
return id;
|
||
|
}
|
||
|
|
||
|
// a mesh *might* decode to multiple prims if there are submeshes
|
||
|
private MeshPrimitive[] ExportPrimitive(GameObject gameObject)
|
||
|
{
|
||
|
var meshObj = getMesh(gameObject);
|
||
|
|
||
|
var materialsObj = getMaterials(gameObject);
|
||
|
|
||
|
var prims = new MeshPrimitive[meshObj.subMeshCount];
|
||
|
|
||
|
// don't export any more accessors if this mesh is already exported
|
||
|
MeshPrimitive[] primVariations;
|
||
|
if (_meshToPrims.TryGetValue(meshObj, out primVariations)
|
||
|
&& meshObj.subMeshCount == primVariations.Length)
|
||
|
{
|
||
|
for (var i = 0; i < primVariations.Length; i++)
|
||
|
{
|
||
|
prims[i] = primVariations[i].Clone();
|
||
|
if(materialsObj[i])
|
||
|
prims[i].Material = ExportMaterial(materialsObj[i]);
|
||
|
}
|
||
|
|
||
|
return prims;
|
||
|
}
|
||
|
|
||
|
AccessorId aPosition = null, aNormal = null, aTangent = null,
|
||
|
aTexcoord0 = null, aTexcoord1 = null, aColor0 = null;
|
||
|
|
||
|
aPosition = ExportVerticesAccessor(meshObj.vertices, gameObject, true);
|
||
|
|
||
|
if (meshObj.normals.Length != 0)
|
||
|
aNormal = ExportAccessor(meshObj.normals, true);
|
||
|
|
||
|
if (meshObj.tangents.Length != 0)
|
||
|
aTangent = ExportAccessor(meshObj.tangents, true);
|
||
|
|
||
|
if (meshObj.uv.Length != 0)
|
||
|
aTexcoord0 = ExportAccessor(meshObj.uv);
|
||
|
|
||
|
if (meshObj.uv2.Length != 0)
|
||
|
aTexcoord1 = ExportAccessor(meshObj.uv2);
|
||
|
|
||
|
if (meshObj.colors.Length != 0)
|
||
|
aColor0 = ExportAccessor(meshObj.colors);
|
||
|
|
||
|
MaterialId lastMaterialId = null;
|
||
|
|
||
|
for (var submesh = 0; submesh < meshObj.subMeshCount; submesh++)
|
||
|
{
|
||
|
var primitive = new MeshPrimitive();
|
||
|
|
||
|
var triangles = meshObj.GetTriangles(submesh);
|
||
|
primitive.Indices = ExportAccessor(FlipFaces(triangles), true);
|
||
|
|
||
|
primitive.Attributes = new Dictionary<string, AccessorId>();
|
||
|
primitive.Attributes.Add(SemanticProperties.POSITION, aPosition);
|
||
|
|
||
|
if (aNormal != null)
|
||
|
primitive.Attributes.Add(SemanticProperties.NORMAL, aNormal);
|
||
|
if (aTangent != null)
|
||
|
primitive.Attributes.Add(SemanticProperties.TANGENT, aTangent);
|
||
|
if (aTexcoord0 != null)
|
||
|
primitive.Attributes.Add(SemanticProperties.TexCoord(0), aTexcoord0);
|
||
|
if (aTexcoord1 != null)
|
||
|
primitive.Attributes.Add(SemanticProperties.TexCoord(1), aTexcoord1);
|
||
|
if (aColor0 != null)
|
||
|
primitive.Attributes.Add(SemanticProperties.Color(0), aColor0);
|
||
|
|
||
|
if (submesh < materialsObj.Length && materialsObj[submesh])
|
||
|
{
|
||
|
primitive.Material = ExportMaterial(materialsObj[submesh]);
|
||
|
lastMaterialId = primitive.Material;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
primitive.Material = lastMaterialId;
|
||
|
}
|
||
|
|
||
|
prims[submesh] = primitive;
|
||
|
}
|
||
|
|
||
|
_meshToPrims[meshObj] = prims;
|
||
|
|
||
|
return prims;
|
||
|
}
|
||
|
|
||
|
private MaterialId ExportMaterial(UnityEngine.Material materialObj)
|
||
|
{
|
||
|
MaterialId id = GetMaterialId(_root, materialObj);
|
||
|
if (id != null)
|
||
|
{
|
||
|
return id;
|
||
|
}
|
||
|
|
||
|
var material = new GLTF.Schema.Material();
|
||
|
|
||
|
if (ExportNames)
|
||
|
{
|
||
|
material.Name = materialObj.name;
|
||
|
}
|
||
|
|
||
|
if (materialObj.HasProperty("_Cutoff"))
|
||
|
{
|
||
|
material.AlphaCutoff = materialObj.GetFloat("_Cutoff");
|
||
|
}
|
||
|
|
||
|
switch (materialObj.GetTag("RenderType", false, ""))
|
||
|
{
|
||
|
case "TransparentCutout":
|
||
|
material.AlphaMode = AlphaMode.MASK;
|
||
|
break;
|
||
|
case "Transparent":
|
||
|
material.AlphaMode = AlphaMode.BLEND;
|
||
|
break;
|
||
|
default:
|
||
|
material.AlphaMode = AlphaMode.OPAQUE;
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
if (materialExportOption == MaterialDoubleSidedExport.DoubleSide) {
|
||
|
material.DoubleSided = true;
|
||
|
}
|
||
|
else if (materialExportOption == MaterialDoubleSidedExport.FrontSide) {
|
||
|
material.DoubleSided = false;
|
||
|
}
|
||
|
else {
|
||
|
material.DoubleSided = materialObj.HasProperty("_Cull") &&
|
||
|
materialObj.GetInt("_Cull") == (float)UnityEngine.Rendering.CullMode.Off;
|
||
|
}
|
||
|
|
||
|
if (materialObj.IsKeywordEnabled("_EMISSION") && materialObj.HasProperty("_EmissionColor"))
|
||
|
{
|
||
|
material.EmissiveFactor = materialObj.GetColor("_EmissionColor").ToNumericsColor();
|
||
|
}
|
||
|
|
||
|
if (materialObj.HasProperty("_EmissionMap"))
|
||
|
{
|
||
|
var emissionTex = materialObj.GetTexture("_EmissionMap");
|
||
|
|
||
|
if (emissionTex != null)
|
||
|
{
|
||
|
material.EmissiveTexture = ExportTextureInfo(emissionTex);
|
||
|
|
||
|
ExportTextureTransform(material.EmissiveTexture, materialObj, "_EmissionMap");
|
||
|
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (materialObj.HasProperty("_BumpMap"))
|
||
|
{
|
||
|
var normalTex = materialObj.GetTexture("_BumpMap");
|
||
|
|
||
|
if (normalTex != null)
|
||
|
{
|
||
|
material.NormalTexture = ExportNormalTextureInfo(normalTex, materialObj);
|
||
|
ExportTextureTransform(material.NormalTexture, materialObj, "_BumpMap");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
switch (materialObj.shader.name)
|
||
|
{
|
||
|
case "Standard":
|
||
|
case "Standard (Roughness setup)":
|
||
|
case "GLTF/GLTFStandard":
|
||
|
material.PbrMetallicRoughness = ExportPBRMetallicRoughness(materialObj);
|
||
|
if (materialObj.HasProperty("_OcclusionMap"))
|
||
|
{
|
||
|
var occTex = materialObj.GetTexture("_OcclusionMap");
|
||
|
if (occTex != null)
|
||
|
{
|
||
|
// Pack occlusion with metallicRoughness if any
|
||
|
if (material.PbrMetallicRoughness.MetallicRoughnessTexture != null)
|
||
|
{
|
||
|
var info = new OcclusionTextureInfo();
|
||
|
if (materialObj.HasProperty("_OcclusionStrength"))
|
||
|
{
|
||
|
info.Strength = materialObj.GetFloat("_OcclusionStrength");
|
||
|
}
|
||
|
info.Index = material.PbrMetallicRoughness.MetallicRoughnessTexture.Index;
|
||
|
material.OcclusionTexture = info;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
material.OcclusionTexture = ExportOcclusionTextureInfo(occTex, materialObj);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
break;
|
||
|
case "Standard (Specular setup)":
|
||
|
KHR_materials_pbrSpecularGlossinessExtension pbr = convertSpecular(materialObj);
|
||
|
material.Extensions.Add(KHR_materials_pbrSpecularGlossinessExtensionFactory.EXTENSION_NAME, pbr);
|
||
|
registerExtension(KHR_materials_pbrSpecularGlossinessExtensionFactory.EXTENSION_NAME);
|
||
|
|
||
|
if (materialObj.HasProperty("_OcclusionMap"))
|
||
|
{
|
||
|
var occTex = materialObj.GetTexture("_OcclusionMap");
|
||
|
if (occTex != null)
|
||
|
{
|
||
|
material.OcclusionTexture = ExportOcclusionTextureInfo(occTex, materialObj);
|
||
|
ExportTextureTransform(material.OcclusionTexture, materialObj, "_OcclusionMap");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
break;
|
||
|
case "GLTF/GLTFConstant":
|
||
|
material.CommonConstant = ExportCommonConstant(materialObj);
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
_materials.Add(materialObj);
|
||
|
|
||
|
id = new MaterialId {
|
||
|
Id = _root.Materials.Count,
|
||
|
Root = _root
|
||
|
};
|
||
|
_root.Materials.Add(material);
|
||
|
|
||
|
return id;
|
||
|
}
|
||
|
|
||
|
private void ExportTextureTransform(TextureInfo def, UnityEngine.Material mat, string texName)
|
||
|
{
|
||
|
Vector2 offset = mat.GetTextureOffset(texName);
|
||
|
Vector2 scale = mat.GetTextureScale(texName);
|
||
|
|
||
|
if (offset == Vector2.zero && scale == Vector2.one) return;
|
||
|
|
||
|
if (_root.ExtensionsUsed == null)
|
||
|
{
|
||
|
_root.ExtensionsUsed = new List<string>(
|
||
|
new string[] { ExtTextureTransformExtensionFactory.EXTENSION_NAME }
|
||
|
);
|
||
|
}
|
||
|
else if (!_root.ExtensionsUsed.Contains(ExtTextureTransformExtensionFactory.EXTENSION_NAME))
|
||
|
{
|
||
|
_root.ExtensionsUsed.Add(ExtTextureTransformExtensionFactory.EXTENSION_NAME);
|
||
|
}
|
||
|
|
||
|
if (def.Extensions == null)
|
||
|
def.Extensions = new Dictionary<string, Extension>();
|
||
|
|
||
|
def.Extensions[ExtTextureTransformExtensionFactory.EXTENSION_NAME] = new ExtTextureTransformExtension(
|
||
|
new GLTF.Math.Vector2(offset.x, -offset.y),
|
||
|
new GLTF.Math.Vector2(scale.x, scale.y),
|
||
|
0 // TODO: support UV channels
|
||
|
);
|
||
|
}
|
||
|
|
||
|
private void registerExtension(string extension, bool isRequired=false)
|
||
|
{
|
||
|
if(!_root.ExtensionsUsed.Contains(extension))
|
||
|
{
|
||
|
_root.ExtensionsUsed.Add(extension);
|
||
|
}
|
||
|
|
||
|
if(isRequired && !_root.ExtensionsRequired.Contains(extension))
|
||
|
{
|
||
|
_root.ExtensionsRequired.Add(extension);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private KHR_materials_pbrSpecularGlossinessExtension convertSpecular(UnityEngine.Material mat)
|
||
|
{
|
||
|
GLTF.Math.Color diffuseFactor = mat.GetColor("_Color").ToNumericsColor();
|
||
|
TextureInfo diffuseTexture = mat.GetTexture("_MainTex") != null ? ExportTextureInfo(mat.GetTexture("_MainTex")) : null;
|
||
|
|
||
|
TextureInfo specularGlossinessTexture = null;
|
||
|
GLTF.Math.Color specularColor = Color.white.ToNumericsColor();
|
||
|
float glossinessFactor = 1.0f;
|
||
|
if (mat.GetTexture("_SpecGlossMap"))
|
||
|
{
|
||
|
specularGlossinessTexture = ExportTextureInfo(mat.GetTexture("_SpecGlossMap"));
|
||
|
if(mat.HasProperty("_GlossMapScale"))
|
||
|
glossinessFactor = mat.GetFloat("_GlossMapScale");
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
specularColor = mat.GetColor("_SpecColor").ToNumericsColor();
|
||
|
if (mat.HasProperty("_Glossiness"))
|
||
|
glossinessFactor = mat.GetFloat("_Glossiness");
|
||
|
}
|
||
|
|
||
|
GLTF.Math.Vector3 specularFactor = new GLTF.Math.Vector3(specularColor.R, specularColor.G, specularColor.B);
|
||
|
|
||
|
|
||
|
return new KHR_materials_pbrSpecularGlossinessExtension(diffuseFactor, diffuseTexture, specularFactor, glossinessFactor, specularGlossinessTexture);
|
||
|
}
|
||
|
|
||
|
private NormalTextureInfo ExportNormalTextureInfo(UnityEngine.Texture texture, UnityEngine.Material material)
|
||
|
{
|
||
|
var info = new NormalTextureInfo();
|
||
|
info.Index = ExportTexture(_textureCache.handleNormalMap((Texture2D)texture));
|
||
|
if (material.HasProperty("_BumpScale"))
|
||
|
{
|
||
|
info.Scale = material.GetFloat("_BumpScale");
|
||
|
}
|
||
|
|
||
|
return info;
|
||
|
}
|
||
|
|
||
|
private OcclusionTextureInfo ExportOcclusionTextureInfo(UnityEngine.Texture texture, UnityEngine.Material material)
|
||
|
{
|
||
|
var info = new OcclusionTextureInfo();
|
||
|
|
||
|
info.Index = ExportTexture(texture);
|
||
|
|
||
|
if (material.HasProperty("_OcclusionStrength"))
|
||
|
{
|
||
|
info.Strength = material.GetFloat("_OcclusionStrength");
|
||
|
}
|
||
|
|
||
|
return info;
|
||
|
}
|
||
|
|
||
|
private PbrMetallicRoughness ExportPBRMetallicRoughness(UnityEngine.Material material)
|
||
|
{
|
||
|
var pbr = new PbrMetallicRoughness();
|
||
|
|
||
|
if (material.HasProperty("_Color"))
|
||
|
{
|
||
|
pbr.BaseColorFactor = material.GetColor("_Color").ToNumericsColor();
|
||
|
}
|
||
|
|
||
|
if (material.HasProperty("_MainTex"))
|
||
|
{
|
||
|
var mainTex = material.GetTexture("_MainTex");
|
||
|
|
||
|
if (mainTex != null)
|
||
|
{
|
||
|
pbr.BaseColorTexture = ExportTextureInfo(mainTex);
|
||
|
ExportTextureTransform(pbr.BaseColorTexture, material, "_MainTex");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (material.HasProperty("_Metallic"))
|
||
|
{
|
||
|
pbr.MetallicFactor = material.GetFloat("_Metallic");
|
||
|
}
|
||
|
|
||
|
if (material.HasProperty("_Roughness"))
|
||
|
{
|
||
|
pbr.RoughnessFactor = material.GetFloat("_Roughness");
|
||
|
}
|
||
|
else if (material.HasProperty("_Glossiness"))
|
||
|
{
|
||
|
pbr.RoughnessFactor = 1.0 - material.GetFloat("_Glossiness");
|
||
|
}
|
||
|
|
||
|
if (material.HasProperty("_MetallicRoughnessMap"))
|
||
|
{
|
||
|
var mrTex = material.GetTexture("_MetallicRoughnessMap");
|
||
|
|
||
|
if (mrTex != null)
|
||
|
{
|
||
|
if (material.HasProperty("_GlossMapScale")) {
|
||
|
pbr.RoughnessFactor = material.GetFloat("_GlossMapScale");
|
||
|
}
|
||
|
|
||
|
pbr.MetallicRoughnessTexture = ExportTextureInfo(mrTex);
|
||
|
// writing 1.0 does not export this for whatever reason
|
||
|
pbr.MetallicFactor = 0.99;
|
||
|
ExportTextureTransform(pbr.MetallicRoughnessTexture, material, "_MetallicRoughnessMap");
|
||
|
}
|
||
|
}
|
||
|
else if (material.HasProperty("_MetallicGlossMap"))
|
||
|
{
|
||
|
var mgTex = material.GetTexture("_MetallicGlossMap") as Texture2D;
|
||
|
|
||
|
if (mgTex != null)
|
||
|
{
|
||
|
if (material.HasProperty("_GlossMapScale")) {
|
||
|
pbr.RoughnessFactor = material.GetFloat("_GlossMapScale");
|
||
|
}
|
||
|
|
||
|
var occTex = (material.HasProperty("_OcclusionMap") ? material.GetTexture("_OcclusionMap") as Texture2D : null);
|
||
|
pbr.MetallicRoughnessTexture = ExportTextureInfo(_textureCache.packOcclusionMetalRough(mgTex, occTex));
|
||
|
// writing 1.0 does not export this for whatever reason
|
||
|
pbr.MetallicFactor = 0.99;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return pbr;
|
||
|
}
|
||
|
|
||
|
private MaterialCommonConstant ExportCommonConstant(UnityEngine.Material materialObj)
|
||
|
{
|
||
|
if (_root.ExtensionsUsed == null)
|
||
|
{
|
||
|
_root.ExtensionsUsed = new List<string>(new string[] { "KHR_materials_common" });
|
||
|
}
|
||
|
else if(!_root.ExtensionsUsed.Contains("KHR_materials_common"))
|
||
|
_root.ExtensionsUsed.Add("KHR_materials_common");
|
||
|
|
||
|
var constant = new MaterialCommonConstant();
|
||
|
|
||
|
if (materialObj.HasProperty("_AmbientFactor"))
|
||
|
{
|
||
|
constant.AmbientFactor = materialObj.GetColor("_AmbientFactor").ToNumericsColor();
|
||
|
}
|
||
|
|
||
|
if (materialObj.HasProperty("_LightMap"))
|
||
|
{
|
||
|
var lmTex = materialObj.GetTexture("_LightMap");
|
||
|
|
||
|
if (lmTex != null)
|
||
|
{
|
||
|
constant.LightmapTexture = ExportTextureInfo(lmTex);
|
||
|
ExportTextureTransform(constant.LightmapTexture, materialObj, "_LightMap");
|
||
|
}
|
||
|
|
||
|
}
|
||
|
|
||
|
if (materialObj.HasProperty("_LightFactor"))
|
||
|
{
|
||
|
constant.LightmapFactor = materialObj.GetColor("_LightFactor").ToNumericsColor();
|
||
|
}
|
||
|
|
||
|
return constant;
|
||
|
}
|
||
|
|
||
|
private TextureInfo ExportTextureInfo(UnityEngine.Texture texture)
|
||
|
{
|
||
|
var info = new TextureInfo();
|
||
|
|
||
|
info.Index = ExportTexture(texture);
|
||
|
|
||
|
return info;
|
||
|
}
|
||
|
|
||
|
private TextureId ExportTexture(UnityEngine.Texture textureObj)
|
||
|
{
|
||
|
TextureId id = GetTextureId(_root, textureObj);
|
||
|
if (id != null)
|
||
|
{
|
||
|
return id;
|
||
|
}
|
||
|
|
||
|
var texture = new GLTF.Schema.Texture();
|
||
|
|
||
|
//If texture name not set give it a unique name using count
|
||
|
if (textureObj.name == "")
|
||
|
{
|
||
|
textureObj.name = (_root.Textures.Count + 1).ToString();
|
||
|
}
|
||
|
|
||
|
if (ExportNames)
|
||
|
{
|
||
|
texture.Name = textureObj.name;
|
||
|
}
|
||
|
|
||
|
texture.Source = ExportImage(textureObj);
|
||
|
texture.Sampler = ExportSampler(textureObj);
|
||
|
|
||
|
_textures.Add(textureObj);
|
||
|
|
||
|
id = new TextureId {
|
||
|
Id = _root.Textures.Count,
|
||
|
Root = _root
|
||
|
};
|
||
|
|
||
|
_root.Textures.Add(texture);
|
||
|
|
||
|
return id;
|
||
|
}
|
||
|
|
||
|
private ImageId ExportImage(UnityEngine.Texture texture)
|
||
|
{
|
||
|
string imagePath = GLTFUtils.buildImageName((Texture2D)texture);
|
||
|
ImageId id = GetImageId(_root, texture);
|
||
|
if(id != null)
|
||
|
{
|
||
|
return id;
|
||
|
}
|
||
|
|
||
|
var image = new Image();
|
||
|
|
||
|
if (ExportNames)
|
||
|
{
|
||
|
image.Name = texture.name;
|
||
|
}
|
||
|
|
||
|
_images.Add(texture as Texture2D);
|
||
|
|
||
|
image.Uri = imagePath;
|
||
|
|
||
|
id = new ImageId {
|
||
|
Id = _root.Images.Count,
|
||
|
Root = _root
|
||
|
};
|
||
|
|
||
|
_root.Images.Add(image);
|
||
|
|
||
|
return id;
|
||
|
}
|
||
|
|
||
|
private SamplerId ExportSampler(UnityEngine.Texture texture)
|
||
|
{
|
||
|
var samplerId = GetSamplerId(_root, texture);
|
||
|
if (samplerId != null)
|
||
|
return samplerId;
|
||
|
|
||
|
var sampler = new Sampler();
|
||
|
|
||
|
if (texture.wrapMode == TextureWrapMode.Clamp)
|
||
|
{
|
||
|
sampler.WrapS = GLTF.Schema.WrapMode.ClampToEdge;
|
||
|
sampler.WrapT = GLTF.Schema.WrapMode.ClampToEdge;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
sampler.WrapS = GLTF.Schema.WrapMode.Repeat;
|
||
|
sampler.WrapT = GLTF.Schema.WrapMode.Repeat;
|
||
|
}
|
||
|
|
||
|
if(texture.filterMode == FilterMode.Point)
|
||
|
{
|
||
|
sampler.MinFilter = MinFilterMode.NearestMipmapNearest;
|
||
|
sampler.MagFilter = MagFilterMode.Nearest;
|
||
|
}
|
||
|
else if(texture.filterMode == FilterMode.Bilinear)
|
||
|
{
|
||
|
sampler.MinFilter = MinFilterMode.Linear;
|
||
|
sampler.MagFilter = MagFilterMode.Linear;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
sampler.MinFilter = MinFilterMode.LinearMipmapLinear;
|
||
|
sampler.MagFilter = MagFilterMode.Linear;
|
||
|
}
|
||
|
|
||
|
samplerId = new SamplerId
|
||
|
{
|
||
|
Id = _root.Samplers.Count,
|
||
|
Root = _root
|
||
|
};
|
||
|
|
||
|
_root.Samplers.Add(sampler);
|
||
|
|
||
|
return samplerId;
|
||
|
}
|
||
|
|
||
|
private Vector2[] FlipY(Vector2[] arr)
|
||
|
{
|
||
|
var len = arr.Length;
|
||
|
for(var i = 0; i < len; i++)
|
||
|
{
|
||
|
arr[i].y = 1 - arr[i].y;
|
||
|
}
|
||
|
return arr;
|
||
|
}
|
||
|
|
||
|
private Vector3[] InvertZ(Vector3[] arr)
|
||
|
{
|
||
|
var len = arr.Length;
|
||
|
for(var i = 0; i < len; i++)
|
||
|
{
|
||
|
arr[i].z = -arr[i].z;
|
||
|
}
|
||
|
return arr;
|
||
|
}
|
||
|
|
||
|
private Vector4[] InvertW(Vector4[] arr)
|
||
|
{
|
||
|
var len = arr.Length;
|
||
|
for(var i = 0; i < len; i++)
|
||
|
{
|
||
|
arr[i].w = -arr[i].w;
|
||
|
}
|
||
|
return arr;
|
||
|
}
|
||
|
|
||
|
private int[] FlipFaces(int[] arr)
|
||
|
{
|
||
|
var triangles = new int[arr.Length];
|
||
|
for (int i = 0; i < arr.Length; i += 3)
|
||
|
{
|
||
|
triangles[i + 2] = arr[i];
|
||
|
triangles[i + 1] = arr[i + 1];
|
||
|
triangles[i] = arr[i + 2];
|
||
|
}
|
||
|
return triangles;
|
||
|
}
|
||
|
|
||
|
private AccessorId ExportAccessor(int[] arr, bool isIndices = false)
|
||
|
{
|
||
|
var count = arr.Length;
|
||
|
|
||
|
if (count == 0)
|
||
|
{
|
||
|
throw new Exception("Accessors can not have a count of 0.");
|
||
|
}
|
||
|
|
||
|
var accessor = new Accessor();
|
||
|
accessor.Count = count;
|
||
|
accessor.Type = GLTFAccessorAttributeType.SCALAR;
|
||
|
|
||
|
int min = arr[0];
|
||
|
int max = arr[0];
|
||
|
|
||
|
for (var i = 1; i < count; i++)
|
||
|
{
|
||
|
var cur = arr[i];
|
||
|
|
||
|
if (cur < min)
|
||
|
{
|
||
|
min = cur;
|
||
|
}
|
||
|
if (cur > max)
|
||
|
{
|
||
|
max = cur;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
var byteOffset = _bufferWriter.BaseStream.Position;
|
||
|
if (_forceIndicesUint)
|
||
|
{
|
||
|
accessor.ComponentType = GLTFComponentType.UnsignedInt;
|
||
|
foreach (var v in arr)
|
||
|
{
|
||
|
_bufferWriter.Write((uint)v);
|
||
|
|
||
|
}
|
||
|
}
|
||
|
else if (max <= byte.MaxValue && min >= byte.MinValue)
|
||
|
{
|
||
|
accessor.ComponentType = GLTFComponentType.UnsignedByte;
|
||
|
|
||
|
foreach (var v in arr)
|
||
|
{
|
||
|
_bufferWriter.Write((byte)v);
|
||
|
}
|
||
|
}
|
||
|
else if (max <= sbyte.MaxValue && min >= sbyte.MinValue && !isIndices)
|
||
|
{
|
||
|
accessor.ComponentType = GLTFComponentType.Byte;
|
||
|
|
||
|
foreach (var v in arr)
|
||
|
{
|
||
|
_bufferWriter.Write((sbyte)v);
|
||
|
}
|
||
|
}
|
||
|
else if (max <= short.MaxValue && min >= short.MinValue && !isIndices)
|
||
|
{
|
||
|
accessor.ComponentType = GLTFComponentType.Short;
|
||
|
|
||
|
foreach (var v in arr)
|
||
|
{
|
||
|
_bufferWriter.Write((short)v);
|
||
|
}
|
||
|
}
|
||
|
else if (max <= ushort.MaxValue && min >= ushort.MinValue)
|
||
|
{
|
||
|
accessor.ComponentType = GLTFComponentType.UnsignedShort;
|
||
|
|
||
|
foreach (var v in arr)
|
||
|
{
|
||
|
_bufferWriter.Write((ushort)v);
|
||
|
}
|
||
|
}
|
||
|
else if (min >= uint.MinValue)
|
||
|
{
|
||
|
accessor.ComponentType = GLTFComponentType.UnsignedInt;
|
||
|
|
||
|
foreach (var v in arr)
|
||
|
{
|
||
|
_bufferWriter.Write((uint)v);
|
||
|
}
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
accessor.ComponentType = GLTFComponentType.Float;
|
||
|
|
||
|
foreach (var v in arr)
|
||
|
{
|
||
|
_bufferWriter.Write((float)v);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
accessor.Min = new List<double> { min };
|
||
|
accessor.Max = new List<double> { max };
|
||
|
|
||
|
var byteLength = _bufferWriter.BaseStream.Position - byteOffset;
|
||
|
|
||
|
accessor.BufferView = ExportBufferView((int)byteOffset, (int)byteLength);
|
||
|
|
||
|
var id = new AccessorId {
|
||
|
Id = _root.Accessors.Count,
|
||
|
Root = _root
|
||
|
};
|
||
|
_root.Accessors.Add(accessor);
|
||
|
|
||
|
return id;
|
||
|
}
|
||
|
|
||
|
private AccessorId ExportAccessor(float[] arr)
|
||
|
{
|
||
|
var count = arr.Length;
|
||
|
|
||
|
if (count == 0)
|
||
|
{
|
||
|
throw new Exception("Accessors can not have a count of 0.");
|
||
|
}
|
||
|
|
||
|
var accessor = new Accessor();
|
||
|
accessor.ComponentType = GLTFComponentType.Float;
|
||
|
accessor.Count = count;
|
||
|
accessor.Type = GLTFAccessorAttributeType.SCALAR;
|
||
|
|
||
|
float min = arr[0];
|
||
|
float max = arr[0];
|
||
|
|
||
|
for (var i = 1; i < count; i++)
|
||
|
{
|
||
|
var cur = arr[i];
|
||
|
|
||
|
if (cur < min)
|
||
|
{
|
||
|
min = cur;
|
||
|
}
|
||
|
if (cur > max)
|
||
|
{
|
||
|
max = cur;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
accessor.Min = new List<double> { min };
|
||
|
accessor.Max = new List<double> { max };
|
||
|
|
||
|
var byteOffset = _bufferWriter.BaseStream.Position;
|
||
|
|
||
|
foreach (var value in arr)
|
||
|
{
|
||
|
_bufferWriter.Write(value);
|
||
|
}
|
||
|
|
||
|
var byteLength = _bufferWriter.BaseStream.Position - byteOffset;
|
||
|
|
||
|
accessor.BufferView = ExportBufferView((int)byteOffset, (int)byteLength);
|
||
|
|
||
|
var id = new AccessorId
|
||
|
{
|
||
|
Id = _root.Accessors.Count,
|
||
|
Root = _root
|
||
|
};
|
||
|
|
||
|
_root.Accessors.Add(accessor);
|
||
|
|
||
|
return id;
|
||
|
}
|
||
|
|
||
|
private AccessorId ExportAccessor(Vector2[] arr)
|
||
|
{
|
||
|
var count = arr.Length;
|
||
|
|
||
|
if (count == 0)
|
||
|
{
|
||
|
throw new Exception("Accessors can not have a count of 0.");
|
||
|
}
|
||
|
|
||
|
var accessor = new Accessor();
|
||
|
accessor.ComponentType = GLTFComponentType.Float;
|
||
|
accessor.Count = count;
|
||
|
accessor.Type = GLTFAccessorAttributeType.VEC2;
|
||
|
|
||
|
float minX = arr[0].x;
|
||
|
float minY = arr[0].y;
|
||
|
float maxX = arr[0].x;
|
||
|
float maxY = arr[0].y;
|
||
|
|
||
|
for (var i = 1; i < count; i++)
|
||
|
{
|
||
|
var cur = arr[i];
|
||
|
|
||
|
if (cur.x < minX)
|
||
|
{
|
||
|
minX = cur.x;
|
||
|
}
|
||
|
if (cur.y < minY)
|
||
|
{
|
||
|
minY = cur.y;
|
||
|
}
|
||
|
if (cur.x > maxX)
|
||
|
{
|
||
|
maxX = cur.x;
|
||
|
}
|
||
|
if (cur.y > maxY)
|
||
|
{
|
||
|
maxY = cur.y;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
accessor.Min = new List<double> { minX, minY };
|
||
|
accessor.Max = new List<double> { maxX, maxY };
|
||
|
|
||
|
var byteOffset = _bufferWriter.BaseStream.Position;
|
||
|
|
||
|
foreach (var vec in arr) {
|
||
|
_bufferWriter.Write(vec.x);
|
||
|
_bufferWriter.Write(vec.y);
|
||
|
}
|
||
|
|
||
|
var byteLength = _bufferWriter.BaseStream.Position - byteOffset;
|
||
|
|
||
|
accessor.BufferView = ExportBufferView((int)byteOffset, (int)byteLength);
|
||
|
|
||
|
var id = new AccessorId {
|
||
|
Id = _root.Accessors.Count,
|
||
|
Root = _root
|
||
|
};
|
||
|
_root.Accessors.Add(accessor);
|
||
|
|
||
|
return id;
|
||
|
}
|
||
|
|
||
|
private AccessorId ExportAccessor(Vector3[] arr, bool switchHandedness=false)
|
||
|
{
|
||
|
var count = arr.Length;
|
||
|
|
||
|
if (count == 0)
|
||
|
{
|
||
|
throw new Exception("Accessors can not have a count of 0.");
|
||
|
}
|
||
|
|
||
|
var accessor = new Accessor();
|
||
|
accessor.ComponentType = GLTFComponentType.Float;
|
||
|
accessor.Count = count;
|
||
|
accessor.Type = GLTFAccessorAttributeType.VEC3;
|
||
|
|
||
|
float minX = arr[0].x;
|
||
|
float minY = arr[0].y;
|
||
|
float minZ = arr[0].z;
|
||
|
float maxX = arr[0].x;
|
||
|
float maxY = arr[0].y;
|
||
|
float maxZ = arr[0].z;
|
||
|
|
||
|
for (var i = 1; i < count; i++)
|
||
|
{
|
||
|
var cur = arr[i];
|
||
|
|
||
|
if (cur.x < minX)
|
||
|
{
|
||
|
minX = cur.x;
|
||
|
}
|
||
|
if (cur.y < minY)
|
||
|
{
|
||
|
minY = cur.y;
|
||
|
}
|
||
|
if (cur.z < minZ)
|
||
|
{
|
||
|
minZ = cur.z;
|
||
|
}
|
||
|
if (cur.x > maxX)
|
||
|
{
|
||
|
maxX = cur.x;
|
||
|
}
|
||
|
if (cur.y > maxY)
|
||
|
{
|
||
|
maxY = cur.y;
|
||
|
}
|
||
|
if (cur.z > maxZ)
|
||
|
{
|
||
|
maxZ = cur.z;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
accessor.Min = new List<double> { minX, minY, minZ };
|
||
|
accessor.Max = new List<double> { maxX, maxY, maxZ };
|
||
|
|
||
|
var byteOffset = _bufferWriter.BaseStream.Position;
|
||
|
|
||
|
foreach (var vec in arr) {
|
||
|
if(switchHandedness)
|
||
|
{
|
||
|
Vector3 vect = vec.switchHandedness();
|
||
|
_bufferWriter.Write(vect.x);
|
||
|
_bufferWriter.Write(vect.y);
|
||
|
_bufferWriter.Write(vect.z);
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
_bufferWriter.Write(vec.x);
|
||
|
_bufferWriter.Write(vec.y);
|
||
|
_bufferWriter.Write(vec.z);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
var byteLength = _bufferWriter.BaseStream.Position - byteOffset;
|
||
|
|
||
|
accessor.BufferView = ExportBufferView((int)byteOffset, (int)byteLength);
|
||
|
|
||
|
var id = new AccessorId {
|
||
|
Id = _root.Accessors.Count,
|
||
|
Root = _root
|
||
|
};
|
||
|
_root.Accessors.Add(accessor);
|
||
|
|
||
|
return id;
|
||
|
}
|
||
|
|
||
|
private AccessorId ExportVerticesAccessor(Vector3[] arr, GameObject parent, bool switchHandedness=false)
|
||
|
{
|
||
|
var count = arr.Length;
|
||
|
|
||
|
if (count == 0)
|
||
|
{
|
||
|
throw new Exception("Accessors can not have a count of 0.");
|
||
|
}
|
||
|
|
||
|
var accessor = new Accessor();
|
||
|
accessor.ComponentType = GLTFComponentType.Float;
|
||
|
accessor.Count = count;
|
||
|
accessor.Type = GLTFAccessorAttributeType.VEC3;
|
||
|
|
||
|
if (GLTFUtils.boundsExportOption == GLTFUtils.BoundsExportOption.Default) {
|
||
|
float minX = arr[0].x;
|
||
|
float minY = arr[0].y;
|
||
|
float minZ = arr[0].z;
|
||
|
float maxX = arr[0].x;
|
||
|
float maxY = arr[0].y;
|
||
|
float maxZ = arr[0].z;
|
||
|
|
||
|
for (var i = 1; i < count; i++)
|
||
|
{
|
||
|
var cur = arr[i];
|
||
|
|
||
|
if (cur.x < minX)
|
||
|
{
|
||
|
minX = cur.x;
|
||
|
}
|
||
|
if (cur.y < minY)
|
||
|
{
|
||
|
minY = cur.y;
|
||
|
}
|
||
|
if (cur.z < minZ)
|
||
|
{
|
||
|
minZ = cur.z;
|
||
|
}
|
||
|
if (cur.x > maxX)
|
||
|
{
|
||
|
maxX = cur.x;
|
||
|
}
|
||
|
if (cur.y > maxY)
|
||
|
{
|
||
|
maxY = cur.y;
|
||
|
}
|
||
|
if (cur.z > maxZ)
|
||
|
{
|
||
|
maxZ = cur.z;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
accessor.Min = new List<double> { minX, minY, minZ };
|
||
|
accessor.Max = new List<double> { maxX, maxY, maxZ };
|
||
|
}
|
||
|
else if (GLTFUtils.boundsExportOption == GLTFUtils.BoundsExportOption.SceneViewerFix) {
|
||
|
Bounds totalBounds = new Bounds();
|
||
|
|
||
|
for (var i = 0; i < count; i++) {
|
||
|
totalBounds.Encapsulate(arr[i]);
|
||
|
}
|
||
|
|
||
|
float pivotX = (totalBounds.min.x + totalBounds.max.x) / 2.0f;
|
||
|
float pivotY = (totalBounds.min.y + totalBounds.max.y) / 2.0f;
|
||
|
float pivotZ = (totalBounds.min.z + totalBounds.max.z) / 2.0f;
|
||
|
|
||
|
float minX = totalBounds.min.x;
|
||
|
float minY = totalBounds.min.y;
|
||
|
float minZ = totalBounds.min.z;
|
||
|
|
||
|
float maxX = totalBounds.max.x;
|
||
|
float maxY = totalBounds.max.y;
|
||
|
float maxZ = totalBounds.max.z;
|
||
|
|
||
|
accessor.Min = new List<double> {minX - pivotX, minY - pivotY, minZ - pivotZ};
|
||
|
accessor.Max = new List<double> {maxX - pivotX, maxY - pivotY, maxZ - pivotZ};
|
||
|
}
|
||
|
|
||
|
var byteOffset = _bufferWriter.BaseStream.Position;
|
||
|
|
||
|
foreach (var vec in arr) {
|
||
|
if(switchHandedness)
|
||
|
{
|
||
|
Vector3 vect = vec.switchHandedness();
|
||
|
_bufferWriter.Write(vect.x);
|
||
|
_bufferWriter.Write(vect.y);
|
||
|
_bufferWriter.Write(vect.z);
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
_bufferWriter.Write(vec.x);
|
||
|
_bufferWriter.Write(vec.y);
|
||
|
_bufferWriter.Write(vec.z);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
var byteLength = _bufferWriter.BaseStream.Position - byteOffset;
|
||
|
|
||
|
accessor.BufferView = ExportBufferView((int)byteOffset, (int)byteLength);
|
||
|
|
||
|
var id = new AccessorId {
|
||
|
Id = _root.Accessors.Count,
|
||
|
Root = _root
|
||
|
};
|
||
|
_root.Accessors.Add(accessor);
|
||
|
|
||
|
return id;
|
||
|
}
|
||
|
|
||
|
private AccessorId ExportAccessorUint(Vector4[] arr)
|
||
|
{
|
||
|
var count = arr.Length;
|
||
|
|
||
|
if (count == 0)
|
||
|
{
|
||
|
throw new Exception("Accessors can not have a count of 0.");
|
||
|
}
|
||
|
|
||
|
var accessor = new Accessor();
|
||
|
accessor.ComponentType = GLTFComponentType.UnsignedShort;
|
||
|
accessor.Count = count;
|
||
|
accessor.Type = GLTFAccessorAttributeType.VEC4;
|
||
|
|
||
|
float minX = arr[0].x;
|
||
|
float minY = arr[0].y;
|
||
|
float minZ = arr[0].z;
|
||
|
float minW = arr[0].w;
|
||
|
float maxX = arr[0].x;
|
||
|
float maxY = arr[0].y;
|
||
|
float maxZ = arr[0].z;
|
||
|
float maxW = arr[0].w;
|
||
|
|
||
|
for (var i = 1; i < count; i++)
|
||
|
{
|
||
|
var cur = arr[i];
|
||
|
|
||
|
if (cur.x < minX)
|
||
|
{
|
||
|
minX = cur.x;
|
||
|
}
|
||
|
if (cur.y < minY)
|
||
|
{
|
||
|
minY = cur.y;
|
||
|
}
|
||
|
if (cur.z < minZ)
|
||
|
{
|
||
|
minZ = cur.z;
|
||
|
}
|
||
|
if (cur.w < minW)
|
||
|
{
|
||
|
minW = cur.w;
|
||
|
}
|
||
|
if (cur.x > maxX)
|
||
|
{
|
||
|
maxX = cur.x;
|
||
|
}
|
||
|
if (cur.y > maxY)
|
||
|
{
|
||
|
maxY = cur.y;
|
||
|
}
|
||
|
if (cur.z > maxZ)
|
||
|
{
|
||
|
maxZ = cur.z;
|
||
|
}
|
||
|
if (cur.w > maxW)
|
||
|
{
|
||
|
maxW = cur.w;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
accessor.Min = new List<double> { minX, minY, minZ, minW };
|
||
|
accessor.Max = new List<double> { maxX, maxY, maxZ, maxW };
|
||
|
|
||
|
var byteOffset = _bufferWriter.BaseStream.Position;
|
||
|
|
||
|
foreach (var vec in arr)
|
||
|
{
|
||
|
_bufferWriter.Write((ushort)vec.x);
|
||
|
_bufferWriter.Write((ushort)vec.y);
|
||
|
_bufferWriter.Write((ushort)vec.z);
|
||
|
_bufferWriter.Write((ushort)vec.w);
|
||
|
}
|
||
|
|
||
|
var byteLength = _bufferWriter.BaseStream.Position - byteOffset;
|
||
|
|
||
|
accessor.BufferView = ExportBufferView((int)byteOffset, (int)byteLength);
|
||
|
|
||
|
var id = new AccessorId
|
||
|
{
|
||
|
Id = _root.Accessors.Count,
|
||
|
Root = _root
|
||
|
};
|
||
|
_root.Accessors.Add(accessor);
|
||
|
|
||
|
return id;
|
||
|
}
|
||
|
|
||
|
private AccessorId ExportAccessor(Vector4[] arr, bool switchHandedness=false)
|
||
|
{
|
||
|
var count = arr.Length;
|
||
|
|
||
|
if (count == 0)
|
||
|
{
|
||
|
throw new Exception("Accessors can not have a count of 0.");
|
||
|
}
|
||
|
|
||
|
var accessor = new Accessor();
|
||
|
accessor.ComponentType = GLTFComponentType.Float;
|
||
|
accessor.Count = count;
|
||
|
accessor.Type = GLTFAccessorAttributeType.VEC4;
|
||
|
|
||
|
float minX = arr[0].x;
|
||
|
float minY = arr[0].y;
|
||
|
float minZ = arr[0].z;
|
||
|
float minW = arr[0].w;
|
||
|
float maxX = arr[0].x;
|
||
|
float maxY = arr[0].y;
|
||
|
float maxZ = arr[0].z;
|
||
|
float maxW = arr[0].w;
|
||
|
|
||
|
for (var i = 1; i < count; i++)
|
||
|
{
|
||
|
var cur = arr[i];
|
||
|
|
||
|
if (cur.x < minX)
|
||
|
{
|
||
|
minX = cur.x;
|
||
|
}
|
||
|
if (cur.y < minY)
|
||
|
{
|
||
|
minY = cur.y;
|
||
|
}
|
||
|
if (cur.z < minZ)
|
||
|
{
|
||
|
minZ = cur.z;
|
||
|
}
|
||
|
if (cur.w < minW)
|
||
|
{
|
||
|
minW = cur.w;
|
||
|
}
|
||
|
if (cur.x > maxX)
|
||
|
{
|
||
|
maxX = cur.x;
|
||
|
}
|
||
|
if (cur.y > maxY)
|
||
|
{
|
||
|
maxY = cur.y;
|
||
|
}
|
||
|
if (cur.z > maxZ)
|
||
|
{
|
||
|
maxZ = cur.z;
|
||
|
}
|
||
|
if (cur.w > maxW)
|
||
|
{
|
||
|
maxW = cur.w;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
accessor.Min = new List<double> { minX, minY, minZ, minW };
|
||
|
accessor.Max = new List<double> { maxX, maxY, maxZ, maxW };
|
||
|
|
||
|
var byteOffset = _bufferWriter.BaseStream.Position;
|
||
|
|
||
|
foreach (var vec in arr) {
|
||
|
Vector4 vect = switchHandedness ? vec.switchHandedness() : vec;
|
||
|
_bufferWriter.Write(vect.x);
|
||
|
_bufferWriter.Write(vect.y);
|
||
|
_bufferWriter.Write(vect.z);
|
||
|
_bufferWriter.Write(vect.w);
|
||
|
}
|
||
|
|
||
|
var byteLength = _bufferWriter.BaseStream.Position - byteOffset;
|
||
|
|
||
|
accessor.BufferView = ExportBufferView((int)byteOffset, (int)byteLength);
|
||
|
|
||
|
var id = new AccessorId {
|
||
|
Id = _root.Accessors.Count,
|
||
|
Root = _root
|
||
|
};
|
||
|
_root.Accessors.Add(accessor);
|
||
|
|
||
|
return id;
|
||
|
}
|
||
|
|
||
|
private AccessorId ExportAccessor(UnityEngine.Color[] arr)
|
||
|
{
|
||
|
var count = arr.Length;
|
||
|
|
||
|
if (count == 0)
|
||
|
{
|
||
|
throw new Exception("Accessors can not have a count of 0.");
|
||
|
}
|
||
|
|
||
|
var accessor = new Accessor();
|
||
|
accessor.ComponentType = GLTFComponentType.Float;
|
||
|
accessor.Count = count;
|
||
|
accessor.Type = GLTFAccessorAttributeType.VEC4;
|
||
|
|
||
|
float minR = arr[0].r;
|
||
|
float minG = arr[0].g;
|
||
|
float minB = arr[0].b;
|
||
|
float minA = arr[0].a;
|
||
|
float maxR = arr[0].r;
|
||
|
float maxG = arr[0].g;
|
||
|
float maxB = arr[0].b;
|
||
|
float maxA = arr[0].a;
|
||
|
|
||
|
for (var i = 1; i < count; i++)
|
||
|
{
|
||
|
var cur = arr[i];
|
||
|
|
||
|
if (cur.r < minR)
|
||
|
{
|
||
|
minR = cur.r;
|
||
|
}
|
||
|
if (cur.g < minG)
|
||
|
{
|
||
|
minG = cur.g;
|
||
|
}
|
||
|
if (cur.b < minB)
|
||
|
{
|
||
|
minB = cur.b;
|
||
|
}
|
||
|
if (cur.a < minA)
|
||
|
{
|
||
|
minA = cur.a;
|
||
|
}
|
||
|
if (cur.r > maxR)
|
||
|
{
|
||
|
maxR = cur.r;
|
||
|
}
|
||
|
if (cur.g > maxG)
|
||
|
{
|
||
|
maxG = cur.g;
|
||
|
}
|
||
|
if (cur.b > maxB)
|
||
|
{
|
||
|
maxB = cur.b;
|
||
|
}
|
||
|
if (cur.a > maxA)
|
||
|
{
|
||
|
maxA = cur.a;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
accessor.Min = new List<double> { minR, minG, minB, minA };
|
||
|
accessor.Max = new List<double> { maxR, maxG, maxB, maxA };
|
||
|
|
||
|
var byteOffset = _bufferWriter.BaseStream.Position;
|
||
|
|
||
|
foreach (var color in arr) {
|
||
|
_bufferWriter.Write(color.r);
|
||
|
_bufferWriter.Write(color.g);
|
||
|
_bufferWriter.Write(color.b);
|
||
|
_bufferWriter.Write(color.a);
|
||
|
}
|
||
|
|
||
|
var byteLength = _bufferWriter.BaseStream.Position - byteOffset;
|
||
|
|
||
|
accessor.BufferView = ExportBufferView((int)byteOffset, (int)byteLength);
|
||
|
|
||
|
var id = new AccessorId {
|
||
|
Id = _root.Accessors.Count,
|
||
|
Root = _root
|
||
|
};
|
||
|
_root.Accessors.Add(accessor);
|
||
|
|
||
|
return id;
|
||
|
}
|
||
|
|
||
|
private AccessorId ExportAccessor(Matrix4x4[] arr, bool switchHandedness = false)
|
||
|
{
|
||
|
var count = arr.Length;
|
||
|
|
||
|
if (count == 0)
|
||
|
{
|
||
|
throw new Exception("Accessors can not have a count of 0.");
|
||
|
}
|
||
|
|
||
|
var accessor = new Accessor();
|
||
|
accessor.ComponentType = GLTFComponentType.Float;
|
||
|
accessor.Count = count;
|
||
|
accessor.Type = GLTFAccessorAttributeType.MAT4;
|
||
|
|
||
|
// Dont serialize min/max for matrices
|
||
|
|
||
|
var byteOffset = _bufferWriter.BaseStream.Position;
|
||
|
|
||
|
foreach (var mat in arr)
|
||
|
{
|
||
|
Matrix4x4 mamat = switchHandedness ? mat.switchHandedness() : mat;
|
||
|
for (int i = 0; i < 4; ++i)
|
||
|
{
|
||
|
Vector4 col = mamat.GetColumn(i);
|
||
|
_bufferWriter.Write(col.x);
|
||
|
_bufferWriter.Write(col.y);
|
||
|
_bufferWriter.Write(col.z);
|
||
|
_bufferWriter.Write(col.w);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
var byteLength = _bufferWriter.BaseStream.Position - byteOffset;
|
||
|
|
||
|
accessor.BufferView = ExportBufferView((int)byteOffset, (int)byteLength);
|
||
|
|
||
|
var id = new AccessorId
|
||
|
{
|
||
|
Id = _root.Accessors.Count,
|
||
|
Root = _root
|
||
|
};
|
||
|
_root.Accessors.Add(accessor);
|
||
|
|
||
|
return id;
|
||
|
}
|
||
|
|
||
|
private BufferViewId ExportBufferView(int byteOffset, int byteLength)
|
||
|
{
|
||
|
var bufferView = new BufferView {
|
||
|
Buffer = _bufferId,
|
||
|
ByteOffset = byteOffset,
|
||
|
ByteLength = byteLength,
|
||
|
};
|
||
|
|
||
|
var id = new BufferViewId {
|
||
|
Id = _root.BufferViews.Count,
|
||
|
Root = _root
|
||
|
};
|
||
|
|
||
|
_root.BufferViews.Add(bufferView);
|
||
|
|
||
|
return id;
|
||
|
}
|
||
|
|
||
|
public MaterialId GetMaterialId(GLTFRoot root, UnityEngine.Material materialObj)
|
||
|
{
|
||
|
for (var i = 0; i < _materials.Count; i++)
|
||
|
{
|
||
|
if (_materials[i] == materialObj)
|
||
|
{
|
||
|
return new MaterialId
|
||
|
{
|
||
|
Id = i,
|
||
|
Root = root
|
||
|
};
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
public TextureId GetTextureId(GLTFRoot root, UnityEngine.Texture textureObj)
|
||
|
{
|
||
|
for (var i = 0; i < _textures.Count; i++)
|
||
|
{
|
||
|
if (_textures[i] == textureObj)
|
||
|
{
|
||
|
return new TextureId
|
||
|
{
|
||
|
Id = i,
|
||
|
Root = root
|
||
|
};
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
public ImageId GetImageId(GLTFRoot root, UnityEngine.Texture imageObj)
|
||
|
{
|
||
|
for (var i = 0; i < _images.Count; i++)
|
||
|
{
|
||
|
if (_images[i] == imageObj)
|
||
|
{
|
||
|
return new ImageId
|
||
|
{
|
||
|
Id = i,
|
||
|
Root = root
|
||
|
};
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
public SamplerId GetSamplerId(GLTFRoot root, UnityEngine.Texture textureObj)
|
||
|
{
|
||
|
for (var i = 0; i < root.Samplers.Count; i++)
|
||
|
{
|
||
|
bool filterIsNearest = root.Samplers[i].MinFilter == MinFilterMode.Nearest
|
||
|
|| root.Samplers[i].MinFilter == MinFilterMode.NearestMipmapNearest
|
||
|
|| root.Samplers[i].MinFilter == MinFilterMode.LinearMipmapNearest;
|
||
|
|
||
|
bool filterIsLinear = root.Samplers[i].MinFilter == MinFilterMode.Linear
|
||
|
|| root.Samplers[i].MinFilter == MinFilterMode.NearestMipmapLinear;
|
||
|
|
||
|
bool filterMatched = textureObj.filterMode == FilterMode.Point && filterIsNearest
|
||
|
|| textureObj.filterMode == FilterMode.Bilinear && filterIsLinear
|
||
|
|| textureObj.filterMode == FilterMode.Trilinear && root.Samplers[i].MinFilter == MinFilterMode.LinearMipmapLinear;
|
||
|
|
||
|
bool wrapMatched = textureObj.wrapMode == TextureWrapMode.Clamp && root.Samplers[i].WrapS == GLTF.Schema.WrapMode.ClampToEdge
|
||
|
|| textureObj.wrapMode == TextureWrapMode.Repeat && root.Samplers[i].WrapS != GLTF.Schema.WrapMode.ClampToEdge;
|
||
|
|
||
|
if (filterMatched && wrapMatched)
|
||
|
{
|
||
|
return new SamplerId
|
||
|
{
|
||
|
Id = i,
|
||
|
Root = root
|
||
|
};
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
public enum ROTATION_TYPE
|
||
|
{
|
||
|
UNKNOWN,
|
||
|
QUATERNION,
|
||
|
EULER
|
||
|
};
|
||
|
|
||
|
private struct TargetCurveSet
|
||
|
{
|
||
|
public AnimationCurve[] translationCurves;
|
||
|
public AnimationCurve[] rotationCurves;
|
||
|
//Additional curve types
|
||
|
public AnimationCurve[] localEulerAnglesRaw;
|
||
|
public AnimationCurve[] m_LocalEuler;
|
||
|
public AnimationCurve[] scaleCurves;
|
||
|
public ROTATION_TYPE rotationType;
|
||
|
public void Init()
|
||
|
{
|
||
|
translationCurves = new AnimationCurve[3];
|
||
|
rotationCurves = new AnimationCurve[4];
|
||
|
scaleCurves = new AnimationCurve[3];
|
||
|
}
|
||
|
}
|
||
|
|
||
|
static int bakingFramerate = 30; // FPS
|
||
|
static bool bake = true;
|
||
|
|
||
|
// Parses Animation/Animator component and generate a glTF animation for the active clip
|
||
|
public void exportAnimationFromNode(ref Transform transform/*, ref GLTF.Schema.Animation anim*/)
|
||
|
{
|
||
|
Animator a = transform.GetComponent<Animator>();
|
||
|
|
||
|
if (a != null) {
|
||
|
AnimationClip[] clips = AnimationUtility.GetAnimationClips(transform.gameObject);
|
||
|
|
||
|
for (int i = 0; i < clips.Length; i++) {
|
||
|
GLTF.Schema.Animation anim = new GLTF.Schema.Animation();
|
||
|
anim.Name = GLTFEditorExporter.cleanNonAlphanumeric(a.name);
|
||
|
convertClipToGLTFAnimation(ref clips[i], ref transform, ref anim);
|
||
|
|
||
|
if (anim.Channels.Count > 0 && anim.Samplers.Count > 0) {
|
||
|
_root.Animations.Add(anim);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
UnityEngine.Animation animation = transform.GetComponent<UnityEngine.Animation>();
|
||
|
if (animation != null) {
|
||
|
foreach (AnimationState state in animation) {
|
||
|
AnimationClip clip = state.clip;
|
||
|
|
||
|
GLTF.Schema.Animation anim = new GLTF.Schema.Animation();
|
||
|
anim.Name = GLTFEditorExporter.cleanNonAlphanumeric(state.name);
|
||
|
convertClipToGLTFAnimation(ref clip, ref transform, ref anim);
|
||
|
|
||
|
if (anim.Channels.Count > 0 && anim.Samplers.Count > 0) {
|
||
|
_root.Animations.Add(anim);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private int getTargetIdFromTransform(ref Transform transform)
|
||
|
{
|
||
|
if (_exportedTransforms.ContainsKey(transform.GetInstanceID()))
|
||
|
{
|
||
|
return _exportedTransforms[transform.GetInstanceID()];
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
Debug.Log(transform.name + " " + transform.GetInstanceID());
|
||
|
return 0;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private AccessorId ExportAccessor()
|
||
|
{
|
||
|
var id = new AccessorId
|
||
|
{
|
||
|
Id = 5,
|
||
|
Root = _root
|
||
|
};
|
||
|
|
||
|
return id;
|
||
|
}
|
||
|
|
||
|
private void convertClipToGLTFAnimation(ref AnimationClip clip, ref Transform transform, ref GLTF.Schema.Animation animation)
|
||
|
{
|
||
|
// Generate GLTF.Schema.AnimationChannel and GLTF.Schema.AnimationSampler
|
||
|
// 1 channel per node T/R/S, one sampler per node T/R/S
|
||
|
// Need to keep a list of nodes to convert to indexes
|
||
|
|
||
|
// 1. browse clip, collect all curves and create a TargetCurveSet for each target
|
||
|
Dictionary<string, TargetCurveSet> targetCurvesBinding = new Dictionary<string, TargetCurveSet>();
|
||
|
collectClipCurves(clip, ref targetCurvesBinding);
|
||
|
|
||
|
// Baking needs all properties, fill missing curves with transform data in 2 keyframes (start, endTime)
|
||
|
// where endTime is clip duration
|
||
|
// Note: we should avoid creating curves for a property if none of it's components is animated
|
||
|
generateMissingCurves(clip.length, ref transform, ref targetCurvesBinding);
|
||
|
|
||
|
if (bake)
|
||
|
{
|
||
|
// Bake animation for all animated nodes
|
||
|
foreach (string target in targetCurvesBinding.Keys)
|
||
|
{
|
||
|
Transform targetTr = target.Length > 0 ? transform.Find(target) : transform;
|
||
|
if (targetTr == null || targetTr.GetComponent<SkinnedMeshRenderer>())
|
||
|
{
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
|
||
|
// Initialize data
|
||
|
// Bake and populate animation data
|
||
|
float[] times = null;
|
||
|
Vector3[] positions = null;
|
||
|
Vector3[] scales = null;
|
||
|
Vector4[] rotations = null;
|
||
|
bakeCurveSet(targetCurvesBinding[target], clip.length, bakingFramerate, ref times, ref positions, ref rotations, ref scales);
|
||
|
|
||
|
int channelTargetId = getTargetIdFromTransform(ref targetTr);
|
||
|
AccessorId timeAccessor = ExportAccessor(times);
|
||
|
|
||
|
// Create channel
|
||
|
AnimationChannel Tchannel = new AnimationChannel();
|
||
|
AnimationChannelTarget TchannelTarget = new AnimationChannelTarget();
|
||
|
TchannelTarget.Path = GLTFAnimationChannelPath.translation;
|
||
|
TchannelTarget.Node = new NodeId
|
||
|
{
|
||
|
Id = channelTargetId,
|
||
|
Root = _root
|
||
|
};
|
||
|
|
||
|
Tchannel.Target = TchannelTarget;
|
||
|
|
||
|
AnimationSampler Tsampler = new AnimationSampler();
|
||
|
Tsampler.Input = timeAccessor;
|
||
|
Tsampler.Output = ExportAccessor(positions, true); // Vec3 for translation
|
||
|
Tchannel.Sampler = new AnimationSamplerId
|
||
|
{
|
||
|
Id = animation.Samplers.Count,
|
||
|
GLTFAnimation = animation,
|
||
|
Root = _root
|
||
|
};
|
||
|
|
||
|
animation.Samplers.Add(Tsampler);
|
||
|
animation.Channels.Add(Tchannel);
|
||
|
|
||
|
// Rotation
|
||
|
AnimationChannel Rchannel = new AnimationChannel();
|
||
|
AnimationChannelTarget RchannelTarget = new AnimationChannelTarget();
|
||
|
RchannelTarget.Path = GLTFAnimationChannelPath.rotation;
|
||
|
RchannelTarget.Node = new NodeId
|
||
|
{
|
||
|
Id = channelTargetId,
|
||
|
Root = _root
|
||
|
};
|
||
|
|
||
|
Rchannel.Target = RchannelTarget;
|
||
|
|
||
|
AnimationSampler Rsampler = new AnimationSampler();
|
||
|
Rsampler.Input = timeAccessor; // Float, for time
|
||
|
Rsampler.Output = ExportAccessor(rotations, true); // Vec4 for
|
||
|
Rchannel.Sampler = new AnimationSamplerId
|
||
|
{
|
||
|
Id = animation.Samplers.Count,
|
||
|
GLTFAnimation = animation,
|
||
|
Root = _root
|
||
|
};
|
||
|
|
||
|
animation.Samplers.Add(Rsampler);
|
||
|
animation.Channels.Add(Rchannel);
|
||
|
|
||
|
// Scale
|
||
|
AnimationChannel Schannel = new AnimationChannel();
|
||
|
AnimationChannelTarget SchannelTarget = new AnimationChannelTarget();
|
||
|
SchannelTarget.Path = GLTFAnimationChannelPath.scale;
|
||
|
SchannelTarget.Node = new NodeId
|
||
|
{
|
||
|
Id = channelTargetId,
|
||
|
Root = _root
|
||
|
};
|
||
|
|
||
|
Schannel.Target = SchannelTarget;
|
||
|
|
||
|
AnimationSampler Ssampler = new AnimationSampler();
|
||
|
Ssampler.Input = timeAccessor; // Float, for time
|
||
|
Ssampler.Output = ExportAccessor(scales); // Vec3 for scale
|
||
|
Schannel.Sampler = new AnimationSamplerId
|
||
|
{
|
||
|
Id = animation.Samplers.Count,
|
||
|
GLTFAnimation = animation,
|
||
|
Root = _root
|
||
|
};
|
||
|
|
||
|
animation.Samplers.Add(Ssampler);
|
||
|
animation.Channels.Add(Schannel);
|
||
|
}
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
Debug.LogError("Only baked animation is supported for now. Skipping animation");
|
||
|
}
|
||
|
|
||
|
}
|
||
|
|
||
|
private void collectClipCurves(AnimationClip clip, ref Dictionary<string, TargetCurveSet> targetCurves)
|
||
|
{
|
||
|
foreach (var binding in UnityEditor.AnimationUtility.GetCurveBindings(clip))
|
||
|
{
|
||
|
AnimationCurve curve = AnimationUtility.GetEditorCurve(clip, binding);
|
||
|
|
||
|
if (!targetCurves.ContainsKey(binding.path))
|
||
|
{
|
||
|
TargetCurveSet curveSet = new TargetCurveSet();
|
||
|
curveSet.Init();
|
||
|
targetCurves.Add(binding.path, curveSet);
|
||
|
}
|
||
|
|
||
|
TargetCurveSet current = targetCurves[binding.path];
|
||
|
if (binding.propertyName.Contains("m_LocalPosition"))
|
||
|
{
|
||
|
if (binding.propertyName.Contains(".x"))
|
||
|
current.translationCurves[0] = curve;
|
||
|
else if (binding.propertyName.Contains(".y"))
|
||
|
current.translationCurves[1] = curve;
|
||
|
else if (binding.propertyName.Contains(".z"))
|
||
|
current.translationCurves[2] = curve;
|
||
|
}
|
||
|
else if (binding.propertyName.Contains("m_LocalScale"))
|
||
|
{
|
||
|
if (binding.propertyName.Contains(".x"))
|
||
|
current.scaleCurves[0] = curve;
|
||
|
else if (binding.propertyName.Contains(".y"))
|
||
|
current.scaleCurves[1] = curve;
|
||
|
else if (binding.propertyName.Contains(".z"))
|
||
|
current.scaleCurves[2] = curve;
|
||
|
}
|
||
|
else if (binding.propertyName.ToLower().Contains("localrotation"))
|
||
|
{
|
||
|
current.rotationType = ROTATION_TYPE.QUATERNION;
|
||
|
if (binding.propertyName.Contains(".x"))
|
||
|
current.rotationCurves[0] = curve;
|
||
|
else if (binding.propertyName.Contains(".y"))
|
||
|
current.rotationCurves[1] = curve;
|
||
|
else if (binding.propertyName.Contains(".z"))
|
||
|
current.rotationCurves[2] = curve;
|
||
|
else if (binding.propertyName.Contains(".w"))
|
||
|
current.rotationCurves[3] = curve;
|
||
|
}
|
||
|
// Takes into account 'localEuler', 'localEulerAnglesBaked' and 'localEulerAnglesRaw'
|
||
|
else if (binding.propertyName.ToLower().Contains("localeuler"))
|
||
|
{
|
||
|
current.rotationType = ROTATION_TYPE.EULER;
|
||
|
if (binding.propertyName.Contains(".x"))
|
||
|
current.rotationCurves[0] = curve;
|
||
|
else if (binding.propertyName.Contains(".y"))
|
||
|
current.rotationCurves[1] = curve;
|
||
|
else if (binding.propertyName.Contains(".z"))
|
||
|
current.rotationCurves[2] = curve;
|
||
|
}
|
||
|
targetCurves[binding.path] = current;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private void generateMissingCurves(float endTime, ref Transform tr, ref Dictionary<string, TargetCurveSet> targetCurvesBinding)
|
||
|
{
|
||
|
foreach (string target in targetCurvesBinding.Keys)
|
||
|
{
|
||
|
Transform targetTr = target.Length > 0 ? tr.Find(target) : tr;
|
||
|
if (targetTr == null)
|
||
|
continue;
|
||
|
|
||
|
TargetCurveSet current = targetCurvesBinding[target];
|
||
|
if (current.translationCurves[0] == null)
|
||
|
{
|
||
|
current.translationCurves[0] = createConstantCurve(targetTr.localPosition.x, endTime);
|
||
|
current.translationCurves[1] = createConstantCurve(targetTr.localPosition.y, endTime);
|
||
|
current.translationCurves[2] = createConstantCurve(targetTr.localPosition.z, endTime);
|
||
|
}
|
||
|
|
||
|
if (current.scaleCurves[0] == null)
|
||
|
{
|
||
|
current.scaleCurves[0] = createConstantCurve(targetTr.localScale.x, endTime);
|
||
|
current.scaleCurves[1] = createConstantCurve(targetTr.localScale.y, endTime);
|
||
|
current.scaleCurves[2] = createConstantCurve(targetTr.localScale.z, endTime);
|
||
|
}
|
||
|
|
||
|
if (current.rotationCurves[0] == null)
|
||
|
{
|
||
|
current.rotationCurves[0] = createConstantCurve(targetTr.localRotation.x, endTime);
|
||
|
current.rotationCurves[1] = createConstantCurve(targetTr.localRotation.y, endTime);
|
||
|
current.rotationCurves[2] = createConstantCurve(targetTr.localRotation.z, endTime);
|
||
|
current.rotationCurves[3] = createConstantCurve(targetTr.localRotation.w, endTime);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private AnimationCurve createConstantCurve(float value, float endTime)
|
||
|
{
|
||
|
// No translation curves, adding them
|
||
|
AnimationCurve curve = new AnimationCurve();
|
||
|
curve.AddKey(0, value);
|
||
|
curve.AddKey(endTime, value);
|
||
|
return curve;
|
||
|
}
|
||
|
|
||
|
private void bakeCurveSet(TargetCurveSet curveSet, float length, int bakingFramerate, ref float[] times, ref Vector3[] positions, ref Vector4[] rotations, ref Vector3[] scales)
|
||
|
{
|
||
|
int nbSamples = (int)(length * 30);
|
||
|
float deltaTime = length / nbSamples;
|
||
|
|
||
|
// Initialize Arrays
|
||
|
times = new float[nbSamples];
|
||
|
positions = new Vector3[nbSamples];
|
||
|
scales = new Vector3[nbSamples];
|
||
|
rotations = new Vector4[nbSamples];
|
||
|
|
||
|
// Assuming all the curves exist now
|
||
|
for (int i = 0; i < nbSamples; ++i)
|
||
|
{
|
||
|
float currentTime = i * deltaTime;
|
||
|
times[i] = currentTime;
|
||
|
positions[i] = new Vector3(curveSet.translationCurves[0].Evaluate(currentTime), curveSet.translationCurves[1].Evaluate(currentTime), curveSet.translationCurves[2].Evaluate(currentTime));
|
||
|
scales[i] = new Vector3(curveSet.scaleCurves[0].Evaluate(currentTime), curveSet.scaleCurves[1].Evaluate(currentTime), curveSet.scaleCurves[2].Evaluate(currentTime));
|
||
|
if (curveSet.rotationType == ROTATION_TYPE.EULER)
|
||
|
{
|
||
|
Quaternion eulerToQuat = Quaternion.Euler(curveSet.rotationCurves[0].Evaluate(currentTime), curveSet.rotationCurves[1].Evaluate(currentTime), curveSet.rotationCurves[2].Evaluate(currentTime));
|
||
|
rotations[i] = new Vector4(eulerToQuat.x, eulerToQuat.y, eulerToQuat.z, eulerToQuat.w);
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
rotations[i] = new Vector4(curveSet.rotationCurves[0].Evaluate(currentTime), curveSet.rotationCurves[1].Evaluate(currentTime), curveSet.rotationCurves[2].Evaluate(currentTime), curveSet.rotationCurves[3].Evaluate(currentTime));
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private void exportSkinFromNode(Transform transform)
|
||
|
{
|
||
|
PrimKey key = new PrimKey();
|
||
|
UnityEngine.Mesh mesh = getMesh(transform.gameObject);
|
||
|
key.Mesh = mesh;
|
||
|
key.Material = getMaterial(transform.gameObject);
|
||
|
MeshId val;
|
||
|
if(!_primOwner.TryGetValue(key, out val))
|
||
|
{
|
||
|
Debug.Log("No mesh found for skin");
|
||
|
return;
|
||
|
}
|
||
|
SkinnedMeshRenderer skin = transform.GetComponent<SkinnedMeshRenderer>();
|
||
|
GLTF.Schema.Skin gltfSkin = new Skin();
|
||
|
|
||
|
for (int i = 0; i < skin.bones.Length; ++i)
|
||
|
{
|
||
|
gltfSkin.Joints.Add(
|
||
|
new NodeId
|
||
|
{
|
||
|
Id = _exportedTransforms[skin.bones[i].GetInstanceID()],
|
||
|
Root = _root
|
||
|
});
|
||
|
}
|
||
|
|
||
|
gltfSkin.InverseBindMatrices = ExportAccessor(mesh.bindposes, true);
|
||
|
|
||
|
Vector4[] bones = boneWeightToBoneVec4(mesh.boneWeights);
|
||
|
Vector4[] weights = boneWeightToWeightVec4(mesh.boneWeights);
|
||
|
|
||
|
GLTF.Schema.Mesh gltfMesh = _root.Meshes[val.Id];
|
||
|
foreach(MeshPrimitive prim in gltfMesh.Primitives)
|
||
|
{
|
||
|
if(!prim.Attributes.ContainsKey("JOINTS_0"))
|
||
|
prim.Attributes.Add("JOINTS_0", ExportAccessorUint(bones));
|
||
|
if (!prim.Attributes.ContainsKey("WEIGHTS_0"))
|
||
|
prim.Attributes.Add("WEIGHTS_0", ExportAccessor(weights));
|
||
|
}
|
||
|
|
||
|
_root.Nodes[_exportedTransforms[transform.GetInstanceID()]].Skin = new SkinId() { Id = _root.Skins.Count, Root = _root };
|
||
|
_root.Skins.Add(gltfSkin);
|
||
|
}
|
||
|
|
||
|
private Vector4[] boneWeightToBoneVec4(BoneWeight[] bw)
|
||
|
{
|
||
|
Vector4[] bones = new Vector4[bw.Length];
|
||
|
for (int i = 0; i < bw.Length; ++i)
|
||
|
{
|
||
|
bones[i] = new Vector4(bw[i].boneIndex0, bw[i].boneIndex1, bw[i].boneIndex2, bw[i].boneIndex3);
|
||
|
}
|
||
|
|
||
|
return bones;
|
||
|
}
|
||
|
|
||
|
private Vector4[] boneWeightToWeightVec4(BoneWeight[] bw)
|
||
|
{
|
||
|
Vector4[] weights = new Vector4[bw.Length];
|
||
|
for (int i = 0; i < bw.Length; ++i)
|
||
|
{
|
||
|
weights[i] = new Vector4(bw[i].weight0, bw[i].weight1, bw[i].weight2, bw[i].weight3);
|
||
|
}
|
||
|
|
||
|
return weights;
|
||
|
}
|
||
|
|
||
|
public static Regex rgx = new Regex("[^a-zA-Z0-9 -_.]");
|
||
|
|
||
|
static public string cleanNonAlphanumeric(string s)
|
||
|
{
|
||
|
return rgx.Replace(s, "");
|
||
|
}
|
||
|
}
|
||
|
}
|