Fix: blendshape normals import (UniGLTF/UniVRM)

- Properly decode morph target VEC3 accessors (normalized BYTE/SHORT, etc.) for POSITION/NORMAL/TANGENT
  - Preserve imported morph normal deltas (remove heuristic recompute)
  - Add intermediate frames for normal-only targets to stabilize Unity’s normal interpolation
  - Copy all blendshape frames when duplicating meshes so extra frames are retained
This commit is contained in:
tdw46
2025-12-29 22:28:31 -05:00
parent 3b99078d26
commit 1f2570a41c
7 changed files with 285 additions and 29 deletions

View File

@@ -52,8 +52,8 @@ namespace UniGLTF.MeshUtility
if (copyBlendShape)
{
var vertices = src.vertices;
var normals = src.normals;
var deltaVertices = new Vector3[src.vertexCount];
var deltaNormals = new Vector3[src.vertexCount];
Vector3[] tangents = null;
if (Symbols.VRM_NORMALIZE_BLENDSHAPE_TANGENT)
{
@@ -62,14 +62,18 @@ namespace UniGLTF.MeshUtility
for (int i = 0; i < src.blendShapeCount; ++i)
{
src.GetBlendShapeFrameVertices(i, 0, vertices, normals, tangents);
dst.AddBlendShapeFrame(
src.GetBlendShapeName(i),
src.GetBlendShapeFrameWeight(i, 0),
vertices,
normals,
tangents
);
var frameCount = src.GetBlendShapeFrameCount(i);
for (int f = 0; f < frameCount; ++f)
{
src.GetBlendShapeFrameVertices(i, f, deltaVertices, deltaNormals, tangents);
dst.AddBlendShapeFrame(
src.GetBlendShapeName(i),
src.GetBlendShapeFrameWeight(i, f),
deltaVertices,
deltaNormals,
tangents
);
}
}
}

View File

@@ -283,6 +283,138 @@ namespace UniGLTF
}
}
private static NativeArray<Vector3> GetMorphTargetVec3(GltfData data, int accessorIndex, string attribute)
{
if (accessorIndex < 0) return data.NativeArrayManager.CreateNativeArray<Vector3>(0);
var accessor = data.GLTF.accessors[accessorIndex];
if (accessor.type != "VEC3")
{
throw new ArgumentException($"unknown {attribute} type: {accessor.componentType}:{accessor.type}");
}
static float NormalizeSByte(sbyte v)
{
// glTF normalized signed integer maps min to -1.0 exactly.
return Mathf.Max(v / 127.0f, -1.0f);
}
static float NormalizeShort(short v)
{
// glTF normalized signed integer maps min to -1.0 exactly.
return Mathf.Max(v / 32767.0f, -1.0f);
}
switch (accessor.componentType)
{
case glComponentType.FLOAT:
return data.GetArrayFromAccessor<Vector3>(accessorIndex);
case glComponentType.BYTE:
{
var src = data.GetArrayFromAccessor<SByte3>(accessorIndex);
var dst = data.NativeArrayManager.CreateNativeArray<Vector3>(src.Length);
if (accessor.normalized)
{
for (int i = 0; i < src.Length; ++i)
{
var v = src[i];
dst[i] = new Vector3(
NormalizeSByte(v.x),
NormalizeSByte(v.y),
NormalizeSByte(v.z));
}
}
else
{
for (int i = 0; i < src.Length; ++i)
{
var v = src[i];
dst[i] = new Vector3(v.x, v.y, v.z);
}
}
return dst;
}
case glComponentType.UNSIGNED_BYTE:
{
var src = data.GetArrayFromAccessor<Byte3>(accessorIndex);
var dst = data.NativeArrayManager.CreateNativeArray<Vector3>(src.Length);
if (accessor.normalized)
{
const float factor = 1.0f / 255.0f;
for (int i = 0; i < src.Length; ++i)
{
var v = src[i];
dst[i] = new Vector3(v.x * factor, v.y * factor, v.z * factor);
}
}
else
{
for (int i = 0; i < src.Length; ++i)
{
var v = src[i];
dst[i] = new Vector3(v.x, v.y, v.z);
}
}
return dst;
}
case glComponentType.SHORT:
{
var src = data.GetArrayFromAccessor<Short3>(accessorIndex);
var dst = data.NativeArrayManager.CreateNativeArray<Vector3>(src.Length);
if (accessor.normalized)
{
for (int i = 0; i < src.Length; ++i)
{
var v = src[i];
dst[i] = new Vector3(
NormalizeShort(v.x),
NormalizeShort(v.y),
NormalizeShort(v.z));
}
}
else
{
for (int i = 0; i < src.Length; ++i)
{
var v = src[i];
dst[i] = new Vector3(v.x, v.y, v.z);
}
}
return dst;
}
case glComponentType.UNSIGNED_SHORT:
{
var src = data.GetArrayFromAccessor<UShort3>(accessorIndex);
var dst = data.NativeArrayManager.CreateNativeArray<Vector3>(src.Length);
if (accessor.normalized)
{
const float factor = 1.0f / 65535.0f;
for (int i = 0; i < src.Length; ++i)
{
var v = src[i];
dst[i] = new Vector3(v.x * factor, v.y * factor, v.z * factor);
}
}
else
{
for (int i = 0; i < src.Length; ++i)
{
var v = src[i];
dst[i] = new Vector3(v.x, v.y, v.z);
}
}
return dst;
}
default:
throw new NotImplementedException($"unknown {attribute} type: {accessor.componentType}:{accessor.type}");
}
}
/// <summary>
/// 各 primitive の attribute の要素が同じでない。=> uv が有るものと無いものが混在するなど
/// glTF 的にはありうる。
@@ -438,7 +570,7 @@ namespace UniGLTF
var blendShape = GetOrCreateBlendShape(i);
if (primTarget.POSITION != -1)
{
var array = data.GetArrayFromAccessor<Vector3>(primTarget.POSITION);
var array = GetMorphTargetVec3(data, primTarget.POSITION, "POSITION");
if (array.Length != positions.Length)
{
throw new Exception("different length");
@@ -449,7 +581,7 @@ namespace UniGLTF
if (primTarget.NORMAL != -1)
{
var array = data.GetArrayFromAccessor<Vector3>(primTarget.NORMAL);
var array = GetMorphTargetVec3(data, primTarget.NORMAL, "NORMAL");
if (array.Length != positions.Length)
{
throw new Exception("different length");
@@ -460,7 +592,7 @@ namespace UniGLTF
if (primTarget.TANGENT != -1)
{
var array = data.GetArrayFromAccessor<Vector3>(primTarget.TANGENT);
var array = GetMorphTargetVec3(data, primTarget.TANGENT, "TANGENT");
if (array.Length != positions.Length)
{
throw new Exception("different length");
@@ -579,7 +711,7 @@ namespace UniGLTF
if (hasPosition)
{
var morphPositions = data.GetArrayFromAccessor<Vector3>(primTarget.POSITION);
var morphPositions = GetMorphTargetVec3(data, primTarget.POSITION, "POSITION");
blendShape.Positions.Capacity = morphPositions.Length;
for (var j = 0; j < positions.Length; ++j)
{
@@ -589,7 +721,7 @@ namespace UniGLTF
if (hasNormal)
{
var morphNormals = data.GetArrayFromAccessor<Vector3>(primTarget.NORMAL);
var morphNormals = GetMorphTargetVec3(data, primTarget.NORMAL, "NORMAL");
blendShape.Normals.Capacity = morphNormals.Length;
for (var j = 0; j < positions.Length; ++j)
{
@@ -600,7 +732,7 @@ namespace UniGLTF
if (hasTangent)
{
var morphTangents = data.GetArrayFromAccessor<Vector3>(primTarget.TANGENT);
var morphTangents = GetMorphTargetVec3(data, primTarget.TANGENT, "TANGENT");
blendShape.Tangents.Capacity = morphTangents.Length;
for (var j = 0; j < positions.Length; ++j)
{
@@ -639,4 +771,4 @@ namespace UniGLTF
}
}
}
}
}

View File

@@ -1,5 +1,4 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.Profiling;
@@ -10,6 +9,36 @@ namespace UniGLTF
internal static class MeshUploader
{
private const float FrameWeight = 100.0f;
private const float EpsilonSqr = 1e-16f;
private static bool HasAnyNonZero(Vector3[] delta)
{
if (delta == null) return false;
for (int i = 0; i < delta.Length; i++)
{
if (delta[i].sqrMagnitude > EpsilonSqr) return true;
}
return false;
}
private static Vector3[] CalcDeltaNormalsForWeight(
Vector3[] baseNormals,
Vector3[] deltaNormalsAt100,
float weight01)
{
var delta = new Vector3[baseNormals.Length];
for (int i = 0; i < baseNormals.Length; i++)
{
var n = baseNormals[i] + deltaNormalsAt100[i] * weight01;
var sqr = n.sqrMagnitude;
if (sqr > float.Epsilon)
{
n *= 1.0f / Mathf.Sqrt(sqr);
}
delta[i] = n - baseNormals[i];
}
return delta;
}
/// <summary>
/// 頂点情報をMeshに対して送る
@@ -41,28 +70,59 @@ namespace UniGLTF
}
}
private static async Task BuildBlendShapeAsync(IAwaitCaller awaitCaller, Mesh mesh, BlendShape blendShape,
Vector3[] emptyVertices)
private static async Task BuildBlendShapeAsync(
IAwaitCaller awaitCaller,
Mesh mesh,
BlendShape blendShape,
Vector3[] emptyVertices,
Vector3[] baseNormals)
{
Vector3[] positions = null;
Vector3[] normals = null;
await awaitCaller.Run(() =>
{
positions = blendShape.Positions.ToArray();
if (blendShape.Normals != null)
{
normals = blendShape.Normals.ToArray();
}
positions = blendShape.Positions != null ? blendShape.Positions.ToArray() : Array.Empty<Vector3>();
normals = blendShape.Normals != null ? blendShape.Normals.ToArray() : Array.Empty<Vector3>();
});
Profiler.BeginSample("MeshUploader.BuildBlendShapeAsync");
var hasPositions = positions.Length == mesh.vertexCount;
var hasNormals = normals.Length == mesh.vertexCount;
// Unity blendshape normal interpolation can look slightly off when vertex deltas are all-zero
// (normal-only targets). Add a few intermediate frames with renormalized normals to keep the
// interpolation closer to the intended (unit-length) normals across weights.
if (hasNormals && !HasAnyNonZero(positions) && HasAnyNonZero(normals))
{
foreach (var frameWeight in new[] { 25.0f, 50.0f, 75.0f })
{
var deltaNormals = CalcDeltaNormalsForWeight(baseNormals, normals, frameWeight / 100.0f);
mesh.AddBlendShapeFrame(blendShape.Name, frameWeight,
emptyVertices,
deltaNormals,
null
);
}
mesh.AddBlendShapeFrame(blendShape.Name, FrameWeight,
emptyVertices,
normals,
null
);
Profiler.EndSample();
return;
}
if (positions.Length > 0)
{
if (positions.Length == mesh.vertexCount)
if (hasPositions)
{
var deltaNormals = hasNormals ? normals : null;
mesh.AddBlendShapeFrame(blendShape.Name, FrameWeight,
positions,
normals.Length == mesh.vertexCount && normals.Length == positions.Length ? normals : null,
deltaNormals,
null
);
}
@@ -76,7 +136,7 @@ namespace UniGLTF
// add empty blend shape for keep blend shape index
mesh.AddBlendShapeFrame(blendShape.Name, FrameWeight,
emptyVertices,
null,
normals.Length == mesh.vertexCount ? normals : null,
null
);
}
@@ -129,10 +189,16 @@ namespace UniGLTF
if (data.BlendShapes.Count > 0)
{
var baseNormals = mesh.normals;
var emptyVertices = new Vector3[mesh.vertexCount];
foreach (var blendShape in data.BlendShapes)
{
await BuildBlendShapeAsync(awaitCaller, mesh, blendShape, emptyVertices);
await BuildBlendShapeAsync(
awaitCaller,
mesh,
blendShape,
emptyVertices,
baseNormals);
}
}

View File

@@ -0,0 +1,25 @@
using System;
using System.Runtime.InteropServices;
namespace UniGLTF
{
[Serializable, StructLayout(LayoutKind.Sequential, Pack = 1)]
public readonly struct SByte3 : IEquatable<SByte3>
{
public readonly sbyte x;
public readonly sbyte y;
public readonly sbyte z;
public SByte3(sbyte _x, sbyte _y, sbyte _z)
{
x = _x;
y = _y;
z = _z;
}
public bool Equals(SByte3 other)
{
return x == other.x && y == other.y && z == other.z;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 93c772ed657e23c448e9b036d9c9071c

View File

@@ -0,0 +1,25 @@
using System;
using System.Runtime.InteropServices;
namespace UniGLTF
{
[Serializable, StructLayout(LayoutKind.Sequential, Pack = 1)]
public readonly struct Short3 : IEquatable<Short3>
{
public readonly short x;
public readonly short y;
public readonly short z;
public Short3(short _x, short _y, short _z)
{
x = _x;
y = _y;
z = _z;
}
public bool Equals(Short3 other)
{
return x == other.x && y == other.y && z == other.z;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: f74b316e688844c428c7b45dcb38ee54