initial game submission
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
#if UNITY_EDITOR
|
||||
using UnityEditor;
|
||||
#endif
|
||||
|
||||
namespace UnityGLTF
|
||||
{
|
||||
[Serializable]
|
||||
public struct BlendShapeFrameWeightSetting
|
||||
{
|
||||
public enum MultiplierOption
|
||||
{
|
||||
Multiplier1 = 0,
|
||||
Multiplier100 = 1,
|
||||
Custom = 2
|
||||
}
|
||||
|
||||
[SerializeField]
|
||||
internal MultiplierOption _option;
|
||||
|
||||
[SerializeField]
|
||||
internal float _multiplier;
|
||||
|
||||
public BlendShapeFrameWeightSetting(MultiplierOption option)
|
||||
{
|
||||
_option = option;
|
||||
_multiplier = 1;
|
||||
}
|
||||
|
||||
public BlendShapeFrameWeightSetting(float multiplier)
|
||||
{
|
||||
_option = MultiplierOption.Custom;
|
||||
_multiplier = multiplier;
|
||||
}
|
||||
|
||||
public float Multiplier
|
||||
{
|
||||
get
|
||||
{
|
||||
switch (_option)
|
||||
{
|
||||
case MultiplierOption.Multiplier1:
|
||||
return 1f;
|
||||
case MultiplierOption.Multiplier100:
|
||||
return 100f;
|
||||
case MultiplierOption.Custom:
|
||||
return _multiplier;
|
||||
default:
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
set
|
||||
{
|
||||
_option = MultiplierOption.Custom;
|
||||
_multiplier = value;
|
||||
}
|
||||
}
|
||||
|
||||
public static implicit operator float(BlendShapeFrameWeightSetting weightSetting)
|
||||
{
|
||||
return weightSetting.Multiplier;
|
||||
}
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
|
||||
[CustomPropertyDrawer(typeof(BlendShapeFrameWeightSetting))]
|
||||
public class BlendShapeFrameWeightMultiplierSettingDrawer : PropertyDrawer
|
||||
{
|
||||
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
|
||||
{
|
||||
var option = property.FindPropertyRelative(nameof(BlendShapeFrameWeightSetting._option));
|
||||
var customValue = property.FindPropertyRelative(nameof(BlendShapeFrameWeightSetting._multiplier));
|
||||
|
||||
EditorGUI.BeginProperty(position, label, property);
|
||||
{
|
||||
position.height = EditorGUIUtility.singleLineHeight;
|
||||
|
||||
var newBlendShapeFrameWeightSettingIndex = EditorGUI.Popup(position, label.text, option.enumValueIndex, new[] {"1", "100", "Custom"});
|
||||
if (newBlendShapeFrameWeightSettingIndex != option.enumValueIndex)
|
||||
{
|
||||
option.enumValueIndex = newBlendShapeFrameWeightSettingIndex;
|
||||
}
|
||||
|
||||
if (newBlendShapeFrameWeightSettingIndex == 2)
|
||||
{
|
||||
position.y += EditorGUIUtility.singleLineHeight;
|
||||
EditorGUI.indentLevel++;
|
||||
customValue.floatValue = EditorGUI.FloatField(position, new GUIContent("Custom", "Custom value used as multiplier for Blend Shape frame weights on import."), customValue.floatValue);
|
||||
EditorGUI.indentLevel--;
|
||||
}
|
||||
}
|
||||
EditorGUI.EndProperty();
|
||||
}
|
||||
|
||||
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
|
||||
{
|
||||
var option = property.FindPropertyRelative(nameof(BlendShapeFrameWeightSetting._option));
|
||||
if (option.enumValueIndex == 2)
|
||||
{
|
||||
return EditorGUIUtility.singleLineHeight * 2f;
|
||||
}
|
||||
return EditorGUIUtility.singleLineHeight;
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9df89f3f8d3645f3b4150e95b864974d
|
||||
timeCreated: 1706174663
|
||||
@@ -0,0 +1,11 @@
|
||||
#if UNITY_2017_1_OR_NEWER
|
||||
namespace UnityGLTF
|
||||
{
|
||||
public enum GLTFImporterNormals
|
||||
{
|
||||
Import,
|
||||
Calculate,
|
||||
None
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f3961a7b54dd64330a1228d3aa9993c0
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,615 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using GLTF;
|
||||
using GLTF.Schema;
|
||||
using GLTF.Schema.KHR_lights_punctual;
|
||||
using GLTF.Utilities;
|
||||
using UnityEngine;
|
||||
using UnityGLTF.Cache;
|
||||
using UnityGLTF.Extensions;
|
||||
using UnityGLTF.Plugins;
|
||||
|
||||
namespace UnityGLTF
|
||||
{
|
||||
public partial class GLTFSceneImporter
|
||||
{
|
||||
#if UNITY_ANIMATION || !UNITY_2019_1_OR_NEWER
|
||||
private static string RelativePathFrom(Transform self, Transform root)
|
||||
{
|
||||
var path = new List<String>();
|
||||
for (var current = self; current != null; current = current.parent)
|
||||
{
|
||||
if (current == root)
|
||||
{
|
||||
return String.Join("/", path.ToArray());
|
||||
}
|
||||
|
||||
path.Insert(0, current.name);
|
||||
}
|
||||
|
||||
throw new Exception("no RelativePath");
|
||||
}
|
||||
|
||||
protected virtual async Task BuildAnimationSamplers(GLTFAnimation animation, int animationId)
|
||||
{
|
||||
// look up expected data types
|
||||
var typeMap = new Dictionary<int, string>();
|
||||
foreach (var channel in animation.Channels)
|
||||
{
|
||||
typeMap[channel.Sampler.Id] = channel.Target.Path.ToString();
|
||||
}
|
||||
|
||||
var samplers = _assetCache.AnimationCache[animationId].Samplers;
|
||||
var samplersByType = new Dictionary<string, List<AttributeAccessor>>
|
||||
{
|
||||
{"time", new List<AttributeAccessor>(animation.Samplers.Count)}
|
||||
};
|
||||
|
||||
for (var i = 0; i < animation.Samplers.Count; i++)
|
||||
{
|
||||
// no sense generating unused samplers
|
||||
if (!typeMap.ContainsKey(i))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var samplerDef = animation.Samplers[i];
|
||||
|
||||
samplers[i].Interpolation = samplerDef.Interpolation;
|
||||
|
||||
// set up input accessors
|
||||
BufferCacheData inputBufferCacheData = await GetBufferData(samplerDef.Input.Value.BufferView.Value.Buffer);
|
||||
AttributeAccessor attributeAccessor = new AttributeAccessor
|
||||
{
|
||||
AccessorId = samplerDef.Input,
|
||||
bufferData = inputBufferCacheData.bufferData,
|
||||
Offset = inputBufferCacheData.ChunkOffset
|
||||
};
|
||||
|
||||
samplers[i].Input = attributeAccessor;
|
||||
samplersByType["time"].Add(attributeAccessor);
|
||||
|
||||
// set up output accessors
|
||||
BufferCacheData outputBufferCacheData = await GetBufferData(samplerDef.Output.Value.BufferView.Value.Buffer);
|
||||
attributeAccessor = new AttributeAccessor
|
||||
{
|
||||
AccessorId = samplerDef.Output,
|
||||
bufferData = outputBufferCacheData.bufferData,
|
||||
Offset = outputBufferCacheData.ChunkOffset
|
||||
};
|
||||
|
||||
samplers[i].Output = attributeAccessor;
|
||||
|
||||
if (!samplersByType.ContainsKey(typeMap[i]))
|
||||
{
|
||||
samplersByType[typeMap[i]] = new List<AttributeAccessor>();
|
||||
}
|
||||
|
||||
samplersByType[typeMap[i]].Add(attributeAccessor);
|
||||
}
|
||||
|
||||
// populate attributeAccessors with buffer data
|
||||
GLTFHelpers.BuildAnimationSamplers(ref samplersByType);
|
||||
}
|
||||
|
||||
protected void SetAnimationCurve(
|
||||
AnimationClip clip,
|
||||
string relativePath,
|
||||
string[] propertyNames,
|
||||
NumericArray input,
|
||||
NumericArray output,
|
||||
InterpolationType mode,
|
||||
Type curveType,
|
||||
ValuesConvertion getConvertedValues)
|
||||
{
|
||||
var channelCount = propertyNames.Length;
|
||||
var frameCount = input.AsFloats.Length;
|
||||
|
||||
// copy all the key frame data to cache
|
||||
Keyframe[][] keyframes = new Keyframe[channelCount][];
|
||||
for (var ci = 0; ci < channelCount; ++ci)
|
||||
{
|
||||
keyframes[ci] = new Keyframe[frameCount];
|
||||
}
|
||||
|
||||
for (var i = 0; i < frameCount; ++i)
|
||||
{
|
||||
var time = input.AsFloats[i];
|
||||
|
||||
float[] values = null;
|
||||
float[] inTangents = null;
|
||||
float[] outTangents = null;
|
||||
if (mode == InterpolationType.CUBICSPLINE)
|
||||
{
|
||||
// For cubic spline, the output will contain 3 values per keyframe; inTangent, dataPoint, and outTangent.
|
||||
// https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#appendix-c-spline-interpolation
|
||||
|
||||
var cubicIndex = i * 3;
|
||||
inTangents = getConvertedValues(output, cubicIndex);
|
||||
values = getConvertedValues(output, cubicIndex + 1);
|
||||
outTangents = getConvertedValues(output, cubicIndex + 2);
|
||||
}
|
||||
else
|
||||
{
|
||||
// For other interpolation types, the output will only contain one value per keyframe
|
||||
values = getConvertedValues(output, i);
|
||||
}
|
||||
|
||||
for (var ci = 0; ci < channelCount; ++ci)
|
||||
{
|
||||
if (mode == InterpolationType.CUBICSPLINE)
|
||||
{
|
||||
keyframes[ci][i] = new Keyframe(time, values[ci], inTangents[ci], outTangents[ci]);
|
||||
}
|
||||
else
|
||||
{
|
||||
keyframes[ci][i] = new Keyframe(time, values[ci]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (mode == InterpolationType.LINEAR && channelCount == 4 && propertyNames.All(p => p == "localRotation.x" || p == "localRotation.y" || p == "localRotation.z" || p == "localRotation.w"))
|
||||
{
|
||||
Quaternion prev = Quaternion.identity;
|
||||
for (int i = 0; i < keyframes[0].Length; i++)
|
||||
{
|
||||
Quaternion q = new Quaternion(keyframes[0][i].value, keyframes[1][i].value, keyframes[2][i].value, keyframes[3][i].value);
|
||||
if (i > 0)
|
||||
{
|
||||
if (Quaternion.Dot(prev, q) < 0)
|
||||
q = new Quaternion(-q.x, -q.y, -q.z, -q.w);
|
||||
|
||||
keyframes[0][i].value = q.x;
|
||||
keyframes[1][i].value = q.y;
|
||||
keyframes[2][i].value = q.z;
|
||||
keyframes[3][i].value = q.w;
|
||||
}
|
||||
prev = q;
|
||||
}
|
||||
}
|
||||
|
||||
for (var ci = 0; ci < channelCount; ci++)
|
||||
{
|
||||
if (mode != InterpolationType.CUBICSPLINE)
|
||||
{
|
||||
// For cubic spline interpolation, the inTangents and outTangents are already explicitly defined.
|
||||
// For the rest, set them appropriately.
|
||||
for (var i = 0; i < keyframes[ci].Length; i++)
|
||||
{
|
||||
SetTangentMode(keyframes[ci], i, mode);
|
||||
}
|
||||
}
|
||||
|
||||
// copy all key frames data to animation curve and add it to the clip
|
||||
AnimationCurve curve = new AnimationCurve(keyframes[ci]);
|
||||
|
||||
#if !UNITY_EDITOR
|
||||
// Only in editor SetCurve works with non-legacy clips
|
||||
if (clip.legacy)
|
||||
#endif
|
||||
clip.SetCurve(relativePath, curveType, propertyNames[ci], curve);
|
||||
}
|
||||
}
|
||||
|
||||
private void SetTangentMode(Keyframe[] keyframes, int keyframeIndex, InterpolationType interpolation)
|
||||
{
|
||||
var key = keyframes[keyframeIndex];
|
||||
|
||||
switch (interpolation)
|
||||
{
|
||||
case InterpolationType.CATMULLROMSPLINE:
|
||||
key.inTangent = 0;
|
||||
key.outTangent = 0;
|
||||
break;
|
||||
case InterpolationType.LINEAR:
|
||||
key.inTangent = GetCurveKeyframeLeftLinearSlope(keyframes, keyframeIndex, ref AnyAnimationTimeNotIncreasing);
|
||||
key.outTangent = GetCurveKeyframeLeftLinearSlope(keyframes, keyframeIndex + 1, ref AnyAnimationTimeNotIncreasing);
|
||||
break;
|
||||
case InterpolationType.STEP:
|
||||
key.inTangent = float.PositiveInfinity;
|
||||
key.outTangent = 0;
|
||||
break;
|
||||
default:
|
||||
throw new NotImplementedException($"Unknown interpolation type for animation (File: {_gltfFileName})");
|
||||
}
|
||||
keyframes[keyframeIndex] = key;
|
||||
}
|
||||
|
||||
private static float GetCurveKeyframeLeftLinearSlope(Keyframe[] keyframes, int keyframeIndex, ref bool anyNonCreasing)
|
||||
{
|
||||
if (keyframeIndex <= 0 || keyframeIndex >= keyframes.Length)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var valueDelta = keyframes[keyframeIndex].value - keyframes[keyframeIndex - 1].value;
|
||||
var timeDelta = keyframes[keyframeIndex].time - keyframes[keyframeIndex - 1].time;
|
||||
|
||||
if (timeDelta <= 0)
|
||||
{
|
||||
var k = keyframes[keyframeIndex];
|
||||
k.time = keyframes[keyframeIndex - 1].time + Mathf.Epsilon + 1 / 100f;
|
||||
keyframes[keyframeIndex] = k;
|
||||
anyNonCreasing = true;
|
||||
return float.PositiveInfinity;
|
||||
}
|
||||
return valueDelta / timeDelta;
|
||||
}
|
||||
|
||||
protected async Task<AnimationClip> ConstructClip(Transform root, int animationId, CancellationToken cancellationToken)
|
||||
{
|
||||
GLTFAnimation animation = _gltfRoot.Animations[animationId];
|
||||
|
||||
AnimationCacheData animationCache = _assetCache.AnimationCache[animationId];
|
||||
if (animationCache == null)
|
||||
{
|
||||
animationCache = new AnimationCacheData(animation.Samplers.Count);
|
||||
_assetCache.AnimationCache[animationId] = animationCache;
|
||||
}
|
||||
else if (animationCache.LoadedAnimationClip != null)
|
||||
{
|
||||
return animationCache.LoadedAnimationClip;
|
||||
}
|
||||
|
||||
// unpack accessors
|
||||
await BuildAnimationSamplers(animation, animationId);
|
||||
|
||||
// init clip
|
||||
AnimationClip clip = new AnimationClip
|
||||
{
|
||||
name = animation.Name ?? $"animation:{animationId}",
|
||||
};
|
||||
_assetCache.AnimationCache[animationId].LoadedAnimationClip = clip;
|
||||
|
||||
if (_options.AnimationMethod == AnimationMethod.Legacy)
|
||||
clip.legacy = true;
|
||||
|
||||
int[] nodeIds = new int[0];
|
||||
|
||||
AnimationPointerImportContext pointerImportContext = null;
|
||||
|
||||
AttributeAccessor FindSecondaryChannel(string animationPointerPath)
|
||||
{
|
||||
foreach (AnimationChannel secondAnimationChannel in animation.Channels)
|
||||
{
|
||||
if (secondAnimationChannel.Target.Extensions == null ||
|
||||
!secondAnimationChannel.Target.Extensions.TryGetValue(KHR_animation_pointer.EXTENSION_NAME,
|
||||
out IExtension secondaryExt))
|
||||
continue;
|
||||
if (secondaryExt is KHR_animation_pointer secondaryPointer)
|
||||
{
|
||||
AnimationSamplerCacheData secondarySamplerCache =
|
||||
animationCache.Samplers[secondAnimationChannel.Sampler.Id];
|
||||
if (secondaryPointer.path == animationPointerPath)
|
||||
{
|
||||
return secondarySamplerCache.Output;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach (AnimationChannel channel in animation.Channels)
|
||||
{
|
||||
bool usesPointer = false;
|
||||
IExtension pointerExtension = null;
|
||||
AnimationSamplerCacheData samplerCache = animationCache.Samplers[channel.Sampler.Id];
|
||||
if (channel.Target.Extensions != null && channel.Target.Extensions.TryGetValue(
|
||||
KHR_animation_pointer.EXTENSION_NAME,
|
||||
out pointerExtension))
|
||||
{
|
||||
if (Context.TryGetPlugin(out pointerImportContext))
|
||||
usesPointer = true;
|
||||
}
|
||||
|
||||
if (!usesPointer && channel.Target.Node == null)
|
||||
{
|
||||
// If a channel doesn't have a target node, then just skip it.
|
||||
// This is legal and is present in one of the asset generator models, but means that animation doesn't actually do anything.
|
||||
// https://github.com/KhronosGroup/glTF-Asset-Generator/tree/master/Output/Positive/Animation_NodeMisc
|
||||
// Model 08
|
||||
continue;
|
||||
}
|
||||
|
||||
string relativePath = null;
|
||||
AnimationPointerData pointerData = null;
|
||||
string[] propertyNames;
|
||||
GLTFAnimationChannelPath path = GLTFAnimationChannelPath.translation;
|
||||
|
||||
if (usesPointer && pointerExtension != null)
|
||||
{
|
||||
KHR_animation_pointer pointer = pointerExtension as KHR_animation_pointer;
|
||||
if (pointer == null || pointer.path == null)
|
||||
continue;
|
||||
|
||||
path = GLTFAnimationChannelPath.pointer;
|
||||
relativePath = pointer.path;
|
||||
var pointerHierarchy = new PointerPath(relativePath);
|
||||
if (!pointerHierarchy.isValid)
|
||||
continue;
|
||||
|
||||
switch (pointerHierarchy.PathElementType)
|
||||
{
|
||||
case PointerPath.PathElement.RootExtension:
|
||||
if (!_gltfRoot.Extensions.TryGetValue(pointerHierarchy.elementName, out IExtension hierarchyExtension))
|
||||
continue;
|
||||
|
||||
// Check if the extension support animation pointers
|
||||
if (hierarchyExtension is IImportAnimationPointerRootExtension rootExtension)
|
||||
{
|
||||
// Let the extension handle the pointer data and create the nodeIds and unity properties
|
||||
if (rootExtension.TryGetImportAnimationPointerData(_gltfRoot, pointerHierarchy, out pointerData))
|
||||
nodeIds = pointerData.targetNodeIds;
|
||||
else
|
||||
continue;
|
||||
}
|
||||
pointerData.primaryData = samplerCache.Output;
|
||||
break;
|
||||
case PointerPath.PathElement.Root:
|
||||
var rootType = pointerHierarchy.elementName;
|
||||
var rootIndex = pointerHierarchy.FindNext(PointerPath.PathElement.Index);
|
||||
if (rootIndex == null)
|
||||
continue;
|
||||
|
||||
switch (rootType)
|
||||
{
|
||||
case "nodes":
|
||||
var pointerPropertyElement = pointerHierarchy.FindNext(PointerPath.PathElement.Property);
|
||||
if (pointerPropertyElement == null)
|
||||
continue;
|
||||
|
||||
pointerData = new AnimationPointerData();
|
||||
pointerData.targetNodeIds = new int[] {rootIndex.index};
|
||||
nodeIds = pointerData.targetNodeIds;
|
||||
|
||||
// Convert translate, scale, rotation from pointer path to to GLTFAnimationChannelPath, so we can use the same code path as the non-animation-pointer channels
|
||||
if (!GLTFAnimationChannelPath.TryParse(pointerPropertyElement.elementName, out path))
|
||||
continue;
|
||||
|
||||
break;
|
||||
case "materials":
|
||||
nodeIds = _gltfRoot.GetAllNodeIdsWithMaterialId(rootIndex.index);
|
||||
if (nodeIds.Length == 0)
|
||||
continue;
|
||||
var materialPath = pointerHierarchy.FindNext(PointerPath.PathElement.Index).next;
|
||||
if (materialPath == null)
|
||||
continue;
|
||||
|
||||
var gltfPropertyPath = materialPath.ExtractPath();
|
||||
var mat = _assetCache.MaterialCache[rootIndex.index];
|
||||
if (!mat.UnityMaterial)
|
||||
continue;
|
||||
|
||||
if (!AnimationPointerHelpers.BuildImportMaterialAnimationPointerData(pointer.path, pointerImportContext.materialPropertiesRemapper, mat.UnityMaterial, gltfPropertyPath, samplerCache.Output, out pointerData))
|
||||
continue;
|
||||
pointerData.targetNodeIds = nodeIds;
|
||||
if (string.IsNullOrEmpty(pointerData.primaryPath))
|
||||
{
|
||||
pointerData.primaryPath = "/" + pointerHierarchy.ExtractPath().Replace(rootIndex.next.ExtractPath(), pointerData.primaryProperty);
|
||||
pointerData.primaryData = FindSecondaryChannel(pointerData.primaryPath);
|
||||
if (pointerData.primaryData != null)
|
||||
{
|
||||
//cancel here and process this combined property later when we found first the Primary Property
|
||||
continue;
|
||||
}
|
||||
}
|
||||
else
|
||||
if (!string.IsNullOrEmpty(pointerData.secondaryProperty))
|
||||
{
|
||||
// When an property has potentially a second Sampler, we need to find it. e.g. like EmissionFactor and EmissionStrength
|
||||
|
||||
pointerData.secondaryPath = "/" + pointerHierarchy.ExtractPath().Replace(rootIndex.next.ExtractPath(), pointerData.secondaryProperty);
|
||||
pointerData.secondaryData = FindSecondaryChannel(pointerData.secondaryPath);
|
||||
}
|
||||
break;
|
||||
case "cameras":
|
||||
int cameraId = rootIndex.index;
|
||||
pointerData = new AnimationPointerData();
|
||||
pointerData.targetType = typeof(Camera);
|
||||
pointerData.primaryData = samplerCache.Output;
|
||||
|
||||
string gltfCameraPropertyPath = rootIndex.next.ExtractPath();
|
||||
switch (gltfCameraPropertyPath)
|
||||
{
|
||||
case "orthographic/ymag":
|
||||
pointerData.secondaryPath = $"/{pointerHierarchy.elementName}/{rootIndex.index.ToString()}/orthographic/xmag";
|
||||
pointerData.unityPropertyNames = new string[] { "orthographic size" };
|
||||
pointerData.secondaryData = FindSecondaryChannel(pointerData.secondaryPath);
|
||||
pointerData.importAccessorContentConversion = (data, frame) =>
|
||||
{
|
||||
var xmag = data.secondaryData.AccessorContent.AsFloats[frame];
|
||||
var ymag = data.primaryData.AccessorContent.AsFloats[frame];
|
||||
return new float[] {Mathf.Max(xmag, ymag)};
|
||||
};
|
||||
break;
|
||||
case "orthographic/xmag":
|
||||
continue;
|
||||
case "perspective/znear":
|
||||
case "orthographic/znear":
|
||||
pointerData.unityPropertyNames = new string[] { "near clip plane" };
|
||||
pointerData.importAccessorContentConversion = (data, frame) =>
|
||||
new float[] {data.primaryData.AccessorContent.AsFloats[frame]};
|
||||
break;
|
||||
case "perspective/zfar":
|
||||
case "orthographic/zfar":
|
||||
pointerData.unityPropertyNames = new string[] { "far clip plane" };
|
||||
pointerData.importAccessorContentConversion = (data, frame) =>
|
||||
new float[] {data.primaryData.AccessorContent.AsFloats[frame]};
|
||||
break;
|
||||
case "perspective/yfov":
|
||||
pointerData.unityPropertyNames = new string[] { "field of view" };
|
||||
pointerData.importAccessorContentConversion = (data, frame) =>
|
||||
{
|
||||
var fov = data.primaryData.AccessorContent.AsFloats[frame] * Mathf.Rad2Deg;
|
||||
return new float[] {fov};
|
||||
};
|
||||
break;
|
||||
case "backgroundColor":
|
||||
pointerData.unityPropertyNames = new string[] { "background color.r", "background color.g", "background color.b", "background color.a" };
|
||||
pointerData.importAccessorContentConversion = (data, frame) =>
|
||||
{
|
||||
var color = data.primaryData.AccessorContent.AsFloat4s[frame].ToUnityColorRaw();
|
||||
return new float[] {color.r, color.g, color.b, color.a};
|
||||
};
|
||||
break;
|
||||
default:
|
||||
Debug.Log(LogType.Warning, "Unknown property name on Camera " + cameraId.ToString() + ": " + gltfCameraPropertyPath);
|
||||
break;
|
||||
}
|
||||
|
||||
nodeIds = _gltfRoot.GetAllNodeIdsWithCameraId(cameraId);
|
||||
break;
|
||||
default:
|
||||
continue;
|
||||
//throw new NotImplementedException();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
continue;
|
||||
}
|
||||
|
||||
if (pointerData == null)
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (channel.Target == null || channel.Target.Node == null)
|
||||
continue;
|
||||
nodeIds = new int[] {channel.Target.Node.Id};
|
||||
}
|
||||
|
||||
// In case an animated material are referenced from many nodes, whe need to create a curve for each node. (e.g. Materials)
|
||||
foreach (var nodeId in nodeIds)
|
||||
{
|
||||
var node = await GetNode(nodeId, cancellationToken);
|
||||
var targetNode = _gltfRoot.Nodes[nodeId];
|
||||
relativePath = RelativePathFrom(node.transform, root);
|
||||
NumericArray input = samplerCache.Input.AccessorContent;
|
||||
NumericArray output = samplerCache.Output.AccessorContent;
|
||||
|
||||
if (!usesPointer)
|
||||
{
|
||||
var known = Enum.TryParse(channel.Target.Path, out path);
|
||||
if (!known) continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (pointerData.targetType == null)
|
||||
pointerData.targetType = targetNode.Skin != null
|
||||
? typeof(SkinnedMeshRenderer)
|
||||
: typeof(MeshRenderer);
|
||||
}
|
||||
|
||||
switch (path)
|
||||
{
|
||||
case GLTFAnimationChannelPath.pointer:
|
||||
if (pointerData.importAccessorContentConversion == null)
|
||||
continue;
|
||||
SetAnimationCurve(clip, relativePath, pointerData.unityPropertyNames, input, output,
|
||||
samplerCache.Interpolation, pointerData.targetType,
|
||||
(data, frame) => pointerData.importAccessorContentConversion(pointerData, frame));
|
||||
break;
|
||||
case GLTFAnimationChannelPath.translation:
|
||||
propertyNames = new string[] { "localPosition.x", "localPosition.y", "localPosition.z" };
|
||||
#if UNITY_EDITOR
|
||||
// TODO technically this should be postprocessing in the ScriptedImporter instead,
|
||||
// but performance is much better if we do it when constructing the clips
|
||||
var factor = Context?.ImportScaleFactor ?? 1f;
|
||||
#endif
|
||||
SetAnimationCurve(clip, relativePath, propertyNames, input, output,
|
||||
samplerCache.Interpolation, typeof(Transform),
|
||||
(data, frame) =>
|
||||
{
|
||||
var position = data.AsFloat3s[frame].ToUnityVector3Convert();
|
||||
#if UNITY_EDITOR
|
||||
return new float[]
|
||||
{ position.x * factor, position.y * factor, position.z * factor };
|
||||
#else
|
||||
return new float[] { position.x, position.y, position.z };
|
||||
#endif
|
||||
});
|
||||
break;
|
||||
|
||||
case GLTFAnimationChannelPath.rotation:
|
||||
propertyNames = new string[]
|
||||
{ "localRotation.x", "localRotation.y", "localRotation.z", "localRotation.w" };
|
||||
bool flipRotation = (targetNode.Extensions != null
|
||||
&& targetNode.Extensions.ContainsKey(KHR_lights_punctualExtensionFactory.EXTENSION_NAME)
|
||||
&& Context.TryGetPlugin<LightsPunctualImportContext>(out _));
|
||||
SetAnimationCurve(clip, relativePath, propertyNames, input, output,
|
||||
samplerCache.Interpolation, typeof(Transform),
|
||||
(data, frame) =>
|
||||
{
|
||||
var rotation = data.AsFloat4s[frame];
|
||||
var quaternion = rotation.ToUnityQuaternionConvert();
|
||||
if (flipRotation)
|
||||
quaternion *= Quaternion.Euler(0, 180, 0);
|
||||
return new float[] { quaternion.x, quaternion.y, quaternion.z, quaternion.w };
|
||||
});
|
||||
break;
|
||||
case GLTFAnimationChannelPath.scale:
|
||||
propertyNames = new string[] { "localScale.x", "localScale.y", "localScale.z" };
|
||||
|
||||
SetAnimationCurve(clip, relativePath, propertyNames, input, output,
|
||||
samplerCache.Interpolation, typeof(Transform),
|
||||
(data, frame) =>
|
||||
{
|
||||
var scale = data.AsFloat3s[frame].ToUnityVector3Raw();
|
||||
return new float[] { scale.x, scale.y, scale.z };
|
||||
});
|
||||
break;
|
||||
|
||||
case GLTFAnimationChannelPath.weights:
|
||||
var mesh = targetNode.Mesh.Value;
|
||||
var primitives = mesh.Primitives;
|
||||
if (primitives[0].Targets == null) break;
|
||||
var targetCount = primitives[0].Targets.Count;
|
||||
for (int primitiveIndex = 0; primitiveIndex < primitives.Count; primitiveIndex++)
|
||||
{
|
||||
// see SceneImporter:156
|
||||
// blend shapes are always called "Morphtarget"
|
||||
var targetNames = mesh.TargetNames;
|
||||
propertyNames = new string[targetCount];
|
||||
for (var i = 0; i < targetCount; i++)
|
||||
propertyNames[i] = _options.ImportBlendShapeNames
|
||||
? ("blendShape." + ((targetNames != null && targetNames.Count > i)
|
||||
? targetNames[i]
|
||||
: ("Morphtarget" + i)))
|
||||
: "blendShape." + i.ToString();
|
||||
var frameFloats = new float[targetCount];
|
||||
|
||||
var blendShapeFrameWeight = _options.BlendShapeFrameWeight;
|
||||
SetAnimationCurve(clip, relativePath, propertyNames, input, output,
|
||||
samplerCache.Interpolation, typeof(SkinnedMeshRenderer),
|
||||
(data, frame) =>
|
||||
{
|
||||
var allValues = data.AsFloats;
|
||||
for (var k = 0; k < targetCount; k++)
|
||||
frameFloats[k] = allValues[frame * targetCount + k] * blendShapeFrameWeight;
|
||||
|
||||
return frameFloats;
|
||||
});
|
||||
}
|
||||
break;
|
||||
default:
|
||||
Debug.Log(LogType.Warning, $"Cannot read GLTF animation path (File: {_gltfFileName})");
|
||||
break;
|
||||
} // switch target type
|
||||
} // foreach nodeIds
|
||||
|
||||
await YieldOnTimeoutAndThrowOnLowMemory();
|
||||
} // foreach channel
|
||||
|
||||
// EnsureQuaternionContinuity results in unwanted tangents on the first and last keyframes > custom Solution in SetAnimationCurve
|
||||
//clip.EnsureQuaternionContinuity();
|
||||
return clip;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 20eade7af2db4b06a0d23b23c3a83c47
|
||||
timeCreated: 1657012127
|
||||
@@ -0,0 +1,46 @@
|
||||
using GLTF.Schema;
|
||||
using UnityEngine;
|
||||
using UnityGLTF.Extensions;
|
||||
|
||||
namespace UnityGLTF
|
||||
{
|
||||
public partial class GLTFSceneImporter
|
||||
{
|
||||
private bool ConstructCamera(GameObject nodeObj, Node node)
|
||||
{
|
||||
if (node.Camera == null)
|
||||
return false;
|
||||
|
||||
if (_options.CameraImport == CameraImportOption.None)
|
||||
return false;
|
||||
|
||||
var camera = node.Camera.Value;
|
||||
Camera unityCamera = null;
|
||||
if (camera.Orthographic != null)
|
||||
{
|
||||
unityCamera = nodeObj.AddComponent<Camera>();
|
||||
unityCamera.orthographic = true;
|
||||
unityCamera.orthographicSize = Mathf.Max((float)camera.Orthographic.XMag, (float)camera.Orthographic.YMag);
|
||||
unityCamera.farClipPlane = (float)camera.Orthographic.ZFar;
|
||||
unityCamera.nearClipPlane = (float)camera.Orthographic.ZNear;
|
||||
}
|
||||
else if (camera.Perspective != null)
|
||||
{
|
||||
unityCamera = nodeObj.AddComponent<Camera>();
|
||||
unityCamera.orthographic = false;
|
||||
unityCamera.fieldOfView = (float)camera.Perspective.YFov * Mathf.Rad2Deg;
|
||||
unityCamera.farClipPlane = (float)camera.Perspective.ZFar;
|
||||
unityCamera.nearClipPlane = (float)camera.Perspective.ZNear;
|
||||
}
|
||||
|
||||
if (!unityCamera)
|
||||
return false;
|
||||
|
||||
if (_options.CameraImport == CameraImportOption.ImportAndCameraDisabled)
|
||||
unityCamera.enabled = false;
|
||||
|
||||
nodeObj.transform.localRotation *= SchemaExtensions.InvertDirection;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4bce81583cdc4b5b831072f00b368b09
|
||||
timeCreated: 1713049096
|
||||
@@ -0,0 +1,18 @@
|
||||
using UnityEngine;
|
||||
using UnityGLTF.Loader;
|
||||
|
||||
namespace UnityGLTF
|
||||
{
|
||||
public abstract class ImporterFactory : ScriptableObject
|
||||
{
|
||||
public abstract GLTFSceneImporter CreateSceneImporter(string gltfFileName, ImportOptions options);
|
||||
}
|
||||
|
||||
public class DefaultImporterFactory : ImporterFactory
|
||||
{
|
||||
public override GLTFSceneImporter CreateSceneImporter(string gltfFileName, ImportOptions options)
|
||||
{
|
||||
return new GLTFSceneImporter(gltfFileName, options);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 26465c5482163224486cdb865e3a0838
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,96 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using GLTF.Schema;
|
||||
using UnityEngine;
|
||||
using UnityGLTF.Plugins;
|
||||
|
||||
namespace UnityGLTF
|
||||
{
|
||||
public partial class GLTFSceneImporter
|
||||
{
|
||||
private async Task ConstructLods(GLTFRoot gltfRoot, GameObject nodeObj, Node node, int nodeIndex, CancellationToken cancellationToken)
|
||||
{
|
||||
if (!Context.TryGetPlugin<LodsImportContext>(out _)) return;
|
||||
|
||||
const string msft_LODExtName = MSFT_LODExtensionFactory.EXTENSION_NAME;
|
||||
MSFT_LODExtension lodsExtension = null;
|
||||
if (_gltfRoot.ExtensionsUsed != null
|
||||
&& _gltfRoot.ExtensionsUsed.Contains(msft_LODExtName)
|
||||
&& node.Extensions != null
|
||||
&& node.Extensions.ContainsKey(msft_LODExtName))
|
||||
{
|
||||
lodsExtension = node.Extensions[msft_LODExtName] as MSFT_LODExtension;
|
||||
if (lodsExtension != null && lodsExtension.NodeIds.Count > 0)
|
||||
{
|
||||
int lodCount = lodsExtension.NodeIds.Count + 1;
|
||||
if (CullFarLOD)
|
||||
{
|
||||
lodCount += 1;
|
||||
}
|
||||
LOD[] lods = new LOD[lodCount];
|
||||
List<double> lodCoverage = lodsExtension.GetLODCoverage(node);
|
||||
if (lodCoverage == null)
|
||||
lodCoverage = new List<double>();
|
||||
|
||||
var hasExplicitLodCoverage = lodCoverage.Count > 0;
|
||||
if (lodCoverage.Count != lods.Length)
|
||||
{
|
||||
if (!hasExplicitLodCoverage)
|
||||
lodCoverage.Add(0.5f);
|
||||
|
||||
var count = lodCoverage.Count;
|
||||
var length = lods.Length;
|
||||
for (var i = count; i < length; i++)
|
||||
{
|
||||
lodCoverage.Add(lodCoverage[i - 1] / 2);
|
||||
}
|
||||
|
||||
if (!CullFarLOD && !hasExplicitLodCoverage)
|
||||
lodCoverage[lodCoverage.Count - 1] = 0;
|
||||
}
|
||||
|
||||
// sanitize values so that they are strictly decreasing
|
||||
for (int i = lodCoverage.Count - 2; i >= 0; i--)
|
||||
{
|
||||
if (lodCoverage[i] <= lodCoverage[i + 1])
|
||||
lodCoverage[i] = lodCoverage[i + 1] + 0.01f;
|
||||
}
|
||||
|
||||
var lodGroupNodeObj = nodeObj;
|
||||
lodGroupNodeObj.SetActive(false);
|
||||
var firstLodChildRenderers = nodeObj.GetComponentsInChildren<Renderer>().ToList();
|
||||
|
||||
LODGroup lodGroup = lodGroupNodeObj.AddComponent<LODGroup>();
|
||||
for (int i = 0; i < lodsExtension.NodeIds.Count; i++)
|
||||
{
|
||||
int lodNodeId = lodsExtension.NodeIds[i];
|
||||
var lodNodeObj = await GetNode(lodNodeId, cancellationToken);
|
||||
// progressStatus.NodeTotal++;
|
||||
lodNodeObj.transform.SetParent(lodGroupNodeObj.transform, false);
|
||||
var childRenderers = lodNodeObj.GetComponentsInChildren<Renderer>();
|
||||
int lodIndex = i + 1;
|
||||
// make sure to kick out the renderers that are used in lower LOD levels from the root LOD level
|
||||
foreach (var child in childRenderers)
|
||||
firstLodChildRenderers.Remove(child);
|
||||
lods[lodIndex] = new LOD(GetLodCoverage(lodCoverage, lodIndex), childRenderers);
|
||||
}
|
||||
|
||||
lods[0] = new LOD(GetLodCoverage(lodCoverage, 0), firstLodChildRenderers.ToArray());
|
||||
|
||||
if (CullFarLOD)
|
||||
{
|
||||
//use the last mesh as the LOD
|
||||
lods[lodsExtension.NodeIds.Count + 1] = new LOD(0, null);
|
||||
}
|
||||
|
||||
lodGroup.SetLODs(lods);
|
||||
lodGroup.RecalculateBounds();
|
||||
lodGroupNodeObj.SetActive(true);
|
||||
_assetCache.NodeCache[nodeIndex] = lodGroupNodeObj;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fe34f290cdf64038ad7d60c706251109
|
||||
timeCreated: 1675772109
|
||||
@@ -0,0 +1,61 @@
|
||||
using GLTF.Schema;
|
||||
using GLTF.Schema.KHR_lights_punctual;
|
||||
using UnityEngine;
|
||||
using UnityGLTF.Extensions;
|
||||
using UnityGLTF.Plugins;
|
||||
using LightType = UnityEngine.LightType;
|
||||
|
||||
namespace UnityGLTF
|
||||
{
|
||||
public partial class GLTFSceneImporter
|
||||
{
|
||||
private bool ConstructLights(GameObject nodeObj, Node node)
|
||||
{
|
||||
var useLightsExtension = Context.TryGetPlugin<LightsPunctualImportContext>(out _);
|
||||
if (!useLightsExtension) return false;
|
||||
|
||||
// TODO this should be handled by the lights extension directly, not here
|
||||
const string lightExt = KHR_lights_punctualExtensionFactory.EXTENSION_NAME;
|
||||
KHR_LightsPunctualNodeExtension lightsExtension = null;
|
||||
if (_gltfRoot.ExtensionsUsed != null && _gltfRoot.ExtensionsUsed.Contains(lightExt) && node.Extensions != null && node.Extensions.ContainsKey(lightExt))
|
||||
{
|
||||
lightsExtension = node.Extensions[lightExt] as KHR_LightsPunctualNodeExtension;
|
||||
var l = lightsExtension.LightId;
|
||||
|
||||
var light = l.Value;
|
||||
|
||||
var newLight = nodeObj.AddComponent<Light>();
|
||||
switch (light.Type)
|
||||
{
|
||||
case GLTF.Schema.KHR_lights_punctual.LightType.spot:
|
||||
newLight.intensity = (float) light.Intensity / Mathf.PI;
|
||||
newLight.type = LightType.Spot;
|
||||
break;
|
||||
case GLTF.Schema.KHR_lights_punctual.LightType.directional:
|
||||
newLight.type = LightType.Directional;
|
||||
break;
|
||||
case GLTF.Schema.KHR_lights_punctual.LightType.point:
|
||||
newLight.type = LightType.Point;
|
||||
newLight.intensity = (float) light.Intensity * Mathf.PI;
|
||||
break;
|
||||
}
|
||||
|
||||
newLight.name = light.Name;
|
||||
newLight.color = light.Color.ToUnityColorRaw();
|
||||
newLight.range = (float) light.Range;
|
||||
if (light.Spot != null)
|
||||
{
|
||||
#if UNITY_2019_1_OR_NEWER
|
||||
newLight.innerSpotAngle = (float) light.Spot.InnerConeAngle * 2 / (Mathf.Deg2Rad * 0.8f);
|
||||
#endif
|
||||
newLight.spotAngle = (float) light.Spot.OuterConeAngle * 2 / Mathf.Deg2Rad;
|
||||
}
|
||||
|
||||
nodeObj.transform.localRotation *= SchemaExtensions.InvertDirection;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7b547db626e3430c87fe1e060a131222
|
||||
timeCreated: 1657014645
|
||||
@@ -0,0 +1,1061 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using GLTF.Schema;
|
||||
using UnityEngine;
|
||||
using UnityGLTF.Cache;
|
||||
using UnityGLTF.Extensions;
|
||||
using UnityGLTF.Plugins;
|
||||
|
||||
namespace UnityGLTF
|
||||
{
|
||||
public partial class GLTFSceneImporter
|
||||
{
|
||||
internal static List<Texture> _runtimeNormalTextures = new List<Texture>();
|
||||
|
||||
protected virtual async Task ConstructMaterial(GLTFMaterial def, int materialIndex)
|
||||
{
|
||||
IUniformMap mapper;
|
||||
|
||||
const string specGlossExtName = KHR_materials_pbrSpecularGlossinessExtensionFactory.EXTENSION_NAME;
|
||||
const string unlitExtName = KHR_MaterialsUnlitExtensionFactory.EXTENSION_NAME;
|
||||
|
||||
if (_gltfRoot.ExtensionsUsed != null && _gltfRoot.ExtensionsUsed.Contains(specGlossExtName) && def.Extensions != null && def.Extensions.ContainsKey(specGlossExtName))
|
||||
{
|
||||
Debug.Log(LogType.Warning, $"KHR_materials_pbrSpecularGlossiness has been deprecated, material {def.Name} may not look correct. Use `gltf-transform metalrough` or other tools to convert to PBR. (File: {_gltfFileName})");
|
||||
|
||||
if (!string.IsNullOrEmpty(CustomShaderName))
|
||||
{
|
||||
mapper = new SpecGlossMap(CustomShaderName, MaximumLod);
|
||||
}
|
||||
else
|
||||
{
|
||||
#if UNITY_2021_3_OR_NEWER
|
||||
// This isn't fully supported. KHR_materials_pbrSpecularGlossiness is deprecated though, so we're warning here.
|
||||
mapper = new PBRGraphMap();
|
||||
#else
|
||||
mapper = new SpecGlossMap(MaximumLod);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
else if (_gltfRoot.ExtensionsUsed != null && _gltfRoot.ExtensionsUsed.Contains(unlitExtName) && def.Extensions != null && def.Extensions.ContainsKey(unlitExtName))
|
||||
{
|
||||
if (!string.IsNullOrEmpty(CustomShaderName))
|
||||
{
|
||||
mapper = new UnlitMap(CustomShaderName, null, MaximumLod);
|
||||
}
|
||||
else
|
||||
{
|
||||
#if UNITY_2021_3_OR_NEWER
|
||||
mapper = new UnlitGraphMap();
|
||||
#elif UNITY_2019_1_OR_NEWER
|
||||
if (UnityEngine.Rendering.GraphicsSettings.currentRenderPipeline)
|
||||
mapper = new UnlitGraphMap(def.AlphaMode == AlphaMode.BLEND, def.DoubleSided);
|
||||
else
|
||||
mapper = new UnlitMap(MaximumLod);
|
||||
#else
|
||||
mapper = new UnlitMap(MaximumLod);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!string.IsNullOrEmpty(CustomShaderName))
|
||||
{
|
||||
mapper = new MetalRoughMap(CustomShaderName, MaximumLod);
|
||||
}
|
||||
else
|
||||
{
|
||||
// do we have URP or Unity 2021.2+? Use the PBR Graph Material!
|
||||
#if UNITY_2021_3_OR_NEWER
|
||||
mapper = new PBRGraphMap();
|
||||
#elif UNITY_2019_1_OR_NEWER
|
||||
if (UnityEngine.Rendering.GraphicsSettings.currentRenderPipeline)
|
||||
mapper = new PBRGraphMap(def.AlphaMode == AlphaMode.BLEND, def.DoubleSided);
|
||||
else
|
||||
mapper = new MetalRoughMap(MaximumLod);
|
||||
#else
|
||||
mapper = new MetalRoughMap(MaximumLod);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
void CalculateYOffsetAndScale(TextureId textureId, ExtTextureTransformExtension ext, out Vector2 scale, out Vector2 offset)
|
||||
{
|
||||
offset = ext.Offset.ToUnityVector2Raw();
|
||||
scale = ext.Scale.ToUnityVector2Raw();
|
||||
|
||||
if (IsTextureFlipped(textureId.Value))
|
||||
{
|
||||
offset.y = scale.y + offset.y;
|
||||
scale.y *= -1f;
|
||||
}
|
||||
else
|
||||
{
|
||||
offset.y = 1 - scale.y - offset.y;
|
||||
}
|
||||
}
|
||||
|
||||
mapper.Material.name = def.Name;
|
||||
mapper.AlphaMode = def.AlphaMode;
|
||||
mapper.AlphaCutoff = def.AlphaCutoff;
|
||||
mapper.DoubleSided = def.DoubleSided;
|
||||
mapper.Material.SetFloat("_BUILTIN_QueueControl", -1);
|
||||
mapper.Material.SetFloat("_QueueControl", -1);
|
||||
|
||||
#if UNITY_EDITOR
|
||||
if (Context.SourceImporter == null)
|
||||
#endif
|
||||
mapper.Material.SetFloat("_NormalMapFormatXYZ", 1);
|
||||
|
||||
void SetTransformKeyword()
|
||||
{
|
||||
MatHelper.SetKeyword(mapper.Material, "_TEXTURE_TRANSFORM", true);
|
||||
}
|
||||
|
||||
var mrMapper = mapper as IMetalRoughUniformMap;
|
||||
if (def.PbrMetallicRoughness != null && mrMapper != null)
|
||||
{
|
||||
var pbr = def.PbrMetallicRoughness;
|
||||
|
||||
mrMapper.BaseColorFactor = pbr.BaseColorFactor.ToUnityColorRaw();
|
||||
|
||||
if (pbr.BaseColorTexture != null)
|
||||
{
|
||||
TextureId textureId = pbr.BaseColorTexture.Index;
|
||||
await ConstructTexture(textureId.Value, textureId.Id, !KeepCPUCopyOfTexture, false, false);
|
||||
mrMapper.BaseColorTexture = _assetCache.TextureCache[textureId.Id].Texture;
|
||||
mrMapper.BaseColorTexCoord = pbr.BaseColorTexture.TexCoord;
|
||||
|
||||
var ext = GetTextureTransform(pbr.BaseColorTexture);
|
||||
if (ext != null)
|
||||
{
|
||||
CalculateYOffsetAndScale(textureId, ext, out var scale, out var offset);
|
||||
mrMapper.BaseColorXOffset = offset;
|
||||
mrMapper.BaseColorXRotation = ext.Rotation;
|
||||
mrMapper.BaseColorXScale = scale;
|
||||
if (ext.TexCoord != null) mrMapper.BaseColorXTexCoord = ext.TexCoord.Value;
|
||||
|
||||
SetTransformKeyword();
|
||||
}
|
||||
else if (IsTextureFlipped(textureId.Value))
|
||||
{
|
||||
mrMapper.BaseColorXScale = new Vector2(1f,-1f);
|
||||
mrMapper.BaseColorXOffset = new Vector2(0f, 1f);
|
||||
SetTransformKeyword();
|
||||
}
|
||||
}
|
||||
|
||||
mrMapper.MetallicFactor = pbr.MetallicFactor;
|
||||
mrMapper.RoughnessFactor = pbr.RoughnessFactor;
|
||||
|
||||
if (pbr.MetallicRoughnessTexture != null)
|
||||
{
|
||||
TextureId textureId = pbr.MetallicRoughnessTexture.Index;
|
||||
await ConstructTexture(textureId.Value, textureId.Id, !KeepCPUCopyOfTexture, true, false);
|
||||
mrMapper.MetallicRoughnessTexture = _assetCache.TextureCache[textureId.Id].Texture;
|
||||
mrMapper.MetallicRoughnessTexCoord = pbr.MetallicRoughnessTexture.TexCoord;
|
||||
|
||||
var ext = GetTextureTransform(pbr.MetallicRoughnessTexture);
|
||||
if (ext != null)
|
||||
{
|
||||
CalculateYOffsetAndScale(textureId, ext, out var scale, out var offset);
|
||||
mrMapper.MetallicRoughnessXOffset = offset;
|
||||
mrMapper.MetallicRoughnessXRotation = ext.Rotation;
|
||||
mrMapper.MetallicRoughnessXScale = scale;
|
||||
if (ext.TexCoord != null) mrMapper.MetallicRoughnessXTexCoord = ext.TexCoord.Value;
|
||||
SetTransformKeyword();
|
||||
}
|
||||
else if (IsTextureFlipped(textureId.Value))
|
||||
{
|
||||
mrMapper.MetallicRoughnessXScale = new Vector2(1f,-1f);
|
||||
mrMapper.MetallicRoughnessXOffset = new Vector2(0f, 1f);
|
||||
SetTransformKeyword();
|
||||
}
|
||||
}
|
||||
}
|
||||
// when PbrMetallicRoughness is not defined, default values MUST apply
|
||||
else if (def.PbrMetallicRoughness == null && mrMapper != null)
|
||||
{
|
||||
mrMapper.MetallicFactor = 1;
|
||||
mrMapper.RoughnessFactor = 1;
|
||||
}
|
||||
|
||||
// get MaterialImportPluginContext and check which options are enabled
|
||||
// ReSharper disable InconsistentNaming IdentifierTypo
|
||||
var settings = (Context.Plugins.FirstOrDefault(x => x is MaterialExtensionsImportContext) as MaterialExtensionsImportContext)?.settings;
|
||||
var KHR_materials_ior = settings && settings.KHR_materials_ior;
|
||||
var KHR_materials_transmission = settings && settings.KHR_materials_transmission;
|
||||
var KHR_materials_volume = settings && settings.KHR_materials_volume;
|
||||
var KHR_materials_iridescence = settings && settings.KHR_materials_iridescence;
|
||||
var KHR_materials_specular = settings && settings.KHR_materials_specular;
|
||||
var KHR_materials_clearcoat = settings && settings.KHR_materials_clearcoat;
|
||||
var KHR_materials_pbrSpecularGlossiness = settings && settings.KHR_materials_pbrSpecularGlossiness;
|
||||
var KHR_materials_emissive_strength = settings && settings.KHR_materials_emissive_strength;
|
||||
var KHR_materials_sheen = settings && settings.KHR_materials_sheen;
|
||||
// ReSharper restore InconsistentNaming
|
||||
|
||||
var sgMapper = mapper as ISpecGlossUniformMap;
|
||||
if (sgMapper != null && KHR_materials_pbrSpecularGlossiness)
|
||||
{
|
||||
var specGloss = def.Extensions[specGlossExtName] as KHR_materials_pbrSpecularGlossinessExtension;
|
||||
|
||||
sgMapper.DiffuseFactor = specGloss.DiffuseFactor.ToUnityColorRaw();
|
||||
|
||||
if (specGloss.DiffuseTexture != null)
|
||||
{
|
||||
TextureId textureId = specGloss.DiffuseTexture.Index;
|
||||
await ConstructTexture(textureId.Value, textureId.Id, !KeepCPUCopyOfTexture, false, false);
|
||||
sgMapper.DiffuseTexture = _assetCache.TextureCache[textureId.Id].Texture;
|
||||
sgMapper.DiffuseTexCoord = specGloss.DiffuseTexture.TexCoord;
|
||||
|
||||
var ext = GetTextureTransform(specGloss.DiffuseTexture);
|
||||
if (ext != null)
|
||||
{
|
||||
CalculateYOffsetAndScale(textureId, ext, out var scale, out var offset);
|
||||
sgMapper.DiffuseXOffset = offset;
|
||||
sgMapper.DiffuseXRotation = ext.Rotation;
|
||||
sgMapper.DiffuseXScale = scale;
|
||||
if (ext.TexCoord != null) sgMapper.DiffuseXTexCoord = ext.TexCoord.Value;
|
||||
MatHelper.SetKeyword(mapper.Material, "_TEXTURE_TRANSFORM", true);
|
||||
SetTransformKeyword();
|
||||
}
|
||||
else if (IsTextureFlipped(textureId.Value))
|
||||
{
|
||||
sgMapper.DiffuseXScale = new Vector2(1f,-1f);
|
||||
sgMapper.DiffuseXOffset = new Vector2(0f, 1f);
|
||||
SetTransformKeyword();
|
||||
}
|
||||
}
|
||||
|
||||
sgMapper.SpecularFactor = specGloss.SpecularFactor.ToUnityVector3Raw();
|
||||
sgMapper.GlossinessFactor = specGloss.GlossinessFactor;
|
||||
|
||||
if (specGloss.SpecularGlossinessTexture != null)
|
||||
{
|
||||
TextureId textureId = specGloss.SpecularGlossinessTexture.Index;
|
||||
await ConstructTexture(textureId.Value, textureId.Id, !KeepCPUCopyOfTexture, false, false);
|
||||
sgMapper.SpecularGlossinessTexture = _assetCache.TextureCache[textureId.Id].Texture;
|
||||
sgMapper.SpecularGlossinessTexCoord = specGloss.SpecularGlossinessTexture.TexCoord;
|
||||
|
||||
var ext = GetTextureTransform(specGloss.SpecularGlossinessTexture);
|
||||
if (ext != null)
|
||||
{
|
||||
CalculateYOffsetAndScale(textureId, ext, out var scale, out var offset);
|
||||
sgMapper.SpecularGlossinessXOffset = offset;
|
||||
sgMapper.SpecularGlossinessXRotation = ext.Rotation;
|
||||
sgMapper.SpecularGlossinessXScale = scale;
|
||||
if (ext.TexCoord != null) sgMapper.SpecularGlossinessXTexCoord = ext.TexCoord.Value;
|
||||
SetTransformKeyword();
|
||||
}
|
||||
else if (IsTextureFlipped(textureId.Value))
|
||||
{
|
||||
sgMapper.SpecularGlossinessXScale = new Vector2(1f,-1f);
|
||||
sgMapper.SpecularGlossinessXOffset = new Vector2(0f, 1f);
|
||||
SetTransformKeyword();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var unlitMapper = mapper as IUnlitUniformMap;
|
||||
if (unlitMapper != null)
|
||||
{
|
||||
var pbr = def.PbrMetallicRoughness;
|
||||
unlitMapper.BaseColorFactor = pbr.BaseColorFactor.ToUnityColorRaw();
|
||||
|
||||
if (pbr.BaseColorTexture != null)
|
||||
{
|
||||
TextureId textureId = pbr.BaseColorTexture.Index;
|
||||
await ConstructTexture(textureId.Value, textureId.Id, !KeepCPUCopyOfTexture, false, false);
|
||||
unlitMapper.BaseColorTexture = _assetCache.TextureCache[textureId.Id].Texture;
|
||||
unlitMapper.BaseColorTexCoord = pbr.BaseColorTexture.TexCoord;
|
||||
|
||||
var ext = GetTextureTransform(pbr.BaseColorTexture);
|
||||
if (ext != null)
|
||||
{
|
||||
CalculateYOffsetAndScale(textureId, ext, out var scale, out var offset);
|
||||
unlitMapper.BaseColorXOffset = offset;
|
||||
unlitMapper.BaseColorXRotation = ext.Rotation;
|
||||
unlitMapper.BaseColorXScale = scale;
|
||||
if (ext.TexCoord != null) unlitMapper.BaseColorXTexCoord = ext.TexCoord.Value;
|
||||
SetTransformKeyword();
|
||||
}
|
||||
else if (IsTextureFlipped(textureId.Value))
|
||||
{
|
||||
unlitMapper.BaseColorXScale = new Vector2(1f,-1f);
|
||||
unlitMapper.BaseColorXOffset = new Vector2(0f, 1f);
|
||||
SetTransformKeyword();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var iorMapper = mapper as IIORMap;
|
||||
if (iorMapper != null && KHR_materials_ior)
|
||||
{
|
||||
var ior = GetIOR(def);
|
||||
if (ior != null)
|
||||
{
|
||||
iorMapper.IOR = ior.ior;
|
||||
}
|
||||
}
|
||||
|
||||
var sheenMapper = mapper as ISheenMap;
|
||||
if (sheenMapper != null && KHR_materials_sheen)
|
||||
{
|
||||
var sheen = GetSheen(def);
|
||||
if (sheen != null)
|
||||
{
|
||||
sheenMapper.SheenColorFactor = sheen.sheenColorFactor.ToUnityColorRaw();
|
||||
sheenMapper.SheenRoughnessFactor = sheen.sheenRoughnessFactor;
|
||||
MatHelper.SetKeyword(mapper.Material, "_SHEEN", true);
|
||||
|
||||
if (sheen.sheenColorTexture != null)
|
||||
{
|
||||
var td = await FromTextureInfo(sheen.sheenColorTexture, false);
|
||||
sheenMapper.SheenColorTexture = td.Texture;
|
||||
sheenMapper.SheenColorTextureTexCoord = td.TexCoord;
|
||||
var ext = GetTextureTransform(sheen.sheenColorTexture);
|
||||
if (ext != null)
|
||||
{
|
||||
CalculateYOffsetAndScale(sheen.sheenColorTexture.Index, ext, out var scale, out var offset);
|
||||
sheenMapper.SheenColorTextureOffset = offset;
|
||||
sheenMapper.SheenColorTextureScale = scale;
|
||||
sheenMapper.SheenColorTextureRotation = td.Rotation;
|
||||
if (td.TexCoordExtra != null) sheenMapper.SheenColorTextureTexCoord = td.TexCoordExtra.Value;
|
||||
SetTransformKeyword();
|
||||
}
|
||||
else if (IsTextureFlipped(sheen.sheenColorTexture.Index.Value))
|
||||
{
|
||||
sheenMapper.SheenColorTextureScale = new Vector2(1f,-1f);
|
||||
sheenMapper.SheenColorTextureOffset = new Vector2(0f, 1f);
|
||||
SetTransformKeyword();
|
||||
}
|
||||
}
|
||||
|
||||
if (sheen.sheenRoughnessTexture != null)
|
||||
{
|
||||
var td = await FromTextureInfo(sheen.sheenRoughnessTexture, false);
|
||||
sheenMapper.SheenRoughnessTexture = td.Texture;
|
||||
sheenMapper.SheenColorTextureTexCoord = td.TexCoord;
|
||||
var ext = GetTextureTransform(sheen.sheenRoughnessTexture);
|
||||
if (ext != null)
|
||||
{
|
||||
CalculateYOffsetAndScale(sheen.sheenRoughnessTexture.Index, ext, out var scale, out var offset);
|
||||
sheenMapper.SheenRoughnessTextureOffset = offset;
|
||||
sheenMapper.SheenRoughnessTextureScale = scale;
|
||||
sheenMapper.SheenRoughnessTextureRotation = td.Rotation;
|
||||
if (td.TexCoordExtra != null) sheenMapper.SheenRoughnessTextureTexCoord = td.TexCoordExtra.Value;
|
||||
SetTransformKeyword();
|
||||
}
|
||||
else if (IsTextureFlipped(sheen.sheenRoughnessTexture.Index.Value))
|
||||
{
|
||||
sheenMapper.SheenRoughnessTextureScale = new Vector2(1f,-1f);
|
||||
sheenMapper.SheenRoughnessTextureOffset = new Vector2(0f, 1f);
|
||||
SetTransformKeyword();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var transmissionMapper = mapper as ITransmissionMap;
|
||||
if (transmissionMapper != null && KHR_materials_transmission)
|
||||
{
|
||||
var transmission = GetTransmission(def);
|
||||
if (transmission != null)
|
||||
{
|
||||
transmissionMapper.TransmissionFactor = transmission.transmissionFactor;
|
||||
|
||||
if (transmission.transmissionTexture != null)
|
||||
{
|
||||
var td = await FromTextureInfo(transmission.transmissionTexture, false);
|
||||
transmissionMapper.TransmissionTexture = td.Texture;
|
||||
transmissionMapper.TransmissionTextureTexCoord = td.TexCoord;
|
||||
var ext = GetTextureTransform(transmission.transmissionTexture);
|
||||
if (ext != null)
|
||||
{
|
||||
CalculateYOffsetAndScale(transmission.transmissionTexture.Index, ext, out var scale, out var offset);
|
||||
transmissionMapper.TransmissionTextureOffset = offset;
|
||||
transmissionMapper.TransmissionTextureScale = scale;
|
||||
transmissionMapper.TransmissionTextureRotation = td.Rotation;
|
||||
if (td.TexCoordExtra != null) transmissionMapper.TransmissionTextureTexCoord = td.TexCoordExtra.Value;
|
||||
SetTransformKeyword();
|
||||
}
|
||||
else if (IsTextureFlipped(transmission.transmissionTexture.Index.Value))
|
||||
{
|
||||
transmissionMapper.TransmissionTextureScale = new Vector2(1f,-1f);
|
||||
transmissionMapper.TransmissionTextureOffset = new Vector2(0f, 1f);
|
||||
SetTransformKeyword();
|
||||
}
|
||||
}
|
||||
|
||||
mapper.Material.renderQueue = 3000;
|
||||
#if UNITY_VISIONOS
|
||||
mapper.AlphaMode = AlphaMode.BLEND;
|
||||
#endif
|
||||
bool hasDispersion = false;
|
||||
if (transmissionMapper is IDispersionMap dispersionMapper)
|
||||
{
|
||||
var dispersion = GetDispersion(def);
|
||||
if (dispersion != null)
|
||||
{
|
||||
hasDispersion = true;
|
||||
dispersionMapper.Dispersion = dispersion.dispersion;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasDispersion)
|
||||
{
|
||||
mapper.Material.EnableKeyword("_VOLUME_TRANSMISSION_ANDDISPERSION");
|
||||
mapper.Material.SetFloat("_VOLUME_TRANSMISSION", 2f);
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
mapper.Material.EnableKeyword("_VOLUME_TRANSMISSION_ON");
|
||||
mapper.Material.SetFloat("_VOLUME_TRANSMISSION", 1f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var volumeMapper = mapper as IVolumeMap;
|
||||
if (volumeMapper != null && KHR_materials_volume)
|
||||
{
|
||||
var volume = GetVolume(def);
|
||||
if (volume != null)
|
||||
{
|
||||
#if UNITY_VISIONOS
|
||||
mapper.AlphaMode = AlphaMode.BLEND;
|
||||
#endif
|
||||
volumeMapper.AttenuationColor = QualitySettings.activeColorSpace == ColorSpace.Linear ? volume.attenuationColor.ToUnityColorLinear() : volume.attenuationColor.ToUnityColorRaw();
|
||||
volumeMapper.AttenuationDistance = volume.attenuationDistance;
|
||||
volumeMapper.ThicknessFactor = volume.thicknessFactor;
|
||||
if (volume.thicknessTexture != null)
|
||||
{
|
||||
var td = await FromTextureInfo(volume.thicknessTexture, false);
|
||||
volumeMapper.ThicknessTexture = td.Texture;
|
||||
volumeMapper.ThicknessTextureTexCoord = td.TexCoord;
|
||||
var ext = GetTextureTransform(volume.thicknessTexture);
|
||||
if (ext != null)
|
||||
{
|
||||
CalculateYOffsetAndScale(volume.thicknessTexture.Index, ext, out var scale, out var offset);
|
||||
volumeMapper.ThicknessTextureOffset = offset;
|
||||
volumeMapper.ThicknessTextureScale = scale;
|
||||
volumeMapper.ThicknessTextureRotation = td.Rotation;
|
||||
if (td.TexCoordExtra != null) volumeMapper.ThicknessTextureTexCoord = td.TexCoordExtra.Value;
|
||||
SetTransformKeyword();
|
||||
}
|
||||
else if (IsTextureFlipped(volume.thicknessTexture.Index.Value))
|
||||
{
|
||||
volumeMapper.ThicknessTextureScale = new Vector2(1f,-1f);
|
||||
volumeMapper.ThicknessTextureOffset = new Vector2(0f, 1f);
|
||||
SetTransformKeyword();
|
||||
}
|
||||
}
|
||||
|
||||
mapper.Material.renderQueue = 3000;
|
||||
mapper.Material.SetFloat("_VOLUME_ON", 1f);
|
||||
}
|
||||
}
|
||||
|
||||
var iridescenceMapper = mapper as IIridescenceMap;
|
||||
if (iridescenceMapper != null && KHR_materials_iridescence)
|
||||
{
|
||||
var iridescence = GetIridescence(def);
|
||||
if (iridescence != null)
|
||||
{
|
||||
iridescenceMapper.IridescenceFactor = iridescence.iridescenceFactor;
|
||||
iridescenceMapper.IridescenceIor = iridescence.iridescenceIor;
|
||||
iridescenceMapper.IridescenceThicknessMinimum = iridescence.iridescenceThicknessMinimum;
|
||||
iridescenceMapper.IridescenceThicknessMaximum = iridescence.iridescenceThicknessMaximum;
|
||||
if (iridescence.iridescenceTexture != null)
|
||||
{
|
||||
var td = await FromTextureInfo(iridescence.iridescenceTexture, false);
|
||||
iridescenceMapper.IridescenceTexture = td.Texture;
|
||||
iridescenceMapper.IridescenceTextureTexCoord = td.TexCoord;
|
||||
var ext = GetTextureTransform(iridescence.iridescenceTexture);
|
||||
if (ext != null)
|
||||
{
|
||||
CalculateYOffsetAndScale(iridescence.iridescenceTexture.Index, ext, out var scale, out var offset);
|
||||
iridescenceMapper.IridescenceTextureOffset = offset;
|
||||
iridescenceMapper.IridescenceTextureScale = scale;
|
||||
iridescenceMapper.IridescenceTextureRotation = td.Rotation;
|
||||
if (td.TexCoordExtra != null) iridescenceMapper.IridescenceTextureTexCoord = td.TexCoordExtra.Value;
|
||||
SetTransformKeyword();
|
||||
}
|
||||
else if (IsTextureFlipped(iridescence.iridescenceTexture.Index.Value))
|
||||
{
|
||||
iridescenceMapper.IridescenceTextureScale = new Vector2(1f,-1f);
|
||||
iridescenceMapper.IridescenceTextureOffset = new Vector2(0f, 1f);
|
||||
SetTransformKeyword();
|
||||
}
|
||||
}
|
||||
|
||||
if (iridescence.iridescenceThicknessTexture != null)
|
||||
{
|
||||
var td2 = await FromTextureInfo(iridescence.iridescenceThicknessTexture, false);
|
||||
iridescenceMapper.IridescenceThicknessTexture = td2.Texture;
|
||||
iridescenceMapper.IridescenceThicknessTextureTexCoord = td2.TexCoord;
|
||||
var ext2 = GetTextureTransform(iridescence.iridescenceThicknessTexture);
|
||||
if (ext2 != null)
|
||||
{
|
||||
CalculateYOffsetAndScale(iridescence.iridescenceThicknessTexture.Index, ext2, out var scale,
|
||||
out var offset);
|
||||
iridescenceMapper.IridescenceThicknessTextureOffset = offset;
|
||||
iridescenceMapper.IridescenceThicknessTextureScale = scale;
|
||||
iridescenceMapper.IridescenceThicknessTextureRotation = td2.Rotation;
|
||||
if (td2.TexCoordExtra != null) iridescenceMapper.IridescenceThicknessTextureTexCoord = td2.TexCoordExtra.Value;
|
||||
SetTransformKeyword();
|
||||
}
|
||||
else if (IsTextureFlipped(iridescence.iridescenceThicknessTexture.Index.Value))
|
||||
{
|
||||
iridescenceMapper.IridescenceThicknessTextureScale = new Vector2(1f, -1f);
|
||||
iridescenceMapper.IridescenceThicknessTextureOffset = new Vector2(0f, 1f);
|
||||
SetTransformKeyword();
|
||||
}
|
||||
}
|
||||
|
||||
mapper.Material.SetKeyword("_IRIDESCENCE", true);
|
||||
}
|
||||
}
|
||||
|
||||
var specularMapper = mapper as ISpecularMap;
|
||||
if (specularMapper != null && KHR_materials_specular)
|
||||
{
|
||||
var specular = GetSpecular(def);
|
||||
if (specular != null)
|
||||
{
|
||||
specularMapper.SpecularFactor = specular.specularFactor;
|
||||
specularMapper.SpecularColorFactor = specular.specularColorFactor.ToUnityColorLinear();
|
||||
if (specular.specularTexture != null)
|
||||
{
|
||||
var td = await FromTextureInfo(specular.specularTexture, false);
|
||||
specularMapper.SpecularTexture = td.Texture;
|
||||
specularMapper.SpecularTextureTexCoord = td.TexCoord;
|
||||
var ext = GetTextureTransform(specular.specularTexture);
|
||||
if (ext != null)
|
||||
{
|
||||
CalculateYOffsetAndScale(specular.specularTexture.Index, ext, out var scale,
|
||||
out var offset);
|
||||
specularMapper.SpecularTextureOffset = offset;
|
||||
specularMapper.SpecularTextureScale = scale;
|
||||
specularMapper.SpecularTextureRotation = td.Rotation;
|
||||
if (td.TexCoordExtra != null) specularMapper.SpecularTextureTexCoord = td.TexCoordExtra.Value;
|
||||
SetTransformKeyword();
|
||||
}
|
||||
else if (IsTextureFlipped(specular.specularTexture.Index.Value))
|
||||
{
|
||||
specularMapper.SpecularTextureScale = new Vector2(1f, -1f);
|
||||
specularMapper.SpecularTextureOffset = new Vector2(0f, 1f);
|
||||
SetTransformKeyword();
|
||||
}
|
||||
}
|
||||
|
||||
if (specular.specularColorTexture != null)
|
||||
{
|
||||
var td2 = await FromTextureInfo(specular.specularColorTexture, false);
|
||||
specularMapper.SpecularColorTexture = td2.Texture;
|
||||
specularMapper.SpecularColorTextureTexCoord = td2.TexCoord;
|
||||
var ext2 = GetTextureTransform(specular.specularColorTexture);
|
||||
if (ext2 != null)
|
||||
{
|
||||
CalculateYOffsetAndScale(specular.specularColorTexture.Index, ext2, out var scale,
|
||||
out var offset);
|
||||
specularMapper.SpecularColorTextureOffset = offset;
|
||||
specularMapper.SpecularColorTextureScale = scale;
|
||||
specularMapper.SpecularColorTextureRotation = td2.Rotation;
|
||||
if (td2.TexCoordExtra != null) specularMapper.SpecularColorTextureTexCoord = td2.TexCoordExtra.Value;
|
||||
SetTransformKeyword();
|
||||
}
|
||||
else if (IsTextureFlipped(specular.specularColorTexture.Index.Value))
|
||||
{
|
||||
specularMapper.SpecularColorTextureScale = new Vector2(1f, -1f);
|
||||
specularMapper.SpecularColorTextureOffset = new Vector2(0f, 1f);
|
||||
SetTransformKeyword();
|
||||
}
|
||||
}
|
||||
|
||||
mapper.Material.SetKeyword("_SPECULAR", true);
|
||||
}
|
||||
}
|
||||
|
||||
var clearcoatMapper = mapper as IClearcoatMap;
|
||||
if (clearcoatMapper != null && KHR_materials_clearcoat)
|
||||
{
|
||||
var clearcoat = GetClearcoat(def);
|
||||
if (clearcoat != null)
|
||||
{
|
||||
clearcoatMapper.ClearcoatFactor = clearcoat.clearcoatFactor;
|
||||
clearcoatMapper.ClearcoatRoughnessFactor = clearcoat.clearcoatRoughnessFactor;
|
||||
if (clearcoat.clearcoatTexture != null)
|
||||
{
|
||||
var td = await FromTextureInfo(clearcoat.clearcoatTexture, false);
|
||||
clearcoatMapper.ClearcoatTexture = td.Texture;
|
||||
clearcoatMapper.ClearcoatTextureTexCoord = td.TexCoord;
|
||||
var ext = GetTextureTransform(clearcoat.clearcoatTexture);
|
||||
if (ext != null)
|
||||
{
|
||||
CalculateYOffsetAndScale(clearcoat.clearcoatTexture.Index, ext, out var scale,
|
||||
out var offset);
|
||||
clearcoatMapper.ClearcoatTextureOffset = offset;
|
||||
clearcoatMapper.ClearcoatTextureScale = scale;
|
||||
clearcoatMapper.ClearcoatTextureRotation = td.Rotation;
|
||||
if (td.TexCoordExtra != null) clearcoatMapper.ClearcoatTextureTexCoord = td.TexCoordExtra.Value;
|
||||
SetTransformKeyword();
|
||||
}
|
||||
else if (IsTextureFlipped(clearcoat.clearcoatTexture.Index.Value))
|
||||
{
|
||||
clearcoatMapper.ClearcoatTextureScale = new Vector2(1f, -1f);
|
||||
clearcoatMapper.ClearcoatTextureOffset = new Vector2(0f, 1f);
|
||||
SetTransformKeyword();
|
||||
}
|
||||
}
|
||||
|
||||
if (clearcoat.clearcoatRoughnessTexture != null)
|
||||
{
|
||||
var td = await FromTextureInfo(clearcoat.clearcoatRoughnessTexture, false);
|
||||
clearcoatMapper.ClearcoatRoughnessTexture = td.Texture;
|
||||
clearcoatMapper.ClearcoatRoughnessTextureTexCoord = td.TexCoord;
|
||||
var ext = GetTextureTransform(clearcoat.clearcoatRoughnessTexture);
|
||||
if (ext != null)
|
||||
{
|
||||
CalculateYOffsetAndScale(clearcoat.clearcoatRoughnessTexture.Index, ext, out var scale,
|
||||
out var offset);
|
||||
clearcoatMapper.ClearcoatRoughnessTextureOffset = offset;
|
||||
clearcoatMapper.ClearcoatRoughnessTextureScale = scale;
|
||||
clearcoatMapper.ClearcoatRoughnessTextureRotation = td.Rotation;
|
||||
if (td.TexCoordExtra != null) clearcoatMapper.ClearcoatRoughnessTextureTexCoord = td.TexCoordExtra.Value;
|
||||
SetTransformKeyword();
|
||||
}
|
||||
else if (IsTextureFlipped(clearcoat.clearcoatRoughnessTexture.Index.Value))
|
||||
{
|
||||
clearcoatMapper.ClearcoatRoughnessTextureScale = new Vector2(1f, -1f);
|
||||
clearcoatMapper.ClearcoatRoughnessTextureOffset = new Vector2(0f, 1f);
|
||||
SetTransformKeyword();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
var clearcoatNormalMapper = mapper as IClearcoatNormalMap;
|
||||
if (clearcoatNormalMapper != null)
|
||||
{
|
||||
if (clearcoat.clearcoatNormalTexture != null)
|
||||
{
|
||||
var td = await FromTextureInfo(clearcoat.clearcoatNormalTexture, false);
|
||||
|
||||
_runtimeNormalTextures.Add(td.Texture);
|
||||
|
||||
clearcoatNormalMapper.ClearcoatNormalTexture = td.Texture;
|
||||
clearcoatNormalMapper.ClearcoatNormalTextureTexCoord = td.TexCoord;
|
||||
var ext = GetTextureTransform(clearcoat.clearcoatNormalTexture);
|
||||
if (ext != null)
|
||||
{
|
||||
CalculateYOffsetAndScale(clearcoat.clearcoatNormalTexture.Index, ext, out var scale,
|
||||
out var offset);
|
||||
clearcoatNormalMapper.ClearcoatNormalTextureOffset = offset;
|
||||
clearcoatNormalMapper.ClearcoatNormalTextureScale = scale;
|
||||
clearcoatNormalMapper.ClearcoatNormalTextureRotation = td.Rotation;
|
||||
if (td.TexCoordExtra != null)
|
||||
clearcoatNormalMapper.ClearcoatNormalTextureTexCoord = td.TexCoordExtra.Value;
|
||||
SetTransformKeyword();
|
||||
}
|
||||
else if (IsTextureFlipped(clearcoat.clearcoatNormalTexture.Index.Value))
|
||||
{
|
||||
clearcoatNormalMapper.ClearcoatNormalTextureScale = new Vector2(1f, -1f);
|
||||
clearcoatNormalMapper.ClearcoatNormalTextureOffset = new Vector2(0f, 1f);
|
||||
SetTransformKeyword();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mapper.Material.SetKeyword("_CLEARCOAT", true);
|
||||
}
|
||||
}
|
||||
|
||||
var uniformMapper = mapper as ILitMap;
|
||||
if (uniformMapper != null)
|
||||
{
|
||||
if (def.NormalTexture != null)
|
||||
{
|
||||
TextureId textureId = def.NormalTexture.Index;
|
||||
await ConstructTexture(textureId.Value, textureId.Id, !KeepCPUCopyOfTexture, true, true);
|
||||
|
||||
uniformMapper.NormalTexture = _assetCache.TextureCache[textureId.Id].Texture;
|
||||
uniformMapper.NormalTexCoord = def.NormalTexture.TexCoord;
|
||||
uniformMapper.NormalTexScale = def.NormalTexture.Scale;
|
||||
|
||||
_runtimeNormalTextures.Add(uniformMapper.NormalTexture);
|
||||
|
||||
var ext = GetTextureTransform(def.NormalTexture);
|
||||
if (ext != null)
|
||||
{
|
||||
CalculateYOffsetAndScale(textureId, ext, out var scale, out var offset);
|
||||
uniformMapper.NormalXOffset = offset;
|
||||
uniformMapper.NormalXRotation = ext.Rotation;
|
||||
uniformMapper.NormalXScale = scale;
|
||||
if (ext.TexCoord != null) uniformMapper.NormalXTexCoord = ext.TexCoord.Value;
|
||||
SetTransformKeyword();
|
||||
}
|
||||
else if (IsTextureFlipped(textureId.Value))
|
||||
{
|
||||
uniformMapper.NormalXScale = new Vector2(1f,-1f);
|
||||
uniformMapper.NormalXOffset = new Vector2(0f, 1f);
|
||||
SetTransformKeyword();
|
||||
}
|
||||
}
|
||||
|
||||
if (def.EmissiveTexture != null)
|
||||
{
|
||||
TextureId textureId = def.EmissiveTexture.Index;
|
||||
await ConstructTexture(textureId.Value, textureId.Id, !KeepCPUCopyOfTexture, false, false);
|
||||
uniformMapper.EmissiveTexture = _assetCache.TextureCache[textureId.Id].Texture;
|
||||
uniformMapper.EmissiveTexCoord = def.EmissiveTexture.TexCoord;
|
||||
|
||||
var ext = GetTextureTransform(def.EmissiveTexture);
|
||||
if (ext != null)
|
||||
{
|
||||
CalculateYOffsetAndScale(textureId, ext, out var scale, out var offset);
|
||||
uniformMapper.EmissiveXOffset = offset;
|
||||
uniformMapper.EmissiveXRotation = ext.Rotation;
|
||||
uniformMapper.EmissiveXScale = scale;
|
||||
if (ext.TexCoord != null) uniformMapper.EmissiveXTexCoord = ext.TexCoord.Value;
|
||||
SetTransformKeyword();
|
||||
}
|
||||
else if (IsTextureFlipped(textureId.Value))
|
||||
{
|
||||
uniformMapper.EmissiveXScale = new Vector2(1f,-1f);
|
||||
uniformMapper.EmissiveXOffset = new Vector2(0f, 1f);
|
||||
SetTransformKeyword();
|
||||
}
|
||||
}
|
||||
|
||||
if (def.OcclusionTexture != null)
|
||||
{
|
||||
uniformMapper.OcclusionTexStrength = def.OcclusionTexture.Strength;
|
||||
TextureId textureId = def.OcclusionTexture.Index;
|
||||
await ConstructTexture(textureId.Value, textureId.Id, !KeepCPUCopyOfTexture, true, false);
|
||||
uniformMapper.OcclusionTexture = _assetCache.TextureCache[textureId.Id].Texture;
|
||||
uniformMapper.OcclusionTexCoord = def.OcclusionTexture.TexCoord;
|
||||
|
||||
var ext = GetTextureTransform(def.OcclusionTexture);
|
||||
|
||||
if (ext != null)
|
||||
{
|
||||
CalculateYOffsetAndScale(textureId, ext, out var scale, out var offset);
|
||||
uniformMapper.OcclusionXOffset = offset;
|
||||
uniformMapper.OcclusionXRotation = ext.Rotation;
|
||||
uniformMapper.OcclusionXScale = scale;
|
||||
if (ext.TexCoord != null) uniformMapper.OcclusionXTexCoord = ext.TexCoord.Value;
|
||||
SetTransformKeyword();
|
||||
}
|
||||
else if (IsTextureFlipped(textureId.Value))
|
||||
{
|
||||
uniformMapper.OcclusionXScale = new Vector2(1f,-1f);
|
||||
uniformMapper.OcclusionXOffset = new Vector2(0f, 1f);
|
||||
SetTransformKeyword();
|
||||
}
|
||||
}
|
||||
|
||||
// ??
|
||||
uniformMapper.EmissiveFactor = QualitySettings.activeColorSpace == ColorSpace.Linear ? def.EmissiveFactor.ToUnityColorLinear() : def.EmissiveFactor.ToUnityColorLinear();
|
||||
|
||||
var emissiveExt = GetEmissiveStrength(def);
|
||||
if (emissiveExt != null && KHR_materials_emissive_strength)
|
||||
{
|
||||
uniformMapper.EmissiveFactor = uniformMapper.EmissiveFactor * emissiveExt.emissiveStrength;
|
||||
}
|
||||
}
|
||||
var vertColorMapper = mapper.Clone();
|
||||
vertColorMapper.VertexColorsEnabled = true;
|
||||
|
||||
// if (mapper is PBRGraphMap pbrGraphMap)
|
||||
// {
|
||||
// MaterialExtensions.ValidateMaterialKeywords(pbrGraphMap.Material);
|
||||
// }
|
||||
|
||||
MaterialCacheData materialWrapper = new MaterialCacheData
|
||||
{
|
||||
UnityMaterial = mapper.Material,
|
||||
UnityMaterialWithVertexColor = vertColorMapper.Material,
|
||||
GLTFMaterial = def
|
||||
};
|
||||
|
||||
if (materialIndex >= 0)
|
||||
{
|
||||
_assetCache.MaterialCache[materialIndex] = materialWrapper;
|
||||
}
|
||||
else
|
||||
{
|
||||
_defaultLoadedMaterial = materialWrapper;
|
||||
}
|
||||
|
||||
foreach (var plugin in Context.Plugins)
|
||||
{
|
||||
plugin.OnAfterImportMaterial(def, materialIndex, mapper.Material);
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual Task ConstructMaterialImageBuffers(GLTFMaterial def)
|
||||
{
|
||||
var tasks = new List<Task>(8);
|
||||
if (def.PbrMetallicRoughness != null)
|
||||
{
|
||||
var pbr = def.PbrMetallicRoughness;
|
||||
|
||||
if (pbr.BaseColorTexture != null)
|
||||
{
|
||||
var textureId = pbr.BaseColorTexture.Index;
|
||||
tasks.Add(ConstructImageBuffer(textureId.Value, textureId.Id));
|
||||
}
|
||||
if (pbr.MetallicRoughnessTexture != null)
|
||||
{
|
||||
var textureId = pbr.MetallicRoughnessTexture.Index;
|
||||
|
||||
tasks.Add(ConstructImageBuffer(textureId.Value, textureId.Id));
|
||||
}
|
||||
}
|
||||
|
||||
if (def.CommonConstant != null)
|
||||
{
|
||||
if (def.CommonConstant.LightmapTexture != null)
|
||||
{
|
||||
var textureId = def.CommonConstant.LightmapTexture.Index;
|
||||
|
||||
tasks.Add(ConstructImageBuffer(textureId.Value, textureId.Id));
|
||||
}
|
||||
}
|
||||
|
||||
if (def.NormalTexture != null)
|
||||
{
|
||||
var textureId = def.NormalTexture.Index;
|
||||
tasks.Add(ConstructImageBuffer(textureId.Value, textureId.Id));
|
||||
}
|
||||
|
||||
if (def.OcclusionTexture != null)
|
||||
{
|
||||
var textureId = def.OcclusionTexture.Index;
|
||||
|
||||
if (!(def.PbrMetallicRoughness != null
|
||||
&& def.PbrMetallicRoughness.MetallicRoughnessTexture != null
|
||||
&& def.PbrMetallicRoughness.MetallicRoughnessTexture.Index.Id == textureId.Id))
|
||||
{
|
||||
tasks.Add(ConstructImageBuffer(textureId.Value, textureId.Id));
|
||||
}
|
||||
}
|
||||
|
||||
if (def.EmissiveTexture != null)
|
||||
{
|
||||
var textureId = def.EmissiveTexture.Index;
|
||||
tasks.Add(ConstructImageBuffer(textureId.Value, textureId.Id));
|
||||
}
|
||||
|
||||
// pbr_spec_gloss extension
|
||||
const string specGlossExtName = KHR_materials_pbrSpecularGlossinessExtensionFactory.EXTENSION_NAME;
|
||||
if (def.Extensions != null && def.Extensions.ContainsKey(specGlossExtName))
|
||||
{
|
||||
var specGlossDef = (KHR_materials_pbrSpecularGlossinessExtension)def.Extensions[specGlossExtName];
|
||||
if (specGlossDef.DiffuseTexture != null)
|
||||
{
|
||||
var textureId = specGlossDef.DiffuseTexture.Index;
|
||||
tasks.Add(ConstructImageBuffer(textureId.Value, textureId.Id));
|
||||
}
|
||||
|
||||
if (specGlossDef.SpecularGlossinessTexture != null)
|
||||
{
|
||||
var textureId = specGlossDef.SpecularGlossinessTexture.Index;
|
||||
tasks.Add(ConstructImageBuffer(textureId.Value, textureId.Id));
|
||||
}
|
||||
}
|
||||
|
||||
if (def.Extensions != null && def.Extensions.ContainsKey(KHR_materials_transmission_Factory.EXTENSION_NAME))
|
||||
{
|
||||
var transmissionDef = (KHR_materials_transmission)def.Extensions[KHR_materials_transmission_Factory.EXTENSION_NAME];
|
||||
if (transmissionDef.transmissionTexture != null)
|
||||
{
|
||||
var textureId = transmissionDef.transmissionTexture.Index;
|
||||
tasks.Add(ConstructImageBuffer(textureId.Value, textureId.Id));
|
||||
}
|
||||
}
|
||||
|
||||
if (def.Extensions != null && def.Extensions.ContainsKey(KHR_materials_volume_Factory.EXTENSION_NAME))
|
||||
{
|
||||
var transmissionDef = (KHR_materials_volume)def.Extensions[KHR_materials_volume_Factory.EXTENSION_NAME];
|
||||
if (transmissionDef.thicknessTexture != null)
|
||||
{
|
||||
var textureId = transmissionDef.thicknessTexture.Index;
|
||||
tasks.Add(ConstructImageBuffer(textureId.Value, textureId.Id));
|
||||
}
|
||||
}
|
||||
|
||||
if (def.Extensions != null && def.Extensions.ContainsKey(KHR_materials_iridescence_Factory.EXTENSION_NAME))
|
||||
{
|
||||
var iridescenceDef = (KHR_materials_iridescence) def.Extensions[KHR_materials_iridescence_Factory.EXTENSION_NAME];
|
||||
if (iridescenceDef.iridescenceTexture != null)
|
||||
{
|
||||
var textureId = iridescenceDef.iridescenceTexture.Index;
|
||||
tasks.Add(ConstructImageBuffer(textureId.Value, textureId.Id));
|
||||
}
|
||||
if (iridescenceDef.iridescenceThicknessTexture != null)
|
||||
{
|
||||
var textureId = iridescenceDef.iridescenceThicknessTexture.Index;
|
||||
tasks.Add(ConstructImageBuffer(textureId.Value, textureId.Id));
|
||||
}
|
||||
}
|
||||
|
||||
if (def.Extensions != null && def.Extensions.ContainsKey(KHR_materials_specular_Factory.EXTENSION_NAME))
|
||||
{
|
||||
var specularDef = (KHR_materials_specular) def.Extensions[KHR_materials_specular_Factory.EXTENSION_NAME];
|
||||
if (specularDef.specularTexture != null)
|
||||
{
|
||||
var textureId = specularDef.specularTexture.Index;
|
||||
tasks.Add(ConstructImageBuffer(textureId.Value, textureId.Id));
|
||||
}
|
||||
if (specularDef.specularColorTexture != null)
|
||||
{
|
||||
var textureId = specularDef.specularColorTexture.Index;
|
||||
tasks.Add(ConstructImageBuffer(textureId.Value, textureId.Id));
|
||||
}
|
||||
}
|
||||
|
||||
if (def.Extensions != null && def.Extensions.ContainsKey(KHR_materials_sheen_Factory.EXTENSION_NAME))
|
||||
{
|
||||
var sheenDef = (KHR_materials_sheen)def.Extensions[KHR_materials_sheen_Factory.EXTENSION_NAME];
|
||||
if (sheenDef.sheenColorTexture != null)
|
||||
{
|
||||
var textureId = sheenDef.sheenColorTexture.Index;
|
||||
tasks.Add(ConstructImageBuffer(textureId.Value, textureId.Id));
|
||||
}
|
||||
if (sheenDef.sheenRoughnessTexture != null)
|
||||
{
|
||||
var textureId = sheenDef.sheenRoughnessTexture.Index;
|
||||
tasks.Add(ConstructImageBuffer(textureId.Value, textureId.Id));
|
||||
}
|
||||
}
|
||||
|
||||
if (def.Extensions != null && def.Extensions.ContainsKey(KHR_materials_clearcoat_Factory.EXTENSION_NAME))
|
||||
{
|
||||
var clearCoatDef = (KHR_materials_clearcoat) def.Extensions[KHR_materials_clearcoat_Factory.EXTENSION_NAME];
|
||||
if (clearCoatDef.clearcoatTexture != null)
|
||||
{
|
||||
var textureId = clearCoatDef.clearcoatTexture.Index;
|
||||
tasks.Add(ConstructImageBuffer(textureId.Value, textureId.Id));
|
||||
}
|
||||
if (clearCoatDef.clearcoatNormalTexture != null)
|
||||
{
|
||||
var textureId = clearCoatDef.clearcoatNormalTexture.Index;
|
||||
tasks.Add(ConstructImageBuffer(textureId.Value, textureId.Id));
|
||||
}
|
||||
if (clearCoatDef.clearcoatRoughnessTexture != null)
|
||||
{
|
||||
var textureId = clearCoatDef.clearcoatRoughnessTexture.Index;
|
||||
tasks.Add(ConstructImageBuffer(textureId.Value, textureId.Id));
|
||||
}
|
||||
}
|
||||
|
||||
return Task.WhenAll(tasks);
|
||||
}
|
||||
|
||||
protected virtual ExtTextureTransformExtension GetTextureTransform(TextureInfo def)
|
||||
{
|
||||
IExtension extension;
|
||||
if (_gltfRoot.ExtensionsUsed != null &&
|
||||
_gltfRoot.ExtensionsUsed.Contains(ExtTextureTransformExtensionFactory.EXTENSION_NAME) &&
|
||||
def.Extensions != null &&
|
||||
def.Extensions.TryGetValue(ExtTextureTransformExtensionFactory.EXTENSION_NAME, out extension))
|
||||
{
|
||||
return (ExtTextureTransformExtension)extension;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected virtual KHR_materials_emissive_strength GetEmissiveStrength(GLTFMaterial def)
|
||||
{
|
||||
if (_gltfRoot.ExtensionsUsed != null && _gltfRoot.ExtensionsUsed.Contains(KHR_materials_emissive_strength_Factory.EXTENSION_NAME) &&
|
||||
def.Extensions != null && def.Extensions.TryGetValue(KHR_materials_emissive_strength_Factory.EXTENSION_NAME, out var extension))
|
||||
{
|
||||
return (KHR_materials_emissive_strength) extension;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected virtual KHR_materials_transmission GetTransmission(GLTFMaterial def)
|
||||
{
|
||||
if (_gltfRoot.ExtensionsUsed != null && _gltfRoot.ExtensionsUsed.Contains(KHR_materials_transmission_Factory.EXTENSION_NAME) &&
|
||||
def.Extensions != null && def.Extensions.TryGetValue(KHR_materials_transmission_Factory.EXTENSION_NAME, out var extension))
|
||||
{
|
||||
return (KHR_materials_transmission) extension;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected virtual KHR_materials_sheen GetSheen(GLTFMaterial def)
|
||||
{
|
||||
if (_gltfRoot.ExtensionsUsed != null && _gltfRoot.ExtensionsUsed.Contains(KHR_materials_sheen_Factory.EXTENSION_NAME) &&
|
||||
def.Extensions != null && def.Extensions.TryGetValue(KHR_materials_sheen_Factory.EXTENSION_NAME, out var extension))
|
||||
{
|
||||
return (KHR_materials_sheen) extension;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected virtual KHR_materials_dispersion GetDispersion(GLTFMaterial def)
|
||||
{
|
||||
if (_gltfRoot.ExtensionsUsed != null && _gltfRoot.ExtensionsUsed.Contains(KHR_materials_dispersion_Factory.EXTENSION_NAME) &&
|
||||
def.Extensions != null && def.Extensions.TryGetValue(KHR_materials_dispersion_Factory.EXTENSION_NAME, out var extension))
|
||||
{
|
||||
return (KHR_materials_dispersion) extension;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
protected virtual KHR_materials_volume GetVolume(GLTFMaterial def)
|
||||
{
|
||||
if (_gltfRoot.ExtensionsUsed != null && _gltfRoot.ExtensionsUsed.Contains(KHR_materials_volume_Factory.EXTENSION_NAME) &&
|
||||
def.Extensions != null && def.Extensions.TryGetValue(KHR_materials_volume_Factory.EXTENSION_NAME, out var extension))
|
||||
{
|
||||
return (KHR_materials_volume) extension;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected virtual KHR_materials_ior GetIOR(GLTFMaterial def)
|
||||
{
|
||||
if (_gltfRoot.ExtensionsUsed != null && _gltfRoot.ExtensionsUsed.Contains(KHR_materials_ior_Factory.EXTENSION_NAME) &&
|
||||
def.Extensions != null && def.Extensions.TryGetValue(KHR_materials_ior_Factory.EXTENSION_NAME, out var extension))
|
||||
{
|
||||
return (KHR_materials_ior) extension;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected virtual KHR_materials_iridescence GetIridescence(GLTFMaterial def)
|
||||
{
|
||||
if (_gltfRoot.ExtensionsUsed != null && _gltfRoot.ExtensionsUsed.Contains(KHR_materials_iridescence_Factory.EXTENSION_NAME) &&
|
||||
def.Extensions != null && def.Extensions.TryGetValue(KHR_materials_iridescence_Factory.EXTENSION_NAME, out var extension))
|
||||
{
|
||||
return (KHR_materials_iridescence) extension;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected virtual KHR_materials_specular GetSpecular(GLTFMaterial def)
|
||||
{
|
||||
if (_gltfRoot.ExtensionsUsed != null && _gltfRoot.ExtensionsUsed.Contains(KHR_materials_specular_Factory.EXTENSION_NAME) &&
|
||||
def.Extensions != null && def.Extensions.TryGetValue(KHR_materials_specular_Factory.EXTENSION_NAME, out var extension))
|
||||
{
|
||||
return (KHR_materials_specular) extension;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected virtual KHR_materials_clearcoat GetClearcoat(GLTFMaterial def)
|
||||
{
|
||||
if (_gltfRoot.ExtensionsUsed != null && _gltfRoot.ExtensionsUsed.Contains(KHR_materials_clearcoat_Factory.EXTENSION_NAME) &&
|
||||
def.Extensions != null && def.Extensions.TryGetValue(KHR_materials_clearcoat_Factory.EXTENSION_NAME, out var extension))
|
||||
{
|
||||
return (KHR_materials_clearcoat) extension;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 25e2614154434641a15540a7203e47f6
|
||||
timeCreated: 1657012132
|
||||
@@ -0,0 +1,1430 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using GLTF;
|
||||
using GLTF.Schema;
|
||||
using Unity.Collections;
|
||||
using Unity.Jobs;
|
||||
using Unity.Mathematics;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Rendering;
|
||||
using UnityGLTF.Cache;
|
||||
using UnityGLTF.Extensions;
|
||||
#if HAVE_DRACO
|
||||
using Draco;
|
||||
using UnityGLTF.Plugins;
|
||||
#if !HAVE_DRACO_VERSION_5
|
||||
using DecodeResult = Draco.DracoMeshLoader.DecodeResult;
|
||||
#endif
|
||||
#endif
|
||||
|
||||
namespace UnityGLTF
|
||||
{
|
||||
public partial class GLTFSceneImporter
|
||||
{
|
||||
#if HAVE_DRACO
|
||||
protected class DracoDecodeResult
|
||||
{
|
||||
public int meshIndex;
|
||||
public Task<DecodeResult>[] decodeResults = new Task<DecodeResult>[0];
|
||||
}
|
||||
#endif
|
||||
|
||||
private async Task CreateMaterials(MeshPrimitive primitive)
|
||||
{
|
||||
bool shouldUseDefaultMaterial = primitive.Material == null;
|
||||
GLTFMaterial materialToLoad = shouldUseDefaultMaterial ? DefaultMaterial : primitive.Material.Value;
|
||||
if ((shouldUseDefaultMaterial && _defaultLoadedMaterial == null) ||
|
||||
(!shouldUseDefaultMaterial && _assetCache.MaterialCache[primitive.Material.Id] == null))
|
||||
{
|
||||
await ConstructMaterial(materialToLoad, shouldUseDefaultMaterial ? -1 : primitive.Material.Id);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Triggers loading, converting, and constructing of a UnityEngine.Mesh, and stores it in the asset cache
|
||||
/// </summary>
|
||||
/// <param name="mesh">The definition of the mesh to generate</param>
|
||||
/// <param name="meshIndex">The index of the mesh to generate</param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns>A task that completes when the mesh is attached to the given GameObject</returns>
|
||||
protected virtual async Task ConstructMesh(GLTFMesh mesh, int meshIndex, CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
if (_assetCache.MeshCache[meshIndex] == null)
|
||||
{
|
||||
throw new Exception($"Cannot generate mesh before ConstructMeshAttributes is called! (File: {_gltfFileName})");
|
||||
}
|
||||
else if (_assetCache.MeshCache[meshIndex].LoadedMesh)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var anyHadDraco = mesh.Primitives.Any(p => p.Extensions != null && p.Extensions.ContainsKey(KHR_draco_mesh_compression_Factory.EXTENSION_NAME));
|
||||
#if HAVE_DRACO
|
||||
if (anyHadDraco)
|
||||
{
|
||||
if (Context.TryGetPlugin<DracoImportContext>(out _))
|
||||
{
|
||||
await PrepareDracoMesh(mesh, meshIndex);
|
||||
var dracoTask = ConstructDracoMesh(mesh, meshIndex, cancellationToken);
|
||||
await Task.WhenAll( dracoTask.decodeResults);
|
||||
for (int i = 0; i < dracoTask.decodeResults.Length; i++)
|
||||
{
|
||||
_assetCache.MeshCache[meshIndex].DracoMeshDecodeResult[i] = dracoTask.decodeResults[i].Result;
|
||||
}
|
||||
await BuildUnityDracoMesh(mesh, meshIndex);
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new NotSupportedException($"Can't import model because it uses the KHR_draco_mesh_compression extension. Add the package \"com.unity.cloud.draco\" to your project to import this file. (File: {_gltfFileName})");
|
||||
}
|
||||
}
|
||||
#else
|
||||
if (anyHadDraco)
|
||||
{
|
||||
throw new NotSupportedException($"Can't import model because it uses the KHR_draco_mesh_compression extension. Add the package \"com.unity.cloud.draco\" to your project to import this file. (File: {_gltfFileName})");
|
||||
}
|
||||
#endif
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var meshCache = _assetCache.MeshCache[meshIndex];
|
||||
|
||||
var unityData = CreateUnityMeshData(mesh, meshIndex);
|
||||
|
||||
for (int i = 0; i < mesh.Primitives.Count; ++i)
|
||||
{
|
||||
var primitive = mesh.Primitives[i];
|
||||
var primCache = meshCache.Primitives[i];
|
||||
unityData.Topology[i] = GetTopology(primitive.Mode);
|
||||
unityData.DrawModes[i] = primitive.Mode;
|
||||
|
||||
ConvertAttributeAccessorsToUnityTypes(primCache, unityData, unityData.subMeshVertexOffset[i], i);
|
||||
|
||||
await CreateMaterials(primitive);
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
if (unityData.Topology[i] == MeshTopology.Triangles && primitive.Indices != null && primitive.Indices.Value != null)
|
||||
{
|
||||
Statistics.TriangleCount += primitive.Indices.Value.Count / 3;
|
||||
}
|
||||
}
|
||||
|
||||
Statistics.VertexCount += unityData.Vertices.Length;
|
||||
await ConstructUnityMesh(unityData, meshIndex, mesh.Name);
|
||||
}
|
||||
|
||||
private static uint[] CalculateSubMeshVertexOffset(GLTFMesh mesh, out uint totalVertCount)
|
||||
{
|
||||
Dictionary<int, AccessorId> accessorIds = new Dictionary<int, AccessorId>();
|
||||
uint vOffset = 0;
|
||||
int primIndex = 0;
|
||||
uint[] vertOffsetBySubMesh = new uint[mesh.Primitives.Count];
|
||||
totalVertCount = 0;
|
||||
uint lastVertOffset = 0;
|
||||
foreach (var p in mesh.Primitives)
|
||||
{
|
||||
var acc = p.Attributes[SemanticProperties.POSITION];
|
||||
if (!accessorIds.ContainsKey(acc.Id))
|
||||
{
|
||||
accessorIds.Add(acc.Id, acc);
|
||||
totalVertCount += acc.Value.Count;
|
||||
vOffset = lastVertOffset;
|
||||
lastVertOffset += acc.Value.Count;
|
||||
}
|
||||
|
||||
vertOffsetBySubMesh[primIndex] = vOffset;
|
||||
|
||||
primIndex++;
|
||||
}
|
||||
|
||||
return vertOffsetBySubMesh;
|
||||
}
|
||||
|
||||
protected void PrepareUnityMeshData()
|
||||
{
|
||||
if (_gltfRoot.Meshes == null)
|
||||
return;
|
||||
|
||||
for (int i = 0; i < _gltfRoot.Meshes.Count(); i++)
|
||||
{
|
||||
int meshIndex = i;
|
||||
var mesh = _gltfRoot.Meshes[meshIndex];
|
||||
var meshCache = _assetCache.MeshCache[meshIndex];
|
||||
var unityData = CreateUnityMeshData(mesh, meshIndex);
|
||||
for (int primIndex = 0; primIndex < mesh.Primitives.Count; ++primIndex)
|
||||
{
|
||||
var primitive = mesh.Primitives[primIndex];
|
||||
var primCache = meshCache.Primitives[primIndex];
|
||||
unityData.Topology[primIndex] = GetTopology(primitive.Mode);
|
||||
unityData.DrawModes[primIndex] = primitive.Mode;
|
||||
|
||||
ConvertAttributeAccessorsToUnityTypes(primCache, unityData,
|
||||
unityData.subMeshVertexOffset[primIndex], primIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if HAVE_DRACO
|
||||
protected virtual async Task PrepareDracoMesh(GLTFMesh mesh, int meshIndex)
|
||||
{
|
||||
if (_assetCache.MeshCache[meshIndex].DracoMeshDataPrepared)
|
||||
return;
|
||||
|
||||
Mesh.MeshDataArray meshDataArray = Mesh.AllocateWritableMeshData(mesh.Primitives.Count);
|
||||
_assetCache.MeshCache[meshIndex].DracoMeshData = meshDataArray;
|
||||
|
||||
_assetCache.MeshCache[meshIndex].DracoMeshDecodeResult = new DecodeResult[mesh.Primitives.Count];
|
||||
|
||||
_assetCache.MeshCache[meshIndex].DracoMeshDataPrepared = true;
|
||||
for (int i = 0; i < mesh.Primitives.Count; i++)
|
||||
{
|
||||
var primitive = mesh.Primitives[i];
|
||||
if (primitive.Extensions == null || !primitive.Extensions.ContainsKey("KHR_draco_mesh_compression"))
|
||||
continue;
|
||||
|
||||
if (primitive.Extensions.TryGetValue("KHR_draco_mesh_compression", out var extension))
|
||||
{
|
||||
var dracoExtension = (KHR_draco_mesh_compression)extension;
|
||||
if (_assetCache.BufferCache[dracoExtension.bufferView.Value.Buffer.Id] == null)
|
||||
await ConstructBuffer(dracoExtension.bufferView.Value.Buffer.Value,
|
||||
dracoExtension.bufferView.Value.Buffer.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual DracoDecodeResult ConstructDracoMesh(GLTFMesh mesh, int meshIndex, CancellationToken cancellationToken)
|
||||
{
|
||||
var firstPrim = mesh.Primitives.Count > 0 ? mesh.Primitives[0] : null;
|
||||
|
||||
DracoDecodeResult decodeResult = new DracoDecodeResult
|
||||
{
|
||||
meshIndex = meshIndex,
|
||||
decodeResults = new Task<DecodeResult>[_assetCache.MeshCache[meshIndex].DracoMeshDecodeResult.Length]
|
||||
};
|
||||
|
||||
if (!_assetCache.MeshCache[meshIndex].DracoMeshDataPrepared)
|
||||
{
|
||||
Debug.Log(LogType.Error, $"Draco Mesh Data is not prepared! Call PrepareDracoMesh first (File: {_gltfFileName})");
|
||||
return new DracoDecodeResult();
|
||||
}
|
||||
|
||||
if (_assetCache.MeshCache[meshIndex].HasDracoMeshData)
|
||||
{
|
||||
return new DracoDecodeResult();
|
||||
}
|
||||
|
||||
_assetCache.MeshCache[meshIndex].HasDracoMeshData = true;
|
||||
|
||||
for (int i = 0; i < mesh.Primitives.Count; i++)
|
||||
{
|
||||
var primitive = mesh.Primitives[i];
|
||||
if (primitive.Extensions == null || !primitive.Extensions.ContainsKey("KHR_draco_mesh_compression"))
|
||||
continue;
|
||||
|
||||
if (primitive.Extensions.TryGetValue("KHR_draco_mesh_compression", out var extension))
|
||||
{
|
||||
var dracoExtension = (KHR_draco_mesh_compression)extension;
|
||||
|
||||
BufferCacheData bufferContents =
|
||||
_assetCache.BufferCache[dracoExtension.bufferView.Value.Buffer.Id];
|
||||
|
||||
GLTFHelpers.LoadBufferView(dracoExtension.bufferView.Value, bufferContents.ChunkOffset,
|
||||
bufferContents.bufferData, out NativeArray<byte> bufferViewData);
|
||||
|
||||
int weightsAttributeId = -1;
|
||||
if (dracoExtension.attributes.TryGetValue("WEIGHTS_0", out var attribute))
|
||||
weightsAttributeId = (int)attribute;
|
||||
|
||||
int jointsAttributeId = -1;
|
||||
if (dracoExtension.attributes.TryGetValue("JOINTS_0", out var extensionAttribute))
|
||||
jointsAttributeId = (int)extensionAttribute;
|
||||
|
||||
// TODO: check if normals and tangents are needed
|
||||
#pragma warning disable 0219
|
||||
bool hasTangents = dracoExtension.attributes.ContainsKey(SemanticProperties.TANGENT);
|
||||
|
||||
bool needsTangents = _options.ImportTangents != GLTFImporterNormals.None && hasTangents;
|
||||
bool needsNormals = _options.ImportNormals != GLTFImporterNormals.None || needsTangents;
|
||||
#pragma warning restore 0219
|
||||
|
||||
#if HAVE_DRACO_VERSION_5
|
||||
DecodeSettings decodeSettings = DecodeSettings.ConvertSpace;
|
||||
if (needsNormals)
|
||||
decodeSettings |= DecodeSettings.RequireNormals;
|
||||
if (needsTangents)
|
||||
decodeSettings |= DecodeSettings.RequireTangents;
|
||||
if (firstPrim != null && firstPrim.Targets != null)
|
||||
decodeSettings |= DecodeSettings.ForceUnityVertexLayout;
|
||||
|
||||
var attrMap = DracoDecoder.CreateAttributeIdMap(weightsAttributeId, jointsAttributeId);
|
||||
if (attrMap == null)
|
||||
attrMap = new Dictionary<VertexAttribute, int>();
|
||||
|
||||
if (hasTangents)
|
||||
attrMap.Add( VertexAttribute.Tangent, dracoExtension.attributes[SemanticProperties.TANGENT]);
|
||||
|
||||
if (dracoExtension.attributes.TryGetValue(SemanticProperties.COLOR_0, out var colorAttr))
|
||||
attrMap.Add( VertexAttribute.Color, colorAttr);
|
||||
|
||||
if (dracoExtension.attributes.TryGetValue(SemanticProperties.TEXCOORD_0, out var uvAttr))
|
||||
attrMap.Add( VertexAttribute.TexCoord0, uvAttr);
|
||||
|
||||
if (dracoExtension.attributes.TryGetValue(SemanticProperties.TEXCOORD_1, out var uv2Attr))
|
||||
attrMap.Add( VertexAttribute.TexCoord1, uv2Attr);
|
||||
|
||||
if (dracoExtension.attributes.TryGetValue(SemanticProperties.NORMAL, out var normalAttr))
|
||||
attrMap.Add( VertexAttribute.Normal, normalAttr);
|
||||
|
||||
if (dracoExtension.attributes.TryGetValue(SemanticProperties.POSITION, out var positionAttr))
|
||||
attrMap.Add( VertexAttribute.Position, positionAttr);
|
||||
|
||||
decodeResult.decodeResults[i] = DracoDecoder.DecodeMesh( _assetCache.MeshCache[meshIndex].DracoMeshData[i], bufferViewData, decodeSettings, attrMap);
|
||||
|
||||
#else
|
||||
var draco = new DracoMeshLoader();
|
||||
|
||||
decodeResult.decodeResults[i] = draco.ConvertDracoMeshToUnity(_assetCache.MeshCache[meshIndex].DracoMeshData[i], bufferViewData,
|
||||
needsNormals, needsTangents,
|
||||
weightsAttributeId, jointsAttributeId, firstPrim.Targets != null);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
return decodeResult;
|
||||
}
|
||||
|
||||
private async Task BuildUnityDracoMesh(GLTFMesh mesh, int meshIndex)
|
||||
{
|
||||
if (!_assetCache.MeshCache[meshIndex].HasDracoMeshData)
|
||||
{
|
||||
Debug.Log(LogType.Error, $"Draco Mesh Data is not decoded! Call ConstructDracoMesh first (File: {_gltfFileName})");
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var primitive in mesh.Primitives)
|
||||
await CreateMaterials(primitive);
|
||||
|
||||
await ConstructUnityMesh(mesh, _assetCache.MeshCache[meshIndex].DracoMeshDecodeResult, _assetCache.MeshCache[meshIndex].DracoMeshData, meshIndex, mesh.Name);
|
||||
}
|
||||
#endif
|
||||
|
||||
#if HAVE_MESHOPT_DECOMPRESS
|
||||
private async Task MeshOptDecodeBuffer(GLTFRoot root)
|
||||
{
|
||||
if (root.BufferViews == null)
|
||||
return;
|
||||
|
||||
int bufferViewIndex = 0;
|
||||
var jobHandlesList = new List<JobHandle>(root.BufferViews.Count);
|
||||
var meshOptBufferViews = new Dictionary<int, NativeArray<byte>>();
|
||||
var meshOptReturnValues = new NativeArray<int>( root.BufferViews.Count, Allocator.TempJob);
|
||||
|
||||
foreach (var bView in root.BufferViews)
|
||||
{
|
||||
if (bView.Extensions != null && bView.Extensions.ContainsKey(EXT_meshopt_compression_Factory.EXTENSION_NAME))
|
||||
{
|
||||
var meshOpt = bView.Extensions[EXT_meshopt_compression_Factory.EXTENSION_NAME] as EXT_meshopt_compression;
|
||||
|
||||
var arr = new NativeArray<byte>((int)(meshOpt.count * meshOpt.bufferView.ByteStride), Allocator.TempJob);
|
||||
|
||||
if (_assetCache.BufferCache[meshOpt.bufferView.Buffer.Id] == null)
|
||||
await ConstructBuffer(meshOpt.bufferView.Buffer.Value, meshOpt.bufferView.Buffer.Id);
|
||||
|
||||
BufferCacheData bufferContents = _assetCache.BufferCache[meshOpt.bufferView.Buffer.Id];
|
||||
|
||||
GLTFHelpers.LoadBufferView(meshOpt.bufferView, bufferContents.ChunkOffset, bufferContents.bufferData, out NativeArray<byte> bufferViewData);
|
||||
|
||||
var jobHandle = Meshoptimizer.Decode.DecodeGltfBuffer(
|
||||
new NativeSlice<int>(meshOptReturnValues,bufferViewIndex,1),
|
||||
arr,
|
||||
meshOpt.count,
|
||||
(int)meshOpt.bufferView.ByteStride,
|
||||
bufferViewData,
|
||||
meshOpt.mode,
|
||||
meshOpt.filter
|
||||
);
|
||||
|
||||
jobHandlesList.Add(jobHandle);
|
||||
meshOptBufferViews[bufferViewIndex] = arr;
|
||||
}
|
||||
|
||||
bufferViewIndex++;
|
||||
}
|
||||
|
||||
if (jobHandlesList.Count > 0)
|
||||
{
|
||||
JobHandle meshoptJobHandle;
|
||||
using (var jobHandles = new NativeArray<JobHandle>(jobHandlesList.ToArray(), Allocator.TempJob))
|
||||
{
|
||||
meshoptJobHandle = JobHandle.CombineDependencies(jobHandles);
|
||||
}
|
||||
while (!meshoptJobHandle.IsCompleted)
|
||||
{
|
||||
await Task.Yield();
|
||||
}
|
||||
meshoptJobHandle.Complete();
|
||||
}
|
||||
|
||||
foreach (var m in meshOptBufferViews)
|
||||
{
|
||||
var bufferView = root.BufferViews[m.Key];
|
||||
var bufferData = await GetBufferData(bufferView.Buffer);
|
||||
NativeArray<byte>.Copy(m.Value, 0, bufferData.bufferData, (int)bufferView.ByteOffset, m.Value.Length);
|
||||
m.Value.Dispose();
|
||||
}
|
||||
|
||||
meshOptReturnValues.Dispose();
|
||||
}
|
||||
#endif
|
||||
|
||||
protected void ApplyImportOptionsOnMesh(Mesh mesh)
|
||||
{
|
||||
bool isTriangleTopology = mesh.GetTopology(0) == MeshTopology.Triangles;
|
||||
|
||||
if (_options.ImportNormals == GLTFImporterNormals.None)
|
||||
mesh.normals = Array.Empty<Vector3>();
|
||||
else
|
||||
if (isTriangleTopology)
|
||||
{
|
||||
if (_options.ImportNormals == GLTFImporterNormals.Calculate)
|
||||
mesh.RecalculateNormals();
|
||||
else if (_options.ImportNormals == GLTFImporterNormals.Import && mesh.normals.Length == 0)
|
||||
mesh.RecalculateNormals();
|
||||
else if (_options.ImportTangents != GLTFImporterNormals.None && mesh.normals.Length == 0)
|
||||
mesh.RecalculateNormals();
|
||||
}
|
||||
|
||||
if (_options.ImportTangents == GLTFImporterNormals.None)
|
||||
mesh.tangents = Array.Empty<Vector4>();
|
||||
else
|
||||
if (isTriangleTopology)
|
||||
{
|
||||
if (_options.ImportTangents == GLTFImporterNormals.Calculate)
|
||||
mesh.RecalculateTangents();
|
||||
else if (_options.ImportTangents == GLTFImporterNormals.Import && mesh.tangents.Length == 0)
|
||||
mesh.RecalculateTangents();
|
||||
}
|
||||
|
||||
if (_options.SwapUVs)
|
||||
{
|
||||
var uv = mesh.uv;
|
||||
var uv2 = mesh.uv2;
|
||||
mesh.uv = uv2;
|
||||
if(uv.Length > 0)
|
||||
mesh.uv2 = uv;
|
||||
}
|
||||
}
|
||||
|
||||
#if HAVE_DRACO
|
||||
/// <summary>
|
||||
/// Populate a UnityEngine.Mesh from Draco generated SubMeshes
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
protected async Task ConstructUnityMesh(GLTFMesh gltfMesh, DecodeResult[] decodeResults, Mesh.MeshDataArray meshes, int meshIndex, string meshName)
|
||||
{
|
||||
uint verticesLength = 0;
|
||||
for (int i = 0; i < meshes.Length; i++)
|
||||
verticesLength+= (uint)meshes[i].vertexCount;
|
||||
|
||||
Mesh mesh = new Mesh
|
||||
{
|
||||
name = meshName,
|
||||
#if UNITY_2017_3_OR_NEWER
|
||||
indexFormat = verticesLength > 65535 ? IndexFormat.UInt32 : IndexFormat.UInt16,
|
||||
#endif
|
||||
};
|
||||
|
||||
Mesh[] subMeshes = new Mesh[meshes.Length];
|
||||
for (int i = 0; i < subMeshes.Length; i++)
|
||||
subMeshes[i] = new Mesh();
|
||||
|
||||
Mesh.ApplyAndDisposeWritableMeshData(meshes, subMeshes);
|
||||
|
||||
CombineInstance[] combineInstances = new CombineInstance[subMeshes.Length];
|
||||
for (int i = 0; i < combineInstances.Length; i++)
|
||||
{
|
||||
combineInstances[i] = new CombineInstance();
|
||||
combineInstances[i].mesh = subMeshes[i];
|
||||
}
|
||||
|
||||
int boneWeightCount = 0;
|
||||
int bonesPerVertexCount = 0;
|
||||
for (int i = 0; i < subMeshes.Length; i++)
|
||||
{
|
||||
if (decodeResults[i].boneWeightData == null) continue;
|
||||
decodeResults[i].boneWeightData.ApplyOnMesh(subMeshes[i]);
|
||||
decodeResults[i].boneWeightData.Dispose();
|
||||
|
||||
boneWeightCount += subMeshes[i].GetAllBoneWeights().Length;
|
||||
bonesPerVertexCount += subMeshes[i].GetBonesPerVertex().Length;
|
||||
}
|
||||
|
||||
// Custom combine all boneweights and bonePerVertex of sub meshes and apply to final combined mesh
|
||||
// >> Bug(?) in CombineMeshes that does not proper copy bone weights and bones per vertex
|
||||
NativeArray<BoneWeight1> allBoneWeights = new NativeArray<BoneWeight1>(boneWeightCount, Allocator.TempJob);
|
||||
NativeArray<byte> allBonesPerVertex = new NativeArray<byte>(bonesPerVertexCount, Allocator.TempJob);
|
||||
int currentArrayPositionBoneWeights = 0;
|
||||
int currentArrayPositionBpV = 0;
|
||||
for (int i = 0; i < subMeshes.Length; i++)
|
||||
{
|
||||
var subMeshBoneWeights = subMeshes[i].GetAllBoneWeights();
|
||||
var subMeshBonesPerVertex = subMeshes[i].GetBonesPerVertex();
|
||||
|
||||
NativeArray<BoneWeight1>.Copy(subMeshBoneWeights, 0, allBoneWeights, currentArrayPositionBoneWeights, subMeshBoneWeights.Length);
|
||||
NativeArray<byte>.Copy(subMeshBonesPerVertex, 0, allBonesPerVertex, currentArrayPositionBpV, subMeshBonesPerVertex.Length);
|
||||
|
||||
currentArrayPositionBoneWeights += subMeshBoneWeights.Length;
|
||||
currentArrayPositionBpV += subMeshBonesPerVertex.Length;
|
||||
}
|
||||
|
||||
mesh.CombineMeshes(combineInstances, false, false);
|
||||
|
||||
mesh.SetBoneWeights(allBonesPerVertex, allBoneWeights);
|
||||
allBoneWeights.Dispose();
|
||||
allBonesPerVertex.Dispose();
|
||||
|
||||
foreach (var m in subMeshes)
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
UnityEngine.Object.DestroyImmediate(m);
|
||||
#else
|
||||
UnityEngine.Object.Destroy(m);
|
||||
#endif
|
||||
}
|
||||
|
||||
await YieldOnTimeoutAndThrowOnLowMemory();
|
||||
|
||||
var unityMeshData = CreateUnityMeshData(gltfMesh, meshIndex, true);
|
||||
|
||||
uint vertOffset = 0;
|
||||
var meshCache = _assetCache.MeshCache[meshIndex];
|
||||
|
||||
unityMeshData.BoneWeights = mesh.boneWeights;
|
||||
|
||||
for (int i = 0; i < gltfMesh.Primitives.Count; i++)
|
||||
{
|
||||
var primCache = meshCache.Primitives[i];
|
||||
if (IsMultithreaded)
|
||||
{
|
||||
await Task.Run(() => ConvertAttributeAccessorsToUnityTypes(primCache, unityMeshData, vertOffset, i));
|
||||
}
|
||||
else
|
||||
{
|
||||
ConvertAttributeAccessorsToUnityTypes(primCache, unityMeshData, vertOffset, i);
|
||||
}
|
||||
vertOffset += (uint)mesh.GetSubMesh(i).vertexCount;
|
||||
}
|
||||
|
||||
mesh.RecalculateBounds();
|
||||
await YieldOnTimeoutAndThrowOnLowMemory();
|
||||
|
||||
AddBlendShapesToMesh(unityMeshData, meshIndex, mesh);
|
||||
await YieldOnTimeoutAndThrowOnLowMemory();
|
||||
|
||||
ApplyImportOptionsOnMesh(mesh);
|
||||
|
||||
if (!KeepCPUCopyOfMesh)
|
||||
{
|
||||
mesh.UploadMeshData(true);
|
||||
}
|
||||
|
||||
_assetCache.MeshCache[meshIndex].LoadedMesh = mesh;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
private UnityMeshData CreateUnityMeshData(GLTFMesh gltfMesh, int meshIndex, bool onlyMorphTargets = false)
|
||||
{
|
||||
if (_assetCache.UnityMeshDataCache[meshIndex] != null)
|
||||
{
|
||||
return _assetCache.UnityMeshDataCache[meshIndex];
|
||||
}
|
||||
var vertOffsetBySubMesh = CalculateSubMeshVertexOffset(gltfMesh, out var verticesLength);
|
||||
|
||||
UnityMeshData unityMeshData = new UnityMeshData()
|
||||
{
|
||||
|
||||
Topology = new MeshTopology[gltfMesh.Primitives.Count],
|
||||
DrawModes = new DrawMode[gltfMesh.Primitives.Count],
|
||||
Indices = new int[gltfMesh.Primitives.Count][],
|
||||
subMeshDataCreated = new bool[gltfMesh.Primitives.Count],
|
||||
subMeshVertexOffset = vertOffsetBySubMesh
|
||||
};
|
||||
|
||||
for (int i = 0; i < unityMeshData.subMeshDataCreated.Length; i++)
|
||||
unityMeshData.subMeshDataCreated[i] = false;
|
||||
|
||||
var attributes = new HashSet<string>();
|
||||
bool hasTargets = false;
|
||||
int targetCount = 0;
|
||||
foreach (var prim in gltfMesh.Primitives)
|
||||
{
|
||||
if (prim.Targets != null)
|
||||
{
|
||||
hasTargets = true;
|
||||
targetCount = prim.Targets.Count;
|
||||
}
|
||||
|
||||
if (prim.Attributes == null)
|
||||
continue;
|
||||
|
||||
foreach (var attribute in prim.Attributes)
|
||||
attributes.Add(attribute.Key);
|
||||
}
|
||||
|
||||
if (hasTargets)
|
||||
{
|
||||
unityMeshData.MorphTargetVertices = new Vector3[targetCount][];
|
||||
unityMeshData.MorphTargetNormals = new Vector3[targetCount][];
|
||||
unityMeshData.MorphTargetTangents = new Vector3[targetCount][];
|
||||
|
||||
foreach (var prim in gltfMesh.Primitives)
|
||||
{
|
||||
for (int i = 0; i < prim.Targets.Count; i++)
|
||||
{
|
||||
if (unityMeshData.MorphTargetVertices[i] == null && prim.Targets[i].ContainsKey(SemanticProperties.POSITION))
|
||||
{
|
||||
unityMeshData.MorphTargetVertices[i] = new Vector3[verticesLength];
|
||||
for (int j = 0; j < verticesLength; j++)
|
||||
unityMeshData.MorphTargetVertices[i][j] = Vector3.zero;
|
||||
}
|
||||
|
||||
if (unityMeshData.MorphTargetNormals[i] == null && prim.Targets[i].ContainsKey(SemanticProperties.NORMAL))
|
||||
{
|
||||
unityMeshData.MorphTargetNormals[i] = new Vector3[verticesLength];
|
||||
for (int j = 0; j < verticesLength; j++)
|
||||
unityMeshData.MorphTargetNormals[i][j] = Vector3.zero;
|
||||
}
|
||||
|
||||
if (unityMeshData.MorphTargetTangents[i] == null && prim.Targets[i].ContainsKey(SemanticProperties.TANGENT))
|
||||
{
|
||||
unityMeshData.MorphTargetTangents[i] = new Vector3[verticesLength];
|
||||
for (int j = 0; j < verticesLength; j++)
|
||||
unityMeshData.MorphTargetTangents[i][j] = Vector3.zero;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_assetCache.UnityMeshDataCache[meshIndex] = unityMeshData;
|
||||
|
||||
if (!onlyMorphTargets)
|
||||
{
|
||||
unityMeshData.Vertices = new Vector3[verticesLength];
|
||||
unityMeshData.Normals = attributes.Contains(SemanticProperties.NORMAL)
|
||||
? new Vector3[verticesLength]
|
||||
: null;
|
||||
unityMeshData.Tangents = attributes.Contains(SemanticProperties.TANGENT)
|
||||
? new Vector4[verticesLength]
|
||||
: null;
|
||||
unityMeshData.Uv1 = attributes.Contains(SemanticProperties.TEXCOORD_0)
|
||||
? new Vector2[verticesLength]
|
||||
: null;
|
||||
unityMeshData.Uv2 = attributes.Contains(SemanticProperties.TEXCOORD_1)
|
||||
? new Vector2[verticesLength]
|
||||
: null;
|
||||
unityMeshData.Uv3 = attributes.Contains(SemanticProperties.TEXCOORD_2)
|
||||
? new Vector2[verticesLength]
|
||||
: null;
|
||||
unityMeshData.Uv4 = attributes.Contains(SemanticProperties.TEXCOORD_3)
|
||||
? new Vector2[verticesLength]
|
||||
: null;
|
||||
unityMeshData.Colors = attributes.Contains(SemanticProperties.COLOR_0)
|
||||
? new Color[verticesLength]
|
||||
: null;
|
||||
unityMeshData.BoneWeights = attributes.Contains(SemanticProperties.WEIGHTS_0)
|
||||
? new BoneWeight[verticesLength]
|
||||
: null;
|
||||
}
|
||||
|
||||
return unityMeshData;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Populate a UnityEngine.Mesh from preloaded and preprocessed buffer data
|
||||
/// </summary>
|
||||
/// <param name="unityMeshData"></param>
|
||||
/// <param name="meshIndex"></param>
|
||||
/// <param name="meshName"></param>
|
||||
/// <returns></returns>
|
||||
protected async Task ConstructUnityMesh(UnityMeshData unityMeshData, int meshIndex, string meshName)
|
||||
{
|
||||
await YieldOnTimeoutAndThrowOnLowMemory();
|
||||
Mesh mesh = new Mesh
|
||||
{
|
||||
name = meshName,
|
||||
#if UNITY_2017_3_OR_NEWER
|
||||
indexFormat = unityMeshData.Vertices.Length > 65535 ? IndexFormat.UInt32 : IndexFormat.UInt16,
|
||||
#endif
|
||||
};
|
||||
|
||||
mesh.vertices = unityMeshData.Vertices;
|
||||
await YieldOnTimeoutAndThrowOnLowMemory();
|
||||
mesh.normals = unityMeshData.Normals;
|
||||
await YieldOnTimeoutAndThrowOnLowMemory();
|
||||
mesh.tangents = unityMeshData.Tangents;
|
||||
await YieldOnTimeoutAndThrowOnLowMemory();
|
||||
mesh.uv = unityMeshData.Uv1;
|
||||
await YieldOnTimeoutAndThrowOnLowMemory();
|
||||
mesh.uv2 = unityMeshData.Uv2;
|
||||
await YieldOnTimeoutAndThrowOnLowMemory();
|
||||
mesh.uv3 = unityMeshData.Uv3;
|
||||
await YieldOnTimeoutAndThrowOnLowMemory();
|
||||
mesh.uv4 = unityMeshData.Uv4;
|
||||
await YieldOnTimeoutAndThrowOnLowMemory();
|
||||
mesh.colors = unityMeshData.Colors;
|
||||
await YieldOnTimeoutAndThrowOnLowMemory();
|
||||
mesh.boneWeights = unityMeshData.BoneWeights;
|
||||
await YieldOnTimeoutAndThrowOnLowMemory();
|
||||
|
||||
mesh.subMeshCount = unityMeshData.Indices.Length;
|
||||
for (int i = 0; i < unityMeshData.Indices.Length; i++)
|
||||
{
|
||||
mesh.SetIndices(unityMeshData.Indices[i], unityMeshData.Topology[i], i, false,
|
||||
(int)unityMeshData.subMeshVertexOffset[i]);
|
||||
}
|
||||
mesh.RecalculateBounds();
|
||||
await YieldOnTimeoutAndThrowOnLowMemory();
|
||||
|
||||
AddBlendShapesToMesh(unityMeshData, meshIndex, mesh);
|
||||
await YieldOnTimeoutAndThrowOnLowMemory();
|
||||
|
||||
ApplyImportOptionsOnMesh(mesh);
|
||||
|
||||
if (!KeepCPUCopyOfMesh)
|
||||
{
|
||||
mesh.UploadMeshData(true);
|
||||
}
|
||||
|
||||
_assetCache.MeshCache[meshIndex].LoadedMesh = mesh;
|
||||
|
||||
// Free up some memory
|
||||
unityMeshData.Clear();
|
||||
}
|
||||
|
||||
private void AddBlendShapesToMesh(UnityMeshData unityMeshData, int meshIndex, Mesh mesh)
|
||||
{
|
||||
if (unityMeshData.MorphTargetVertices != null && _gltfRoot.Meshes != null)
|
||||
{
|
||||
var gltfMesh = _gltfRoot.Meshes[meshIndex];
|
||||
var firstPrim = gltfMesh.Primitives[0];
|
||||
// TODO theoretically there could be multiple prims and only one of them has morph targets
|
||||
for (int i = 0; i < firstPrim.Targets.Count; i++)
|
||||
{
|
||||
var targetName = _options.ImportBlendShapeNames ? ((gltfMesh.TargetNames != null && gltfMesh.TargetNames.Count > i) ? gltfMesh.TargetNames[i] : $"Morphtarget{i}") : i.ToString();
|
||||
mesh.AddBlendShapeFrame(targetName, 1f * _options.BlendShapeFrameWeight,
|
||||
unityMeshData.MorphTargetVertices[i],
|
||||
unityMeshData.MorphTargetNormals != null && unityMeshData.MorphTargetNormals[i] != null ? unityMeshData.MorphTargetNormals[i] : null,
|
||||
unityMeshData.MorphTargetTangents != null && unityMeshData.MorphTargetTangents[i] != null ? unityMeshData.MorphTargetTangents[i] : null
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual async Task ConstructMeshTargetsPrepareBuffers(MeshPrimitive primitive, int meshIndex, int primitiveIndex)
|
||||
{
|
||||
var newTargets = new List<Dictionary<string, AttributeAccessor>>(primitive.Targets.Count);
|
||||
_assetCache.MeshCache[meshIndex].Primitives[primitiveIndex].Targets = newTargets;
|
||||
|
||||
// Prepare Buffer Data
|
||||
for (int i = 0; i < primitive.Targets.Count; i++)
|
||||
{
|
||||
var target = primitive.Targets[i];
|
||||
newTargets.Add(new Dictionary<string, AttributeAccessor>());
|
||||
|
||||
foreach (var targetAttribute in target)
|
||||
{
|
||||
BufferId bufferIdPair = null;
|
||||
if (targetAttribute.Value.Value.Sparse == null)
|
||||
{
|
||||
bufferIdPair = targetAttribute.Value.Value.BufferView.Value.Buffer;
|
||||
}
|
||||
else
|
||||
{
|
||||
// When using Draco, it's possible the BufferView is null
|
||||
if (primitive.Attributes[targetAttribute.Key].Value.BufferView == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
bufferIdPair = primitive.Attributes[targetAttribute.Key].Value.BufferView.Value.Buffer;
|
||||
targetAttribute.Value.Value.BufferView =
|
||||
primitive.Attributes[targetAttribute.Key].Value.BufferView;
|
||||
}
|
||||
|
||||
GLTFBuffer buffer = bufferIdPair.Value;
|
||||
int bufferID = bufferIdPair.Id;
|
||||
if (_assetCache.BufferCache[bufferID] == null)
|
||||
{
|
||||
await ConstructBuffer(buffer, bufferID);
|
||||
}
|
||||
|
||||
newTargets[i][targetAttribute.Key] = new AttributeAccessor
|
||||
{
|
||||
AccessorId = targetAttribute.Value,
|
||||
bufferData = _assetCache.BufferCache[bufferID].bufferData,
|
||||
Offset = (uint)_assetCache.BufferCache[bufferID].ChunkOffset
|
||||
};
|
||||
|
||||
// if this buffer isn't sparse, we're done here
|
||||
if (targetAttribute.Value.Value.Sparse == null) continue;
|
||||
|
||||
var bufferId = targetAttribute.Value.Value.Sparse.Values.BufferView.Value.Buffer;
|
||||
await GetBufferData(bufferId);
|
||||
|
||||
bufferId = targetAttribute.Value.Value.Sparse.Indices.BufferView.Value.Buffer;
|
||||
await GetBufferData(bufferId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void ConstructMeshTargets(MeshPrimitive primitive, int meshIndex, int primitiveIndex)
|
||||
{
|
||||
float scaleFactor = 0f;
|
||||
bool hasScale = false;
|
||||
#if UNITY_EDITOR
|
||||
hasScale = Context != null && !Mathf.Approximately(Context.ImportScaleFactor, 1f);
|
||||
scaleFactor = hasScale ? Context.ImportScaleFactor : 1f;
|
||||
#endif
|
||||
|
||||
var newTargets = _assetCache.MeshCache[meshIndex].Primitives[primitiveIndex].Targets;
|
||||
for (int i = 0; i < primitive.Targets.Count; i++)
|
||||
{
|
||||
var target = primitive.Targets[i];
|
||||
var att = newTargets[i];
|
||||
|
||||
NumericArray[] sparseNormals = null;
|
||||
NumericArray[] sparsePositions = null;
|
||||
NumericArray[] sparseTangents = null;
|
||||
|
||||
const string NormalKey = "NORMAL";
|
||||
const string PositionKey = "POSITION";
|
||||
const string TangentKey = "TANGENT";
|
||||
|
||||
// normals, positions, tangents
|
||||
foreach (var targetAttribute in target)
|
||||
{
|
||||
if (targetAttribute.Value.Value.Sparse != null)
|
||||
{
|
||||
// When using Draco, it's possible the BufferView is null
|
||||
if (primitive.Attributes[targetAttribute.Key].Value.BufferView == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// if this buffer isn't sparse, we're done here
|
||||
if (targetAttribute.Value.Value.Sparse == null) continue;
|
||||
|
||||
// Values
|
||||
var bufferId = targetAttribute.Value.Value.Sparse.Values.BufferView.Value.Buffer;
|
||||
var bufferData = _assetCache.BufferCache[bufferId.Id];
|
||||
AttributeAccessor sparseValues = new AttributeAccessor
|
||||
{
|
||||
AccessorId = targetAttribute.Value,
|
||||
bufferData = bufferData.bufferData,
|
||||
Offset = (uint)bufferData.ChunkOffset
|
||||
};
|
||||
GLTFHelpers.LoadBufferView(sparseValues.AccessorId.Value.Sparse.Values.BufferView.Value,
|
||||
sparseValues.Offset, sparseValues.bufferData, out NativeArray<byte> bufferViewCache1);
|
||||
|
||||
// Indices
|
||||
bufferId = targetAttribute.Value.Value.Sparse.Indices.BufferView.Value.Buffer;
|
||||
bufferData = _assetCache.BufferCache[bufferId.Id];
|
||||
AttributeAccessor sparseIndices = new AttributeAccessor
|
||||
{
|
||||
AccessorId = targetAttribute.Value,
|
||||
bufferData = bufferData.bufferData,
|
||||
Offset = (uint)bufferData.ChunkOffset,
|
||||
};
|
||||
GLTFHelpers.LoadBufferView(sparseIndices.AccessorId.Value.Sparse.Indices.BufferView.Value,
|
||||
sparseIndices.Offset, sparseIndices.bufferData, out NativeArray<byte> bufferViewCache2);
|
||||
|
||||
switch (targetAttribute.Key)
|
||||
{
|
||||
case NormalKey:
|
||||
sparseNormals = new NumericArray[2];
|
||||
Accessor.AsSparseFloat3Array(targetAttribute.Value.Value, ref sparseNormals[0],
|
||||
bufferViewCache1, 0, targetAttribute.Value.Value.Normalized);
|
||||
Accessor.AsSparseUIntArray(targetAttribute.Value.Value, ref sparseNormals[1],
|
||||
bufferViewCache2);
|
||||
break;
|
||||
case PositionKey:
|
||||
sparsePositions = new NumericArray[2];
|
||||
if (hasScale)
|
||||
Accessor.AsSparseFloat3ArrayConversion(targetAttribute.Value.Value, ref sparsePositions[0],
|
||||
bufferViewCache1, scaleFactor, 0, targetAttribute.Value.Value.Normalized);
|
||||
else
|
||||
Accessor.AsSparseFloat3Array(targetAttribute.Value.Value, ref sparsePositions[0],
|
||||
bufferViewCache1, 0, targetAttribute.Value.Value.Normalized);
|
||||
Accessor.AsSparseUIntArray(targetAttribute.Value.Value, ref sparsePositions[1],
|
||||
bufferViewCache2);
|
||||
break;
|
||||
case TangentKey:
|
||||
sparseTangents = new NumericArray[2];
|
||||
Accessor.AsSparseFloat3Array(targetAttribute.Value.Value, ref sparseTangents[0],
|
||||
bufferViewCache1, 0, targetAttribute.Value.Value.Normalized);
|
||||
Accessor.AsSparseUIntArray(targetAttribute.Value.Value, ref sparseTangents[1],
|
||||
bufferViewCache2);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
GLTFHelpers.BuildTargetAttributes(ref att, scaleFactor);
|
||||
|
||||
if (sparseNormals != null)
|
||||
{
|
||||
var current = att[NormalKey].AccessorContent;
|
||||
NumericArray before = new NumericArray();
|
||||
before.AsFloat3s = new float3[current.AsFloat3s.Length];
|
||||
for (int j = 0; j < sparseNormals[1].AsUInts.Length; j++)
|
||||
{
|
||||
before.AsFloat3s[sparseNormals[1].AsUInts[j]] = sparseNormals[0].AsFloat3s[j];
|
||||
}
|
||||
|
||||
att[NormalKey].AccessorContent = before;
|
||||
}
|
||||
|
||||
if (sparsePositions != null)
|
||||
{
|
||||
var current = att[PositionKey].AccessorContent;
|
||||
NumericArray before = new NumericArray();
|
||||
before.AsFloat3s = new float3[current.AsFloat3s.Length];
|
||||
for (int j = 0; j < sparsePositions[1].AsUInts.Length; j++)
|
||||
{
|
||||
before.AsFloat3s[sparsePositions[1].AsUInts[j]] = sparsePositions[0].AsFloat3s[j];
|
||||
}
|
||||
|
||||
att[PositionKey].AccessorContent = before;
|
||||
}
|
||||
|
||||
if (sparseTangents != null)
|
||||
{
|
||||
var current = att[TangentKey].AccessorContent;
|
||||
NumericArray before = new NumericArray();
|
||||
before.AsFloat3s = new float3[current.AsFloat3s.Length];
|
||||
for (int j = 0; j < sparseTangents[1].AsUInts.Length; j++)
|
||||
{
|
||||
before.AsFloat3s[sparseTangents[1].AsUInts[j]] = sparseTangents[0].AsFloat3s[j];
|
||||
}
|
||||
|
||||
att[TangentKey].AccessorContent = before;
|
||||
}
|
||||
|
||||
TransformTargets(ref att);
|
||||
}
|
||||
}
|
||||
|
||||
private void FreeUpAccessorContents()
|
||||
{
|
||||
if (_gltfRoot.Meshes == null)
|
||||
return;
|
||||
|
||||
for (int meshIndex = 0; meshIndex < _gltfRoot.Meshes.Count; meshIndex++)
|
||||
{
|
||||
var gltfMesh = _gltfRoot.Meshes[meshIndex];
|
||||
for (int primIndex = 0; primIndex < gltfMesh.Primitives.Count; primIndex++)
|
||||
{
|
||||
var primCache = _assetCache.MeshCache[meshIndex].Primitives[primIndex];
|
||||
if (primCache.meshAttributesCreated)
|
||||
{
|
||||
foreach (var att in primCache.Attributes)
|
||||
{
|
||||
att.Value.AccessorContent = new NumericArray();
|
||||
}
|
||||
|
||||
foreach (var t in primCache.SparseAccessors)
|
||||
{
|
||||
t.Value.sparseValues.AccessorContent = new NumericArray();
|
||||
t.Value.sparseIndices.AccessorContent = new NumericArray();
|
||||
}
|
||||
|
||||
foreach (var t in primCache.Targets)
|
||||
{
|
||||
foreach (var att in t)
|
||||
{
|
||||
att.Value.AccessorContent = new NumericArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task PreparePrimitiveAttributes()
|
||||
{
|
||||
if (_gltfRoot.Meshes == null)
|
||||
return;
|
||||
|
||||
for (int meshIndex = 0; meshIndex < _gltfRoot.Meshes.Count; meshIndex++)
|
||||
{
|
||||
if (_assetCache.MeshCache[meshIndex] == null)
|
||||
_assetCache.MeshCache[meshIndex] = new MeshCacheData();
|
||||
|
||||
var gltfMesh = _gltfRoot.Meshes[meshIndex];
|
||||
for (int i = 0; i < gltfMesh.Primitives.Count; i++)
|
||||
{
|
||||
await ConstructPrimitiveAttributes(gltfMesh.Primitives[i], meshIndex, i);
|
||||
if (gltfMesh.Primitives[i].Targets != null)
|
||||
await ConstructMeshTargetsPrepareBuffers(gltfMesh.Primitives[i], meshIndex, i);
|
||||
}
|
||||
|
||||
#if HAVE_DRACO
|
||||
if (Context.TryGetPlugin<DracoImportContext>(out _))
|
||||
{
|
||||
var anyHadDraco = gltfMesh.Primitives.Any(p =>
|
||||
p.Extensions != null &&
|
||||
p.Extensions.ContainsKey(KHR_draco_mesh_compression_Factory.EXTENSION_NAME));
|
||||
if (anyHadDraco)
|
||||
{
|
||||
await PrepareDracoMesh(gltfMesh, meshIndex);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#if HAVE_DRACO
|
||||
if (Context.TryGetPlugin<DracoImportContext>(out _))
|
||||
{
|
||||
|
||||
List<DracoDecodeResult> dracoDecodeResults = new List<DracoDecodeResult>();
|
||||
for (int meshIndex = 0; meshIndex < _gltfRoot.Meshes.Count; meshIndex++)
|
||||
{
|
||||
var gltfMesh = _gltfRoot.Meshes[meshIndex];
|
||||
var anyHadDraco = gltfMesh.Primitives.Any(p =>
|
||||
p.Extensions != null &&
|
||||
p.Extensions.ContainsKey(KHR_draco_mesh_compression_Factory.EXTENSION_NAME));
|
||||
|
||||
if (anyHadDraco)
|
||||
{
|
||||
dracoDecodeResults.Add(ConstructDracoMesh(gltfMesh, meshIndex, CancellationToken.None));
|
||||
}
|
||||
}
|
||||
|
||||
await Task.WhenAll(dracoDecodeResults.Select(d => d.decodeResults).SelectMany(d => d));
|
||||
|
||||
for (int i = 0; i < dracoDecodeResults.Count; i++)
|
||||
{
|
||||
int meshIndex = dracoDecodeResults[i].meshIndex;
|
||||
|
||||
for (int j = 0; j < dracoDecodeResults[i].decodeResults.Length; j++)
|
||||
{
|
||||
var decodeResult = dracoDecodeResults[i].decodeResults[j].Result;
|
||||
_assetCache.MeshCache[dracoDecodeResults[i].meshIndex].DracoMeshDecodeResult[j] = decodeResult;
|
||||
|
||||
if (!decodeResult.success)
|
||||
{
|
||||
Debug.Log(LogType.Error, $"Error decoding draco mesh (File: {_gltfFileName})", this);
|
||||
_assetCache.MeshCache[meshIndex].DracoMeshData.Dispose();
|
||||
}
|
||||
|
||||
Statistics.VertexCount += _assetCache.MeshCache[meshIndex].DracoMeshData[j].vertexCount;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
#endif
|
||||
void BuildMeshesAttributes()
|
||||
{
|
||||
for (int meshIndex = 0; meshIndex < _gltfRoot.Meshes.Count; meshIndex++)
|
||||
{
|
||||
var gltfMesh = _gltfRoot.Meshes[meshIndex];
|
||||
for (int primIndex = 0; primIndex < gltfMesh.Primitives.Count; primIndex++)
|
||||
{
|
||||
var primCache = _assetCache.MeshCache[meshIndex].Primitives[primIndex];
|
||||
if (!primCache.meshAttributesCreated)
|
||||
{
|
||||
primCache.meshAttributesCreated = true;
|
||||
GLTFHelpers.BuildMeshAttributes(ref primCache.Attributes,ref primCache.SparseAccessors);
|
||||
if (gltfMesh.Primitives[primIndex].Targets != null)
|
||||
ConstructMeshTargets(gltfMesh.Primitives[primIndex], meshIndex, primIndex);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if (IsMultithreaded)
|
||||
await Task.Run(BuildMeshesAttributes);
|
||||
else
|
||||
BuildMeshesAttributes();
|
||||
}
|
||||
|
||||
private async Task ConstructMeshAttributes(GLTFMesh mesh, MeshId meshId)
|
||||
{
|
||||
int meshIndex = meshId.Id;
|
||||
|
||||
if (_assetCache.MeshCache[meshIndex] == null)
|
||||
_assetCache.MeshCache[meshIndex] = new MeshCacheData();
|
||||
|
||||
for (int i = 0; i < mesh.Primitives.Count; ++i)
|
||||
{
|
||||
MeshPrimitive primitive = mesh.Primitives[i];
|
||||
|
||||
await ConstructPrimitiveAttributes(primitive, meshIndex, i);
|
||||
var primCache = _assetCache.MeshCache[meshIndex].Primitives[i];
|
||||
if (!primCache.meshAttributesCreated)
|
||||
{
|
||||
primCache.meshAttributesCreated = true;
|
||||
GLTFHelpers.BuildMeshAttributes(ref primCache.Attributes, ref primCache.SparseAccessors);
|
||||
|
||||
if (primitive.Targets != null)
|
||||
{
|
||||
// read mesh primitive targets into assetcache
|
||||
await ConstructMeshTargetsPrepareBuffers(primitive, meshIndex, i);
|
||||
ConstructMeshTargets(primitive, meshIndex, i);
|
||||
}
|
||||
}
|
||||
|
||||
if (primitive.Material != null)
|
||||
{
|
||||
await ConstructMaterialImageBuffers(primitive.Material.Value);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual async Task ConstructPrimitiveAttributes(MeshPrimitive primitive, int meshIndex, int primitiveIndex)
|
||||
{
|
||||
if (_assetCache.MeshCache[meshIndex].Primitives.Count-1 >= primitiveIndex)
|
||||
return;
|
||||
|
||||
var primData = new MeshCacheData.PrimitiveCacheData();
|
||||
_assetCache.MeshCache[meshIndex].Primitives.Add(primData);
|
||||
|
||||
foreach (var attributePair in primitive.Attributes)
|
||||
{
|
||||
if (attributePair.Value.Value.BufferView == null) // When Draco Compression is used, the bufferView is null
|
||||
continue;
|
||||
|
||||
if (!primData.Attributes.ContainsKey(attributePair.Key))
|
||||
{
|
||||
var bufferId = attributePair.Value.Value.BufferView.Value.Buffer;
|
||||
var bufferData = await GetBufferData(bufferId);
|
||||
|
||||
primData.Attributes[attributePair.Key] = new AttributeAccessor
|
||||
{
|
||||
AccessorId = attributePair.Value,
|
||||
bufferData = bufferData.bufferData,
|
||||
Offset = (uint)bufferData.ChunkOffset
|
||||
};
|
||||
}
|
||||
|
||||
var sparse = attributePair.Value.Value.Sparse;
|
||||
if (sparse != null)
|
||||
{
|
||||
if (!primData.Attributes.ContainsKey(attributePair.Key))
|
||||
{
|
||||
var sparseBufferId = sparse.Values.BufferView.Value.Buffer;
|
||||
var sparseBufferData = await GetBufferData(sparseBufferId);
|
||||
AttributeAccessor sparseValues = new AttributeAccessor
|
||||
{
|
||||
AccessorId = attributePair.Value,
|
||||
bufferData = sparseBufferData.bufferData,
|
||||
Offset = (uint)sparseBufferData.ChunkOffset
|
||||
};
|
||||
|
||||
var sparseIndicesBufferId = sparse.Indices.BufferView.Value.Buffer;
|
||||
var sparseIndicesBufferData = await GetBufferData(sparseIndicesBufferId);
|
||||
AttributeAccessor sparseIndices = new AttributeAccessor
|
||||
{
|
||||
AccessorId = attributePair.Value,
|
||||
bufferData = sparseIndicesBufferData.bufferData,
|
||||
Offset = (uint)sparseIndicesBufferData.ChunkOffset
|
||||
};
|
||||
|
||||
primData.SparseAccessors[attributePair.Key] = (sparseIndices, sparseValues);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (primitive.Indices != null && primitive.Indices.Value.BufferView != null)
|
||||
{
|
||||
if (!primData.Attributes.ContainsKey(SemanticProperties.INDICES))
|
||||
{
|
||||
var bufferId = primitive.Indices.Value.BufferView.Value.Buffer;
|
||||
var bufferData = await GetBufferData(bufferId);
|
||||
|
||||
primData.Attributes[SemanticProperties.INDICES] = new AttributeAccessor
|
||||
{
|
||||
AccessorId = primitive.Indices,
|
||||
bufferData = bufferData.bufferData,
|
||||
Offset = (uint)bufferData.ChunkOffset
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void ConvertAttributeAccessorsToUnityTypes(
|
||||
MeshCacheData.PrimitiveCacheData primData,
|
||||
UnityMeshData unityData,
|
||||
uint vertOffset,
|
||||
int indexOffset)
|
||||
{
|
||||
if (unityData.subMeshDataCreated[indexOffset])
|
||||
return;
|
||||
unityData.subMeshDataCreated[indexOffset] = true;
|
||||
|
||||
var meshAttributes = primData.Attributes;
|
||||
uint vertexCount = 0;
|
||||
if (meshAttributes.TryGetValue(SemanticProperties.POSITION, out var attribute))
|
||||
{
|
||||
vertexCount = attribute.AccessorId.Value.Count;
|
||||
}
|
||||
|
||||
int[] indices = null;
|
||||
|
||||
if (meshAttributes.TryGetValue(SemanticProperties.INDICES, out var indicesAccessor))
|
||||
{
|
||||
indices = indicesAccessor.AccessorContent.AsUInts.ToIntArrayRaw();
|
||||
switch (unityData.DrawModes[indexOffset])
|
||||
{
|
||||
case DrawMode.LineLoop:
|
||||
if (indices[indices.Length - 1] != indices[0])
|
||||
{
|
||||
Array.Resize(ref indices, indices.Length + 1);
|
||||
indices[indices.Length - 1] = indices[0];
|
||||
}
|
||||
break;
|
||||
case DrawMode.Triangles:
|
||||
SchemaExtensions.FlipTriangleFaces(indices);
|
||||
break;
|
||||
case DrawMode.TriangleStrip:
|
||||
indices = MeshPrimitive.ConvertTriangleStripsToTriangles(indices);
|
||||
SchemaExtensions.FlipTriangleFaces(indices);
|
||||
break;
|
||||
case DrawMode.TriangleFan:
|
||||
indices = MeshPrimitive.ConvertTriangleFanToTriangles(indices);
|
||||
SchemaExtensions.FlipTriangleFaces(indices);
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
switch (unityData.DrawModes[indexOffset])
|
||||
{
|
||||
case DrawMode.Points:
|
||||
indices = MeshPrimitive.GeneratePoints((int)vertexCount);
|
||||
break;
|
||||
case DrawMode.Lines:
|
||||
indices = MeshPrimitive.GenerateLines((int)vertexCount);
|
||||
break;
|
||||
case DrawMode.LineLoop:
|
||||
indices = MeshPrimitive.GenerateLineLoop((int)vertexCount);
|
||||
break;
|
||||
case DrawMode.LineStrip:
|
||||
indices = MeshPrimitive.GenerateLineStrip((int)vertexCount);
|
||||
break;
|
||||
case DrawMode.Triangles:
|
||||
indices = MeshPrimitive.GenerateTriangles((int)vertexCount);
|
||||
break;
|
||||
case DrawMode.TriangleStrip:
|
||||
indices = MeshPrimitive.GenerateTriangleStrips((int)vertexCount);
|
||||
break;
|
||||
case DrawMode.TriangleFan:
|
||||
indices = MeshPrimitive.GenerateTriangleFans((int)vertexCount);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (indices != null)
|
||||
unityData.Indices[indexOffset] = indices;
|
||||
|
||||
// Only add weight/joint data when it's not already added to the unity mesh data !
|
||||
if (meshAttributes.ContainsKey(SemanticProperties.Weight[0]) && meshAttributes.ContainsKey(SemanticProperties.Joint[0])
|
||||
&& !unityData.alreadyAddedAccessors.Contains(meshAttributes[SemanticProperties.Weight[0]].AccessorId.Id))
|
||||
{
|
||||
unityData.alreadyAddedAccessors.Add(meshAttributes[SemanticProperties.Weight[0]].AccessorId.Id);
|
||||
|
||||
CreateBoneWeightArray(
|
||||
meshAttributes[SemanticProperties.Joint[0]].AccessorContent.AsFloat4s.ToUnityVector4Raw(),
|
||||
meshAttributes[SemanticProperties.Weight[0]].AccessorContent.AsFloat4s.ToUnityVector4Raw(),
|
||||
ref unityData.BoneWeights,
|
||||
vertOffset);
|
||||
}
|
||||
|
||||
// Only add vertex data when it's not already added to the unity mesh data !
|
||||
if (meshAttributes.ContainsKey(SemanticProperties.POSITION) && !unityData.alreadyAddedAccessors.Contains(meshAttributes[SemanticProperties.POSITION].AccessorId.Id))
|
||||
{
|
||||
|
||||
if (meshAttributes.TryGetValue(SemanticProperties.POSITION, out var attrPos))
|
||||
{
|
||||
unityData.alreadyAddedAccessors.Add(attrPos.AccessorId.Id);
|
||||
attrPos.AccessorContent.AsFloat3s.ToUnityVector3Raw(unityData.Vertices, (int)vertOffset);
|
||||
}
|
||||
if (meshAttributes.TryGetValue(SemanticProperties.NORMAL, out var attrNorm))
|
||||
{
|
||||
attrNorm.AccessorContent.AsFloat3s.ToUnityVector3Raw(unityData.Normals, (int)vertOffset);
|
||||
}
|
||||
if (meshAttributes.TryGetValue(SemanticProperties.TANGENT, out var attrTang))
|
||||
{
|
||||
attrTang.AccessorContent.AsFloat4s.ToUnityVector4Raw(unityData.Tangents, (int)vertOffset);
|
||||
}
|
||||
if (meshAttributes.TryGetValue(SemanticProperties.TexCoord[0], out var attrTex0))
|
||||
{
|
||||
attrTex0.AccessorContent.AsFloat2s.ToUnityVector2Raw(unityData.Uv1, (int)vertOffset);
|
||||
}
|
||||
if (meshAttributes.TryGetValue(SemanticProperties.TexCoord[1], out var attrTex1))
|
||||
{
|
||||
attrTex1.AccessorContent.AsFloat2s.ToUnityVector2Raw(unityData.Uv2, (int)vertOffset);
|
||||
}
|
||||
if (meshAttributes.TryGetValue(SemanticProperties.TexCoord[2], out var attrTex2))
|
||||
{
|
||||
attrTex2.AccessorContent.AsFloat2s.ToUnityVector2Raw(unityData.Uv3, (int)vertOffset);
|
||||
}
|
||||
if (meshAttributes.TryGetValue(SemanticProperties.TexCoord[3], out var attrTex3))
|
||||
{
|
||||
attrTex3.AccessorContent.AsFloat2s.ToUnityVector2Raw(unityData.Uv4, (int)vertOffset);
|
||||
}
|
||||
if (meshAttributes.TryGetValue(SemanticProperties.Color[0], out var attrColor))
|
||||
{
|
||||
if (_activeColorSpace == ColorSpace.Gamma)
|
||||
attrColor.AccessorContent.AsFloat4s.ToUnityColorRaw(unityData.Colors, (int)vertOffset);
|
||||
else
|
||||
attrColor.AccessorContent.AsFloat4s.ToUnityColorLinear(unityData.Colors, (int)vertOffset);
|
||||
}
|
||||
|
||||
}
|
||||
var targets = primData.Targets;
|
||||
if (targets != null)
|
||||
{
|
||||
for (int i = 0; i < targets.Count; ++i)
|
||||
{
|
||||
if (targets[i].TryGetValue(SemanticProperties.POSITION, out var tarAttrPos) && !unityData.alreadyAddedAccessors.Contains(tarAttrPos.AccessorId.Id))
|
||||
{
|
||||
unityData.alreadyAddedAccessors.Add(tarAttrPos.AccessorId.Id);
|
||||
tarAttrPos.AccessorContent.AsFloat3s.ToUnityVector3Raw(unityData.MorphTargetVertices[i], (int)vertOffset);
|
||||
}
|
||||
if (targets[i].TryGetValue(SemanticProperties.NORMAL, out var tarAttrNorm) && !unityData.alreadyAddedAccessors.Contains(tarAttrNorm.AccessorId.Id))
|
||||
{
|
||||
unityData.alreadyAddedAccessors.Add(tarAttrNorm.AccessorId.Id);
|
||||
tarAttrNorm.AccessorContent.AsFloat3s.ToUnityVector3Raw(unityData.MorphTargetNormals[i], (int)vertOffset);
|
||||
}
|
||||
if (targets[i].TryGetValue(SemanticProperties.TANGENT, out var tarAttrTang) && !unityData.alreadyAddedAccessors.Contains(tarAttrTang.AccessorId.Id))
|
||||
{
|
||||
unityData.alreadyAddedAccessors.Add(tarAttrTang.AccessorId.Id);
|
||||
tarAttrTang.AccessorContent.AsFloat3s.ToUnityVector3Raw(unityData.MorphTargetTangents[i], (int)vertOffset);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Flip vectors to Unity coordinate system
|
||||
private void TransformTargets(ref Dictionary<string, AttributeAccessor> attributeAccessors)
|
||||
{
|
||||
if (attributeAccessors.ContainsKey(SemanticProperties.POSITION))
|
||||
{
|
||||
AttributeAccessor attributeAccessor = attributeAccessors[SemanticProperties.POSITION];
|
||||
SchemaExtensions.ConvertVector3CoordinateSpace(ref attributeAccessor, SchemaExtensions.CoordinateSpaceConversionScale);
|
||||
}
|
||||
|
||||
if (attributeAccessors.ContainsKey(SemanticProperties.NORMAL))
|
||||
{
|
||||
AttributeAccessor attributeAccessor = attributeAccessors[SemanticProperties.NORMAL];
|
||||
SchemaExtensions.ConvertVector3CoordinateSpace(ref attributeAccessor, SchemaExtensions.CoordinateSpaceConversionScale);
|
||||
}
|
||||
|
||||
if (attributeAccessors.ContainsKey(SemanticProperties.TANGENT))
|
||||
{
|
||||
AttributeAccessor attributeAccessor = attributeAccessors[SemanticProperties.TANGENT];
|
||||
SchemaExtensions.ConvertVector3CoordinateSpace(ref attributeAccessor, SchemaExtensions.CoordinateSpaceConversionScale);
|
||||
}
|
||||
}
|
||||
|
||||
private static void AddNewBufferAndViewToAccessor(byte[] data, Accessor accessor, GLTFRoot _gltfRoot)
|
||||
{
|
||||
if (_gltfRoot.Buffers == null)
|
||||
_gltfRoot.Buffers = new List<GLTFBuffer>();
|
||||
if (_gltfRoot.BufferViews == null)
|
||||
_gltfRoot.BufferViews = new List<BufferView>();
|
||||
_gltfRoot.Buffers.Add(new GLTFBuffer() { ByteLength = (uint) data.Length });
|
||||
_gltfRoot.BufferViews.Add(new BufferView() { ByteLength = (uint) data.Length, ByteOffset = 0, Buffer = new BufferId() { Id = _gltfRoot.Buffers.Count, Root = _gltfRoot } });
|
||||
accessor.BufferView = new BufferViewId() { Id = _gltfRoot.BufferViews.Count - 1, Root = _gltfRoot };
|
||||
}
|
||||
|
||||
protected MeshTopology GetTopology(DrawMode mode)
|
||||
{
|
||||
switch (mode)
|
||||
{
|
||||
case DrawMode.Points: return MeshTopology.Points;
|
||||
case DrawMode.Lines: return MeshTopology.Lines;
|
||||
case DrawMode.LineStrip: return MeshTopology.LineStrip;
|
||||
case DrawMode.Triangles: return MeshTopology.Triangles;
|
||||
case DrawMode.LineLoop: return MeshTopology.LineStrip;
|
||||
case DrawMode.TriangleStrip: return MeshTopology.Triangles;
|
||||
case DrawMode.TriangleFan: return MeshTopology.Triangles;
|
||||
}
|
||||
|
||||
throw new Exception("Unity does not support glTF draw mode: " + mode + $" (File: {_gltfFileName})");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Allocate a generic type 2D array. The size is depending on the given parameters.
|
||||
/// </summary>
|
||||
/// <param name="x">Defines the depth of the arrays first dimension</param>
|
||||
/// <param name="y">>Defines the depth of the arrays second dimension</param>
|
||||
/// <returns></returns>
|
||||
private static T[][] Allocate2dArray<T>(uint x, uint y)
|
||||
{
|
||||
var result = new T[x][];
|
||||
for (var i = 0; i < x; i++) result[i] = new T[y];
|
||||
return result;
|
||||
}
|
||||
|
||||
private void CheckForMeshDuplicates()
|
||||
{
|
||||
if (_gltfRoot.Meshes == null)
|
||||
return;
|
||||
|
||||
Dictionary<int, int> meshDuplicates = new Dictionary<int, int>();
|
||||
|
||||
for (int meshIndex = 0; meshIndex < _gltfRoot.Meshes.Count; meshIndex++)
|
||||
{
|
||||
if (meshDuplicates.ContainsKey(meshIndex))
|
||||
continue;
|
||||
|
||||
for (int i = meshIndex+1; i < _gltfRoot.Meshes.Count; i++)
|
||||
{
|
||||
|
||||
if (i == meshIndex)
|
||||
continue;
|
||||
if (_assetCache.MeshCache[i] == null)
|
||||
continue;
|
||||
|
||||
if (_assetCache.UnityMeshDataCache[i] == null
|
||||
|| _assetCache.UnityMeshDataCache[meshIndex] == null)
|
||||
continue;
|
||||
|
||||
var meshIsEqual = _assetCache.UnityMeshDataCache[i]
|
||||
.IsEqual(_assetCache.UnityMeshDataCache[meshIndex]);
|
||||
|
||||
if (meshIsEqual)
|
||||
meshDuplicates[i] = meshIndex;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var dm in meshDuplicates)
|
||||
{
|
||||
if (_gltfRoot.Nodes == null) continue;
|
||||
for (int i = 0; i < _gltfRoot.Nodes.Count; i++)
|
||||
{
|
||||
if (_gltfRoot.Nodes[i].Mesh != null && _gltfRoot.Nodes[i].Mesh.Id == dm.Key)
|
||||
{
|
||||
if (_gltfRoot.Nodes[i].Weights == null && _gltfRoot.Meshes[dm.Value].Weights != null)
|
||||
_gltfRoot.Nodes[i].Weights = _gltfRoot.Meshes[_gltfRoot.Nodes[i].Mesh.Id].Weights;
|
||||
|
||||
_gltfRoot.Nodes[i].Mesh.Id = dm.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7f4930a0205140bbbcfaa67b79ab587b
|
||||
timeCreated: 1657013019
|
||||
@@ -0,0 +1,98 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using GLTF;
|
||||
using GLTF.Schema;
|
||||
using Unity.Mathematics;
|
||||
using UnityEngine;
|
||||
using UnityGLTF.Extensions;
|
||||
using Matrix4x4 = GLTF.Math.Matrix4x4;
|
||||
using Vector4 = UnityEngine.Vector4;
|
||||
|
||||
namespace UnityGLTF
|
||||
{
|
||||
public partial class GLTFSceneImporter
|
||||
{
|
||||
|
||||
protected virtual async Task SetupBones(Skin skin, SkinnedMeshRenderer renderer, CancellationToken cancellationToken)
|
||||
{
|
||||
var boneCount = skin.Joints.Count;
|
||||
Transform[] bones = new Transform[boneCount];
|
||||
|
||||
// TODO: build bindpose arrays only once per skin, instead of once per node
|
||||
float4x4[] gltfBindPoses = null;
|
||||
if (skin.InverseBindMatrices != null)
|
||||
{
|
||||
var bufferId = skin.InverseBindMatrices.Value.BufferView.Value.Buffer;
|
||||
var bufferData = await GetBufferData(bufferId);
|
||||
AttributeAccessor attributeAccessor = new AttributeAccessor
|
||||
{
|
||||
AccessorId = skin.InverseBindMatrices,
|
||||
bufferData = bufferData.bufferData,
|
||||
Offset = bufferData.ChunkOffset
|
||||
};
|
||||
|
||||
GLTFHelpers.BuildBindPoseSamplers(ref attributeAccessor);
|
||||
gltfBindPoses = attributeAccessor.AccessorContent.AsMatrix4x4s;
|
||||
}
|
||||
|
||||
UnityEngine.Matrix4x4[] bindPoses = new UnityEngine.Matrix4x4[boneCount];
|
||||
for (int i = 0; i < boneCount; i++)
|
||||
{
|
||||
var node = await GetNode(skin.Joints[i].Id, cancellationToken);
|
||||
|
||||
bones[i] = node.transform;
|
||||
bindPoses[i] = gltfBindPoses != null ? gltfBindPoses[i].ToUnityMatrix4x4Convert() : UnityEngine.Matrix4x4.identity;
|
||||
}
|
||||
|
||||
if (skin.Skeleton != null)
|
||||
{
|
||||
var rootBoneNode = await GetNode(skin.Skeleton.Id, cancellationToken);
|
||||
renderer.rootBone = rootBoneNode.transform;
|
||||
}
|
||||
else
|
||||
{
|
||||
var rootBoneId = GLTFHelpers.FindCommonAncestor(skin.Joints);
|
||||
if (rootBoneId != null)
|
||||
{
|
||||
var rootBoneNode = await GetNode(rootBoneId.Id, cancellationToken);
|
||||
renderer.rootBone = rootBoneNode.transform;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception($"glTF skin joints do not share a root node! (File: {_gltfFileName})");
|
||||
}
|
||||
}
|
||||
renderer.sharedMesh.bindposes = bindPoses;
|
||||
renderer.bones = bones;
|
||||
}
|
||||
|
||||
private void CreateBoneWeightArray(Vector4[] joints, Vector4[] weights, ref BoneWeight[] destArr, uint offset = 0)
|
||||
{
|
||||
// normalize weights (built-in normalize function only normalizes three components)
|
||||
for (int i = 0; i < weights.Length; i++)
|
||||
{
|
||||
var weightSum = (weights[i].x + weights[i].y + weights[i].z + weights[i].w);
|
||||
|
||||
if (!Mathf.Approximately(weightSum, 0))
|
||||
{
|
||||
weights[i] /= weightSum;
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < joints.Length; i++)
|
||||
{
|
||||
destArr[offset + i].boneIndex0 = (int)joints[i].x;
|
||||
destArr[offset + i].boneIndex1 = (int)joints[i].y;
|
||||
destArr[offset + i].boneIndex2 = (int)joints[i].z;
|
||||
destArr[offset + i].boneIndex3 = (int)joints[i].w;
|
||||
|
||||
destArr[offset + i].weight0 = weights[i].x;
|
||||
destArr[offset + i].weight1 = weights[i].y;
|
||||
destArr[offset + i].weight2 = weights[i].z;
|
||||
destArr[offset + i].weight3 = weights[i].w;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cef61a6932d545e0aca7bb7cd4a819db
|
||||
timeCreated: 1657012769
|
||||
@@ -0,0 +1,726 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using GLTF;
|
||||
using GLTF.Schema;
|
||||
using GLTF.Utilities;
|
||||
using Unity.Collections;
|
||||
using UnityEngine;
|
||||
using UnityGLTF.Cache;
|
||||
using UnityGLTF.Extensions;
|
||||
using UnityGLTF.Loader;
|
||||
using UnityGLTF.Plugins;
|
||||
using Object = UnityEngine.Object;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
using UnityEditor;
|
||||
using UnityEditor.Build;
|
||||
#endif
|
||||
|
||||
namespace UnityGLTF
|
||||
{
|
||||
public partial class GLTFSceneImporter
|
||||
{
|
||||
internal const string EMPTY_TEXTURE_NAME_SUFFIX = " \0";
|
||||
private Dictionary<int, int> _imageDeduplicationLinks; // Org Image ID -> distinct Image ID
|
||||
|
||||
private class TextureData
|
||||
{
|
||||
public Texture Texture = null;
|
||||
public int TexCoord = 0;
|
||||
public double DataMultiplier = 1;
|
||||
public Vector2 Offset = Vector2.zero;
|
||||
public double Rotation = 0;
|
||||
public Vector2 Scale = Vector2.one;
|
||||
public Nullable<int> TexCoordExtra = 0;
|
||||
}
|
||||
|
||||
private async Task CreateNotReferencedTexture(int index)
|
||||
{
|
||||
if (Root.Textures == null) return;
|
||||
if (Root.Textures[index].Source != null
|
||||
&& Root.Images?.Count > 0
|
||||
&& Root.Images.Count > Root.Textures[index].Source.Id
|
||||
&& string.IsNullOrEmpty(Root.Textures[index].Source.Value.Uri))
|
||||
{
|
||||
await ConstructImageBuffer(Root.Textures[index], index);
|
||||
await ConstructTexture(Root.Textures[index], index, !KeepCPUCopyOfTexture, true, false);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<TextureData> FromTextureInfo(TextureInfo textureInfo, bool isNormal)
|
||||
{
|
||||
var result = new TextureData();
|
||||
if (textureInfo?.Index?.Value == null) return result;
|
||||
|
||||
TextureId textureId = textureInfo.Index;
|
||||
await ConstructTexture(textureId.Value, textureId.Id, !KeepCPUCopyOfTexture, true, isNormal);
|
||||
result.Texture = _assetCache.TextureCache[textureId.Id].Texture;
|
||||
result.TexCoord = textureInfo.TexCoord;
|
||||
|
||||
if (textureInfo is NormalTextureInfo nti)
|
||||
result.DataMultiplier = nti.Scale;
|
||||
if (textureInfo is OcclusionTextureInfo oti)
|
||||
result.DataMultiplier = oti.Strength;
|
||||
|
||||
var ext = GetTextureTransform(textureInfo);
|
||||
if (ext != null)
|
||||
{
|
||||
result.Offset = ext.Offset.ToUnityVector2Raw();
|
||||
result.Rotation = ext.Rotation;
|
||||
result.Scale = ext.Scale.ToUnityVector2Raw();
|
||||
result.TexCoordExtra = ext.TexCoord;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static int ComputeHash(byte[] data)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
const int p = 16777619;
|
||||
int hash = (int)2166136261;
|
||||
|
||||
for (int i = 0; i < data.Length; i++)
|
||||
hash = (hash ^ data[i]) * p;
|
||||
|
||||
return hash;
|
||||
}
|
||||
}
|
||||
|
||||
private Dictionary<int, int> CollectImageHashes()
|
||||
{
|
||||
if (_gltfRoot.Images == null) return new Dictionary<int, int>();
|
||||
Dictionary<int, int> hashes = new Dictionary<int, int>();
|
||||
|
||||
int index = -1;
|
||||
foreach (var image in _gltfRoot.Images)
|
||||
{
|
||||
index++;
|
||||
if (image.Uri == null)
|
||||
{
|
||||
if (image.BufferView == null)
|
||||
continue;
|
||||
|
||||
var stream = GetImageStream(image, -1);
|
||||
if (stream == null || stream.Length == 0)
|
||||
continue;
|
||||
|
||||
// Create a hash for the stream data
|
||||
var bufferData = new byte[stream.Length];
|
||||
stream.Read(bufferData, 0, bufferData.Length);
|
||||
|
||||
int streamHash = ComputeHash(bufferData);
|
||||
|
||||
hashes.Add(index, streamHash);
|
||||
}
|
||||
else
|
||||
{
|
||||
hashes.Add(index, image.Uri.GetHashCode());
|
||||
}
|
||||
}
|
||||
|
||||
return hashes;
|
||||
}
|
||||
|
||||
private void CheckForDuplicateImages()
|
||||
{
|
||||
var hashes = CollectImageHashes();
|
||||
var reverseHashes = new Dictionary<int, int>();
|
||||
foreach (var h in hashes)
|
||||
reverseHashes[h.Value] = h.Key;
|
||||
|
||||
_imageDeduplicationLinks = new Dictionary<int, int>();
|
||||
foreach (var h in hashes)
|
||||
_imageDeduplicationLinks[h.Key] = reverseHashes[h.Value];
|
||||
}
|
||||
|
||||
private Stream GetImageStream(GLTFImage image, int imageCacheIndex)
|
||||
{
|
||||
Stream stream = null;
|
||||
if (image.Uri == null)
|
||||
{
|
||||
if (image.BufferView == null)
|
||||
return null;
|
||||
|
||||
var bufferView = image.BufferView.Value;
|
||||
BufferCacheData bufferContents = _assetCache.BufferCache[bufferView.Buffer.Id];
|
||||
if (bufferContents.bufferData.IsCreated)
|
||||
{
|
||||
bufferContents.Stream.Position = bufferView.ByteOffset + bufferContents.ChunkOffset;
|
||||
stream = new SubStream(bufferContents.Stream, 0, bufferView.ByteLength);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
string uri = image.Uri;
|
||||
|
||||
byte[] bufferData;
|
||||
URIHelper.TryParseBase64(uri, out bufferData);
|
||||
if (bufferData != null)
|
||||
{
|
||||
stream = new MemoryStream(bufferData, 0, bufferData.Length, false, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
stream = _assetCache.ImageStreamCache[imageCacheIndex];
|
||||
}
|
||||
}
|
||||
|
||||
return stream;
|
||||
}
|
||||
|
||||
protected async Task ConstructImage(GLTFImage image, int imageCacheIndex, bool markGpuOnly, bool isLinear, bool isNormal)
|
||||
{
|
||||
if (_assetCache.InvalidImageCache[imageCacheIndex])
|
||||
return;
|
||||
|
||||
if (_assetCache.ImageCache[imageCacheIndex] == null)
|
||||
{
|
||||
Stream stream = GetImageStream(image, imageCacheIndex);
|
||||
await YieldOnTimeoutAndThrowOnLowMemory();
|
||||
await ConstructUnityTexture(stream, markGpuOnly, isLinear, isNormal, image, imageCacheIndex);
|
||||
}
|
||||
}
|
||||
|
||||
protected async Task ConstructImageBuffer(GLTFTexture texture, int textureIndex)
|
||||
{
|
||||
int sourceId = GetTextureSourceId(texture);
|
||||
if (_assetCache.ImageStreamCache[sourceId] == null)
|
||||
{
|
||||
GLTFImage image = _gltfRoot.Images[sourceId];
|
||||
|
||||
// we only load the streams if not a base64 uri, meaning the data is in the uri
|
||||
if (image.Uri != null && !URIHelper.IsBase64Uri(image.Uri))
|
||||
{
|
||||
_assetCache.ImageStreamCache[sourceId] = await _options.DataLoader.LoadStreamAsync(image.Uri);
|
||||
}
|
||||
else if (image.Uri == null && image.BufferView != null && _assetCache.BufferCache[image.BufferView.Value.Buffer.Id] == null)
|
||||
{
|
||||
int bufferIndex = image.BufferView.Value.Buffer.Id;
|
||||
await ConstructBuffer(_gltfRoot.Buffers[bufferIndex], bufferIndex);
|
||||
}
|
||||
}
|
||||
|
||||
if (_assetCache.TextureCache[textureIndex] == null)
|
||||
{
|
||||
_assetCache.TextureCache[textureIndex] = new TextureCacheData
|
||||
{
|
||||
TextureDefinition = texture
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// With using KTX, we need to return a new Texture2D instance at the moment. Unity KTX package does not support loading into existing one
|
||||
async Task<Texture2D> CheckMimeTypeAndLoadImage(GLTFImage image, Texture2D texture, NativeArray<byte> data, bool markGpuOnly, bool isLinear)
|
||||
{
|
||||
bool textureWillBeCompressed = false;
|
||||
#if UNITY_EDITOR
|
||||
if (Context.SourceImporter != null)
|
||||
#endif
|
||||
{
|
||||
if (_options.RuntimeTextureCompression != RuntimeTextureCompression.None)
|
||||
textureWillBeCompressed = true;
|
||||
}
|
||||
|
||||
switch (image.MimeType)
|
||||
{
|
||||
case "image/png":
|
||||
case "image/jpeg":
|
||||
// NOTE: the second parameter of LoadImage() marks non-readable, but we can't mark it until after we call Apply()
|
||||
texture.LoadImage(data.ToArray(), markGpuOnly && !textureWillBeCompressed);
|
||||
break;
|
||||
case "image/exr":
|
||||
Debug.Log(LogType.Warning, $"EXR images are not supported. The texture {texture.name} won't be imported. File: {_gltfFileName}");
|
||||
break;
|
||||
case "image/ktx2":
|
||||
string textureName = texture.name;
|
||||
|
||||
#if HAVE_KTX
|
||||
if (Context.TryGetPlugin<Ktx2ImportContext>(out _))
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
Texture.DestroyImmediate(texture);
|
||||
#else
|
||||
Texture.Destroy(texture);
|
||||
#endif
|
||||
var ktxTexture = new KtxUnity.KtxTexture();
|
||||
|
||||
var resultTextureData = await ktxTexture.LoadFromBytes(data, isLinear, mipChain: GenerateMipMapsForTextures);
|
||||
texture = resultTextureData.texture;
|
||||
texture.name = textureName;
|
||||
}
|
||||
else
|
||||
#endif
|
||||
{
|
||||
Debug.Log(LogType.Warning, $"Can't import texture \"{image.Name}\" from \"{_gltfFileName}\" because it is a KTX2 file using the KHR_texture_basisu extension. Add the package \"com.unity.cloud.ktx\" version v1.3+ to your project to import KTX2 textures.");
|
||||
await Task.CompletedTask;
|
||||
texture = null;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
texture.LoadImage(data.ToArray(), markGpuOnly && !textureWillBeCompressed);
|
||||
break;
|
||||
}
|
||||
|
||||
// assign default values
|
||||
if (texture)
|
||||
{
|
||||
texture.wrapMode = TextureWrapMode.Repeat;
|
||||
texture.wrapModeV = TextureWrapMode.Repeat;
|
||||
texture.wrapModeU = TextureWrapMode.Repeat;
|
||||
texture.filterMode = FilterMode.Bilinear;
|
||||
}
|
||||
#if UNITY_EDITOR
|
||||
if (Context.SourceImporter != null)
|
||||
#endif
|
||||
{
|
||||
// Only when this import is not an Asset Import
|
||||
|
||||
if (_options.RuntimeTextureCompression != RuntimeTextureCompression.None)
|
||||
{
|
||||
// Texture need to be readable to compress it
|
||||
texture.Compress(_options.RuntimeTextureCompression == RuntimeTextureCompression.HighQuality);
|
||||
if (markGpuOnly)
|
||||
texture.Apply(true, true);
|
||||
}
|
||||
}
|
||||
|
||||
await Task.CompletedTask;
|
||||
return texture;
|
||||
}
|
||||
|
||||
protected virtual async Task ConstructUnityTexture(Stream stream, bool markGpuOnly, bool isLinear, bool isNormal, GLTFImage image, int imageCacheIndex)
|
||||
{
|
||||
bool convertToDxt5nmFormat = false;
|
||||
#if UNITY_EDITOR
|
||||
if (stream is AssetDatabaseStream assetDatabaseStream)
|
||||
{
|
||||
var tx = UnityEditor.AssetDatabase.LoadAssetAtPath<Texture2D>(assetDatabaseStream.AssetUri);
|
||||
progressStatus.TextureLoaded++;
|
||||
progress?.Report(progressStatus);
|
||||
_assetCache.ImageCache[imageCacheIndex] = tx;
|
||||
return;
|
||||
}
|
||||
|
||||
if (isNormal && Context.SourceImporter != null)
|
||||
{
|
||||
BuildTargetGroup activeTargetGroup = BuildPipeline.GetBuildTargetGroup(EditorUserBuildSettings.activeBuildTarget);
|
||||
#if UNITY_2023_1_OR_NEWER
|
||||
if (PlayerSettings.GetNormalMapEncoding(NamedBuildTarget.FromBuildTargetGroup(activeTargetGroup)) == NormalMapEncoding.DXT5nm)
|
||||
#else
|
||||
if (PlayerSettings.GetNormalMapEncoding(activeTargetGroup) == NormalMapEncoding.DXT5nm)
|
||||
#endif
|
||||
{
|
||||
convertToDxt5nmFormat = true;
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
Texture2D texture = new Texture2D(4, 4, TextureFormat.RGBA32, GenerateMipMapsForTextures, isLinear);
|
||||
texture.name = string.IsNullOrEmpty(image.Name) ? Path.GetFileNameWithoutExtension(image.Uri) : image.Name;
|
||||
if (string.IsNullOrEmpty(texture.name))
|
||||
texture.name = $"Texture_{imageCacheIndex.ToString()}{EMPTY_TEXTURE_NAME_SUFFIX}";
|
||||
|
||||
var newTextureObject = texture;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
var haveRemappedTexture = false;
|
||||
if (Context.SourceImporter != null)
|
||||
{
|
||||
// check for remapping, we don't even need to attempt loading the texture in that case.
|
||||
var externalObjects = Context.SourceImporter.GetExternalObjectMap();
|
||||
var sourceIdentifier = new AssetImporter.SourceAssetIdentifier(texture);
|
||||
externalObjects.TryGetValue(sourceIdentifier, out var o);
|
||||
if (o is Texture2D remappedTexture)
|
||||
{
|
||||
if (remappedTexture)
|
||||
{
|
||||
texture = remappedTexture;
|
||||
haveRemappedTexture = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
Context.SourceImporter.RemoveRemap(sourceIdentifier);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (haveRemappedTexture)
|
||||
{
|
||||
// nothing to do here, the texture has already been remapped
|
||||
}
|
||||
else
|
||||
#endif
|
||||
if (stream is FileLoader.InvalidStream invalidStream)
|
||||
{
|
||||
// ignore - we still need a valid texture so that we can properly remap
|
||||
// texture = null;
|
||||
// We need some way to track these mock textures, so that we can get rid of them again after import
|
||||
// TODO we should still set it to null here, and save the importer definition names for remapping instead.
|
||||
// This way here we'll get into weird code for Runtime import, as we would still import mock textures...
|
||||
// Or we add another option to avoid that.
|
||||
texture = null;
|
||||
Debug.Log(LogType.Error, "Buffer file " + invalidStream.RelativeFilePath + " not found in path: " + invalidStream.AbsoluteFilePath+ $" (File: {_gltfFileName})");
|
||||
}
|
||||
else if (stream != null && _nativeBuffers.TryGetValue(stream, out var nativeData))
|
||||
{
|
||||
var bufferView = await GetBufferData(image.BufferView.Value.Buffer);
|
||||
await YieldOnTimeoutAndThrowOnLowMemory();
|
||||
texture = await CheckMimeTypeAndLoadImage(image, texture, bufferView.bufferData, markGpuOnly, isLinear);
|
||||
|
||||
}
|
||||
else if (stream is MemoryStream)
|
||||
{
|
||||
using (MemoryStream memoryStream = stream as MemoryStream)
|
||||
{
|
||||
await YieldOnTimeoutAndThrowOnLowMemory();
|
||||
using (var memoryStreamData = new NativeArray<byte>(memoryStream.ToArray(), Allocator.TempJob))
|
||||
{
|
||||
texture = await CheckMimeTypeAndLoadImage(image, texture, memoryStreamData, markGpuOnly, isLinear);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (stream != null)
|
||||
{
|
||||
byte[] buffer = new byte[stream.Length];
|
||||
|
||||
// todo: potential optimization is to split stream read into multiple frames (or put it on a thread?)
|
||||
if (stream.Length > int.MaxValue)
|
||||
{
|
||||
throw new Exception($"Stream is larger than can be copied into byte array (File: {_gltfFileName})");
|
||||
}
|
||||
|
||||
stream.Read(buffer, 0, (int)stream.Length);
|
||||
await YieldOnTimeoutAndThrowOnLowMemory();
|
||||
using (NativeArray<byte> bufferNative = new NativeArray<byte>(buffer, Allocator.TempJob))
|
||||
{
|
||||
texture = await CheckMimeTypeAndLoadImage(image, texture, bufferNative, markGpuOnly, isLinear);
|
||||
}
|
||||
}
|
||||
|
||||
if (texture && convertToDxt5nmFormat)
|
||||
{
|
||||
texture = await NormalMapEncodingConverter.ConvertToDxt5nmAndCheckTextureFormatAsync(texture);
|
||||
if (texture != newTextureObject)
|
||||
newTextureObject = texture;
|
||||
|
||||
texture.Apply();
|
||||
}
|
||||
|
||||
if (!texture)
|
||||
_assetCache.InvalidImageCache[imageCacheIndex] = newTextureObject;
|
||||
|
||||
if (_assetCache.ImageCache[imageCacheIndex] != null) Debug.Log(LogType.Assert, "ImageCache should not be loaded multiple times");
|
||||
if (texture)
|
||||
{
|
||||
progressStatus.TextureLoaded++;
|
||||
progress?.Report(progressStatus);
|
||||
}
|
||||
_assetCache.ImageCache[imageCacheIndex] = texture;
|
||||
}
|
||||
|
||||
|
||||
protected virtual int GetTextureSourceId(GLTFTexture texture)
|
||||
{
|
||||
if (texture.Extensions != null && texture.Extensions.ContainsKey(KHR_texture_basisu.EXTENSION_NAME))
|
||||
{
|
||||
return ((KHR_texture_basisu)texture.Extensions[KHR_texture_basisu.EXTENSION_NAME]).source.Id;
|
||||
}
|
||||
|
||||
int id = texture.Source?.Id ?? 0;
|
||||
if (_imageDeduplicationLinks != null)
|
||||
{
|
||||
if (_imageDeduplicationLinks.TryGetValue(id, out int replacedId))
|
||||
id = replacedId;
|
||||
}
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
protected virtual bool IsTextureFlipped(GLTFTexture texture)
|
||||
{
|
||||
if (texture.Extensions != null && texture.Extensions.ContainsKey(KHR_texture_basisu.EXTENSION_NAME))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a texture from a glTF texture
|
||||
/// </summary>
|
||||
/// <param name="texture">The texture to load</param>
|
||||
/// <param name="textureIndex">The index in the texture cache</param>
|
||||
/// <param name="markGpuOnly">Whether the texture is GPU only, instead of keeping a CPU copy</param>
|
||||
/// <param name="isLinear">Whether the texture is linear rather than sRGB</param>
|
||||
/// <returns>The loading task</returns>
|
||||
public virtual async Task LoadTextureAsync(GLTFTexture texture, int textureIndex, bool markGpuOnly, bool isLinear)
|
||||
{
|
||||
try
|
||||
{
|
||||
lock (this)
|
||||
{
|
||||
if (_isRunning)
|
||||
{
|
||||
throw new GLTFLoadException($"Cannot CreateTexture while GLTFSceneImporter is already running (File: {_gltfFileName})");
|
||||
}
|
||||
|
||||
_isRunning = true;
|
||||
}
|
||||
|
||||
if (_options.ThrowOnLowMemory)
|
||||
{
|
||||
_memoryChecker = new MemoryChecker();
|
||||
}
|
||||
|
||||
if (_gltfRoot == null)
|
||||
{
|
||||
await LoadJson(_gltfFileName);
|
||||
}
|
||||
|
||||
if (_assetCache == null)
|
||||
{
|
||||
_assetCache = new AssetCache(_gltfRoot);
|
||||
}
|
||||
|
||||
await ConstructImageBuffer(texture, textureIndex);
|
||||
await ConstructTexture(texture, textureIndex, markGpuOnly, isLinear, false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
lock (this)
|
||||
{
|
||||
_isRunning = false;
|
||||
}
|
||||
_gltfStream.Stream.Close();
|
||||
}
|
||||
}
|
||||
|
||||
public virtual Task LoadTextureAsync(GLTFTexture texture, int textureIndex, bool isLinear)
|
||||
{
|
||||
return LoadTextureAsync(texture, textureIndex, !KeepCPUCopyOfTexture, isLinear);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets texture that has been loaded from CreateTexture
|
||||
/// </summary>
|
||||
/// <param name="textureIndex">The texture to get</param>
|
||||
/// <returns>Created texture</returns>
|
||||
public virtual Texture GetTexture(int textureIndex)
|
||||
{
|
||||
if (_assetCache == null)
|
||||
{
|
||||
throw new GLTFLoadException($"Asset cache needs initialized before calling GetTexture (File: {_gltfFileName})");
|
||||
}
|
||||
|
||||
if (_assetCache.TextureCache[textureIndex] == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return _assetCache.TextureCache[textureIndex].Texture;
|
||||
}
|
||||
|
||||
protected virtual async Task ConstructTexture(GLTFTexture texture, int textureIndex, bool markGpuOnly, bool isLinear, bool isNormal)
|
||||
{
|
||||
if (_assetCache.TextureCache[textureIndex].Texture == null)
|
||||
{
|
||||
int sourceId = GetTextureSourceId(texture);
|
||||
GLTFImage image = _gltfRoot.Images[sourceId];
|
||||
|
||||
bool isFirstInstance = _assetCache.ImageCache[sourceId] == null;
|
||||
|
||||
await ConstructImage(image, sourceId, markGpuOnly, isLinear, isNormal);
|
||||
|
||||
var source = _assetCache.ImageCache[sourceId];
|
||||
if (!source) return;
|
||||
|
||||
FilterMode desiredFilterMode;
|
||||
TextureWrapMode desiredWrapModeS, desiredWrapModeT;
|
||||
|
||||
if (texture.Sampler != null)
|
||||
{
|
||||
var sampler = texture.Sampler.Value;
|
||||
switch (sampler.MinFilter)
|
||||
{
|
||||
case MinFilterMode.Nearest:
|
||||
case MinFilterMode.NearestMipmapNearest:
|
||||
case MinFilterMode.NearestMipmapLinear:
|
||||
desiredFilterMode = FilterMode.Point;
|
||||
break;
|
||||
case MinFilterMode.Linear:
|
||||
case MinFilterMode.LinearMipmapNearest:
|
||||
desiredFilterMode = FilterMode.Bilinear;
|
||||
break;
|
||||
case MinFilterMode.LinearMipmapLinear:
|
||||
desiredFilterMode = FilterMode.Trilinear;
|
||||
break;
|
||||
default:
|
||||
Debug.Log(LogType.Warning, "Unsupported Sampler.MinFilter: " + sampler.MinFilter+ $" (File: {_gltfFileName})");
|
||||
desiredFilterMode = FilterMode.Bilinear;
|
||||
break;
|
||||
}
|
||||
|
||||
TextureWrapMode UnityWrapMode(GLTF.Schema.WrapMode gltfWrapMode)
|
||||
{
|
||||
switch (gltfWrapMode)
|
||||
{
|
||||
case GLTF.Schema.WrapMode.ClampToEdge:
|
||||
return TextureWrapMode.Clamp;
|
||||
case GLTF.Schema.WrapMode.Repeat:
|
||||
return TextureWrapMode.Repeat;
|
||||
case GLTF.Schema.WrapMode.MirroredRepeat:
|
||||
return TextureWrapMode.Mirror;
|
||||
default:
|
||||
Debug.Log(LogType.Warning, "Unsupported Sampler.Wrap: " + gltfWrapMode+ $" (File: {_gltfFileName})");
|
||||
return TextureWrapMode.Repeat;
|
||||
}
|
||||
}
|
||||
|
||||
desiredWrapModeS = UnityWrapMode(sampler.WrapS);
|
||||
desiredWrapModeT = UnityWrapMode(sampler.WrapT);
|
||||
}
|
||||
else
|
||||
{
|
||||
desiredFilterMode = FilterMode.Bilinear;
|
||||
desiredWrapModeS = TextureWrapMode.Repeat;
|
||||
desiredWrapModeT = TextureWrapMode.Repeat;
|
||||
}
|
||||
|
||||
if (isFirstInstance)
|
||||
{
|
||||
source.filterMode = desiredFilterMode;
|
||||
source.wrapModeU = desiredWrapModeS;
|
||||
source.wrapModeV = desiredWrapModeT;
|
||||
}
|
||||
|
||||
var matchSamplerState = source.filterMode == desiredFilterMode && source.wrapModeU == desiredWrapModeS && source.wrapModeV == desiredWrapModeT;
|
||||
if (matchSamplerState || markGpuOnly)
|
||||
{
|
||||
if (_assetCache.TextureCache[textureIndex].Texture != null) Debug.Log(LogType.Assert, "Texture should not be reset to prevent memory leaks"+ $" (File: {_gltfFileName})");
|
||||
_assetCache.TextureCache[textureIndex].Texture = source;
|
||||
|
||||
if (!matchSamplerState)
|
||||
{
|
||||
Debug.Log(LogType.Warning, $"Ignoring sampler; filter mode: source {source.filterMode}, desired {desiredFilterMode}; wrap mode: source {source.wrapModeU}x{source.wrapModeV}, desired {desiredWrapModeS}x{desiredWrapModeT}"+ $" (File: {_gltfFileName})");
|
||||
}
|
||||
}
|
||||
else
|
||||
#if UNITY_EDITOR
|
||||
if (!UnityEditor.AssetDatabase.Contains(source))
|
||||
#endif
|
||||
{
|
||||
Texture2D unityTexture;
|
||||
if (!source.isReadable)
|
||||
{
|
||||
unityTexture = new Texture2D(source.width, source.height, source.format, source.mipmapCount, isLinear);
|
||||
Graphics.CopyTexture(source, unityTexture);
|
||||
}
|
||||
else
|
||||
unityTexture = Object.Instantiate(source);
|
||||
|
||||
unityTexture.name = string.IsNullOrEmpty(image.Name) ?
|
||||
string.IsNullOrEmpty(texture.Name) ?
|
||||
Path.GetFileNameWithoutExtension(image.Uri) :
|
||||
texture.Name :
|
||||
image.Name;
|
||||
|
||||
if (string.IsNullOrEmpty(unityTexture.name))
|
||||
unityTexture.name = $"Texture_{textureIndex.ToString()}{EMPTY_TEXTURE_NAME_SUFFIX}";
|
||||
|
||||
unityTexture.filterMode = desiredFilterMode;
|
||||
unityTexture.wrapModeU = desiredWrapModeS;
|
||||
unityTexture.wrapModeV = desiredWrapModeT;
|
||||
|
||||
if (_assetCache.TextureCache[textureIndex].Texture != null) Debug.Log(LogType.Assert, $"Texture should not be reset to prevent memory leaks (File: {_gltfFileName})");
|
||||
_assetCache.TextureCache[textureIndex].Texture = unityTexture;
|
||||
}
|
||||
#if UNITY_EDITOR
|
||||
else
|
||||
{
|
||||
// don't warn for just filter mode, user choice
|
||||
if (source.wrapModeU != desiredWrapModeS || source.wrapModeV != desiredWrapModeT)
|
||||
Debug.Log(LogType.Warning, ($"Sampler state doesn't match but source texture is non-readable. Results might not be correct if textures are used multiple times with different sampler states. {source.filterMode} == {desiredFilterMode} && {source.wrapModeU} == {desiredWrapModeS} && {source.wrapModeV} == {desiredWrapModeT} (File: {_gltfFileName})"));
|
||||
_assetCache.TextureCache[textureIndex].Texture = source;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
_assetCache.TextureCache[textureIndex].IsLinear = isLinear;
|
||||
_assetCache.TextureCache[textureIndex].IsNormal = isNormal;
|
||||
|
||||
try
|
||||
{
|
||||
var tex = _assetCache.TextureCache[textureIndex].Texture;
|
||||
|
||||
foreach (var plugin in Context.Plugins)
|
||||
{
|
||||
plugin.OnAfterImportTexture(texture, textureIndex, tex);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void ConstructImageFromGLB(GLTFImage image, int imageCacheIndex)
|
||||
{
|
||||
var texture = new Texture2D(0, 0);
|
||||
texture.name = nameof(GLTFSceneImporter) + (image.Name != null ? ("." + image.Name) : "");
|
||||
var bufferView = image.BufferView.Value;
|
||||
var data = new byte[bufferView.ByteLength];
|
||||
|
||||
var bufferContents = _assetCache.BufferCache[bufferView.Buffer.Id];
|
||||
bufferContents.Stream.Position = bufferView.ByteOffset + bufferContents.ChunkOffset;
|
||||
bufferContents.Stream.Read(data, 0, data.Length);
|
||||
texture.LoadImage(data);
|
||||
|
||||
if (_assetCache.ImageCache[imageCacheIndex] != null) Debug.Log(LogType.Assert, "ImageCache should not be loaded multiple times");
|
||||
progressStatus.TextureLoaded++;
|
||||
progress?.Report(progressStatus);
|
||||
_assetCache.ImageCache[imageCacheIndex] = texture;
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
internal class AssetDatabaseStream : Stream
|
||||
{
|
||||
public string AssetUri { get; }
|
||||
|
||||
public AssetDatabaseStream(string imageUri)
|
||||
{
|
||||
AssetUri = imageUri;
|
||||
}
|
||||
|
||||
public override void Flush() => throw new NotImplementedException();
|
||||
public override int Read(byte[] buffer, int offset, int count) => throw new NotImplementedException();
|
||||
public override long Seek(long offset, SeekOrigin origin) => throw new NotImplementedException();
|
||||
public override void SetLength(long value) => throw new NotImplementedException();
|
||||
public override void Write(byte[] buffer, int offset, int count) => throw new NotImplementedException();
|
||||
|
||||
public override bool CanRead { get; }
|
||||
public override bool CanSeek { get; }
|
||||
public override bool CanWrite { get; }
|
||||
public override long Length { get; }
|
||||
public override long Position { get; set; }
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
internal static class MatHelper
|
||||
{
|
||||
internal static void SetKeyword(this Material material, string keyword, bool state)
|
||||
{
|
||||
if (state)
|
||||
material.EnableKeyword(keyword + "_ON");
|
||||
else
|
||||
material.DisableKeyword(keyword + "_ON");
|
||||
|
||||
if (material.HasProperty(keyword))
|
||||
material.SetFloat(keyword, state ? 1 : 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 859436f420e4466daa129c3fc03f4480
|
||||
timeCreated: 1657012139
|
||||
@@ -0,0 +1,6 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace UnityGLTF
|
||||
{
|
||||
public class MaterialLibrary: ScriptableObject { }
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: dae4451576d948249604afd3847354bb
|
||||
timeCreated: 1727343650
|
||||
@@ -0,0 +1,107 @@
|
||||
using System.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
|
||||
namespace UnityGLTF
|
||||
{
|
||||
internal static class NormalMapEncodingConverter
|
||||
{
|
||||
public static async Task<Texture2D> ConvertToDxt5nmAndCheckTextureFormatAsync(Texture2D source)
|
||||
{
|
||||
Texture2D dest = source;
|
||||
Color[] pixels;
|
||||
|
||||
void CreateDestinationTexture()
|
||||
{
|
||||
dest = new Texture2D(source.width, source.height, TextureFormat.RGBA32, source.mipmapCount > 0, true);
|
||||
dest.wrapMode = source.wrapMode;
|
||||
dest.wrapModeU = source.wrapModeU;
|
||||
dest.wrapModeV = source.wrapModeV;
|
||||
dest.wrapModeW = source.wrapModeW;
|
||||
dest.filterMode = source.filterMode;
|
||||
dest.anisoLevel = source.anisoLevel;
|
||||
dest.mipMapBias = source.mipMapBias;
|
||||
dest.name = source.name;
|
||||
}
|
||||
|
||||
void DestroySourceTexture()
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
Texture.DestroyImmediate(source);
|
||||
#else
|
||||
Texture.Destroy(source);
|
||||
#endif
|
||||
}
|
||||
|
||||
if (!source.isReadable)
|
||||
{
|
||||
CreateDestinationTexture();
|
||||
|
||||
var destRenderTexture = RenderTexture.GetTemporary(source.width, source.height, 0, RenderTextureFormat.Default, RenderTextureReadWrite.Linear);
|
||||
var previousSRGBState = GL.sRGBWrite;
|
||||
GL.sRGBWrite = false;
|
||||
|
||||
Graphics.Blit(source, destRenderTexture);
|
||||
|
||||
dest.ReadPixels(new Rect(0, 0, destRenderTexture.width, destRenderTexture.height), 0, 0);
|
||||
|
||||
GL.sRGBWrite = previousSRGBState;
|
||||
|
||||
RenderTexture.ReleaseTemporary(destRenderTexture);
|
||||
|
||||
pixels = dest.GetPixels();
|
||||
|
||||
DestroySourceTexture();
|
||||
}
|
||||
else
|
||||
{
|
||||
pixels = source.GetPixels();
|
||||
|
||||
if (source.format != TextureFormat.RGBA32)
|
||||
{
|
||||
CreateDestinationTexture();
|
||||
DestroySourceTexture();
|
||||
}
|
||||
}
|
||||
|
||||
await Task.Run(() =>
|
||||
{
|
||||
for (var i = 0; i < pixels.Length; i++)
|
||||
{
|
||||
var c = pixels[i];
|
||||
pixels[i] = new Color(1, c.g, 1, c.r);
|
||||
}
|
||||
});
|
||||
|
||||
dest.SetPixels(pixels);
|
||||
return dest;
|
||||
}
|
||||
|
||||
public static void ConvertToDxt5nm(Texture2D source, Texture2D dest)
|
||||
{
|
||||
var pixels = source.GetPixels();
|
||||
for (var i = 0; i < pixels.Length; i++)
|
||||
{
|
||||
var c = pixels[i];
|
||||
pixels[i] = new Color(1, c.g, 1, c.r);
|
||||
}
|
||||
|
||||
dest.SetPixels(pixels);
|
||||
}
|
||||
|
||||
public static async Task ConvertToDxt5nmAsync(Texture2D texture)
|
||||
{
|
||||
var pixels = texture.GetPixels();
|
||||
await Task.Run(() =>
|
||||
{
|
||||
for (var i = 0; i < pixels.Length; i++)
|
||||
{
|
||||
var c = pixels[i];
|
||||
pixels[i] = new Color(1, c.g, 1, c.r);
|
||||
}
|
||||
});
|
||||
|
||||
texture.SetPixels(pixels);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 870ecf89bfc64980a38e2977a56943bb
|
||||
timeCreated: 1705055769
|
||||
@@ -0,0 +1,119 @@
|
||||
using System.Collections.Generic;
|
||||
using GLTF.Schema;
|
||||
using UnityEngine;
|
||||
|
||||
namespace UnityGLTF
|
||||
{
|
||||
public class UnityMeshData
|
||||
{
|
||||
public bool[] subMeshDataCreated;
|
||||
public Vector3[] Vertices;
|
||||
public Vector3[] Normals;
|
||||
public Vector4[] Tangents;
|
||||
public Vector2[] Uv1;
|
||||
public Vector2[] Uv2;
|
||||
public Vector2[] Uv3;
|
||||
public Vector2[] Uv4;
|
||||
public Color[] Colors;
|
||||
public BoneWeight[] BoneWeights;
|
||||
|
||||
public Vector3[][] MorphTargetVertices;
|
||||
public Vector3[][] MorphTargetNormals;
|
||||
public Vector3[][] MorphTargetTangents;
|
||||
|
||||
public MeshTopology[] Topology;
|
||||
public DrawMode[] DrawModes;
|
||||
public int[][] Indices;
|
||||
|
||||
public HashSet<int> alreadyAddedAccessors = new HashSet<int>();
|
||||
public uint[] subMeshVertexOffset;
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
Vertices = null;
|
||||
Normals = null;
|
||||
Tangents = null;
|
||||
Uv1 = null;
|
||||
Uv2 = null;
|
||||
Uv3 = null;
|
||||
Uv4 = null;
|
||||
Colors = null;
|
||||
BoneWeights = null;
|
||||
MorphTargetVertices = null;
|
||||
MorphTargetNormals = null;
|
||||
MorphTargetTangents = null;
|
||||
Topology = null;
|
||||
Indices = null;
|
||||
subMeshVertexOffset = null;
|
||||
}
|
||||
|
||||
public bool IsEqual(UnityMeshData other)
|
||||
{
|
||||
bool CompareArray<T>(T[] array1, T[] array2)
|
||||
{
|
||||
if (array1 == null && array2 == null)
|
||||
return true;
|
||||
if (array1 == array2)
|
||||
return true;
|
||||
if (array1?.Length != array2?.Length)
|
||||
return false;
|
||||
|
||||
for (int i = 0; i < array1.Length; i++)
|
||||
{
|
||||
if (!array1[i].Equals(array2[i]))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CompareArray2<T>(T[][] array1, T[][] array2)
|
||||
{
|
||||
if (array1 == null && array2 == null)
|
||||
return true;
|
||||
if (array1 == array2)
|
||||
return true;
|
||||
if (array1?.Length != array2?.Length)
|
||||
return false;
|
||||
|
||||
for (int i = 0; i < array1.Length; i++)
|
||||
{
|
||||
if (array1[i] == null && array2[i] == null)
|
||||
continue;
|
||||
if (array1[i] == array2[i])
|
||||
continue;
|
||||
|
||||
if (array1[i].Length != array2[i].Length)
|
||||
return false;
|
||||
|
||||
for (int j = 0; j < array1.Length; j++)
|
||||
{
|
||||
if (!array1[i][j].Equals(array2[i][j]))
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!CompareArray(Vertices, other.Vertices)
|
||||
|| !CompareArray(Normals, other.Normals)
|
||||
|| !CompareArray(Tangents, other.Tangents)
|
||||
|| !CompareArray(Uv1, other.Uv1)
|
||||
|| !CompareArray(Uv2, other.Uv2)
|
||||
|| !CompareArray(Uv3, other.Uv3)
|
||||
|| !CompareArray(Uv4, other.Uv4)
|
||||
|| !CompareArray(Colors, other.Colors)
|
||||
|| !CompareArray(BoneWeights, other.BoneWeights)
|
||||
|| !CompareArray(Topology, other.Topology)
|
||||
|| !CompareArray(DrawModes, other.DrawModes)
|
||||
|| !CompareArray2(MorphTargetVertices, other.MorphTargetVertices)
|
||||
|| !CompareArray2(MorphTargetNormals, other.MorphTargetNormals)
|
||||
|| !CompareArray2(MorphTargetTangents, other.MorphTargetTangents)
|
||||
|| !CompareArray2(Indices, other.Indices))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5142460d60f647e0b377dd3cc0c0a909
|
||||
timeCreated: 1724924750
|
||||
Reference in New Issue
Block a user