From 9f5161b5eec0c99425ec0e207ec1e7df5eea5cd1 Mon Sep 17 00:00:00 2001 From: Andreas Atteneder Date: Mon, 12 Jul 2021 13:08:08 +0200 Subject: [PATCH] feat: `forceUnityLayout` parameter, to enforce a blend-shape and skinning compatible vertex buffer layout --- CHANGELOG.md | 4 + Runtime/Scripts/DracoMeshLoader.cs | 128 +++++++++++++++++++++++++---- Runtime/Scripts/DracoNative.cs | 61 ++++++++++---- package.json | 2 +- 4 files changed, 161 insertions(+), 34 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 35b4c03..4080004 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [3.1.0] - 2020-07-12 +### Added +- `forceUnityLayout` parameter, to enforce a blend-shape and skinning compatible vertex buffer layout + ## [3.0.3] - 2020-06-09 ### Added - Support for Lumin / Magic Leap diff --git a/Runtime/Scripts/DracoMeshLoader.cs b/Runtime/Scripts/DracoMeshLoader.cs index 73a0a8c..89d8dd0 100644 --- a/Runtime/Scripts/DracoMeshLoader.cs +++ b/Runtime/Scripts/DracoMeshLoader.cs @@ -96,20 +96,31 @@ public void Dispose() { /// If draco does not contain tangents and this is set to true, tangents and normals are calculated. /// Draco attribute ID that contains bone weights (for skinning) /// Draco attribute ID that contains bone joint indices (for skinning) + /// Enforces vertex buffer layout with highest compatibility. Enable this if you want to use blend shapes on the resulting mesh /// Unity Mesh or null in case of errors public async Task ConvertDracoMeshToUnity( NativeSlice encodedData, bool requireNormals = false, bool requireTangents = false, int weightsAttributeId = -1, - int jointsAttributeId = -1 + int jointsAttributeId = -1, + bool forceUnityLayout = false ) { var encodedDataPtr = GetUnsafeReadOnlyIntPtr(encodedData); #if DRACO_MESH_DATA var meshDataArray = Mesh.AllocateWritableMeshData(1); var mesh = meshDataArray[0]; - var result = await ConvertDracoMeshToUnity(mesh, encodedDataPtr, encodedData.Length, requireNormals, requireTangents, weightsAttributeId, jointsAttributeId); + var result = await ConvertDracoMeshToUnity( + mesh, + encodedDataPtr, + encodedData.Length, + requireNormals, + requireTangents, + weightsAttributeId, + jointsAttributeId, + forceUnityLayout + ); if (!result.success) { meshDataArray.Dispose(); return null; @@ -128,7 +139,15 @@ public async Task ConvertDracoMeshToUnity( } return unityMesh; #else - return await ConvertDracoMeshToUnity(encodedDataPtr, encodedData.Length, requireNormals, requireTangents, weightsAttributeId, jointsAttributeId); + return await ConvertDracoMeshToUnity( + encodedDataPtr, + encodedData.Length, + requireNormals, + requireTangents, + weightsAttributeId, + jointsAttributeId, + forceUnityLayout + ); #endif } @@ -140,13 +159,15 @@ public async Task ConvertDracoMeshToUnity( /// If draco does not contain tangents and this is set to true, tangents and normals are calculated. /// Draco attribute ID that contains bone weights (for skinning) /// Draco attribute ID that contains bone joint indices (for skinning) + /// Enforces vertex buffer layout with highest compatibility. Enable this if you want to use blend shapes on the resulting mesh /// Unity Mesh or null in case of errors public async Task ConvertDracoMeshToUnity( byte[] encodedData, bool requireNormals = false, bool requireTangents = false, int weightsAttributeId = -1, - int jointsAttributeId = -1 + int jointsAttributeId = -1, + bool forceUnityLayout = false #if UNITY_EDITOR ,bool sync = false #endif @@ -156,7 +177,15 @@ public async Task ConvertDracoMeshToUnity( #if DRACO_MESH_DATA var meshDataArray = Mesh.AllocateWritableMeshData(1); var mesh = meshDataArray[0]; - var result = await ConvertDracoMeshToUnity(mesh, encodedDataPtr, encodedData.Length, requireNormals, requireTangents, weightsAttributeId, jointsAttributeId + var result = await ConvertDracoMeshToUnity( + mesh, + encodedDataPtr, + encodedData.Length, + requireNormals, + requireTangents, + weightsAttributeId, + jointsAttributeId, + forceUnityLayout #if UNITY_EDITOR ,sync #endif @@ -176,7 +205,14 @@ public async Task ConvertDracoMeshToUnity( } return unityMesh; #else - var result = await ConvertDracoMeshToUnity(encodedDataPtr, encodedData.Length, requireNormals, requireTangents, weightsAttributeId, jointsAttributeId + var result = await ConvertDracoMeshToUnity( + encodedDataPtr, + encodedData.Length, + requireNormals, + requireTangents, + weightsAttributeId, + jointsAttributeId, + forceUnityLayout #if UNITY_EDITOR ,sync #endif @@ -187,34 +223,78 @@ public async Task ConvertDracoMeshToUnity( } #if DRACO_MESH_DATA + /// + /// Decodes a Draco mesh + /// + /// MeshData used to create the mesh + /// Compressed Draco data + /// If draco does not contain normals and this is set to true, normals are calculated. + /// If draco does not contain tangents and this is set to true, tangents and normals are calculated. + /// Draco attribute ID that contains bone weights (for skinning) + /// Draco attribute ID that contains bone joint indices (for skinning) + /// Enforces vertex buffer layout with highest compatibility. Enable this if you want to use blend shapes on the resulting mesh + /// A DecodeResult public async Task ConvertDracoMeshToUnity( Mesh.MeshData mesh, byte[] encodedData, bool requireNormals = false, bool requireTangents = false, int weightsAttributeId = -1, - int jointsAttributeId = -1 + int jointsAttributeId = -1, + bool forceUnityLayout = false ) { var encodedDataPtr = PinGCArrayAndGetDataAddress(encodedData, out var gcHandle); - var result = await ConvertDracoMeshToUnity(mesh, encodedDataPtr, encodedData.Length, requireNormals, requireTangents, weightsAttributeId, jointsAttributeId); + var result = await ConvertDracoMeshToUnity( + mesh, + encodedDataPtr, + encodedData.Length, + requireNormals, + requireTangents, + weightsAttributeId, + jointsAttributeId, + forceUnityLayout + ); UnsafeUtility.ReleaseGCObject(gcHandle); return result; } - public async Task ConvertDracoMeshToUnity(Mesh.MeshData mesh, NativeArray encodedData, bool requireNormals = false, + /// + /// Decodes a Draco mesh + /// + /// MeshData used to create the mesh + /// Compressed Draco data + /// If draco does not contain normals and this is set to true, normals are calculated. + /// If draco does not contain tangents and this is set to true, tangents and normals are calculated. + /// Draco attribute ID that contains bone weights (for skinning) + /// Draco attribute ID that contains bone joint indices (for skinning) + /// Enforces vertex buffer layout with highest compatibility. Enable this if you want to use blend shapes on the resulting mesh + /// A DecodeResult + public async Task ConvertDracoMeshToUnity( + Mesh.MeshData mesh, + NativeArray encodedData, + bool requireNormals = false, bool requireTangents = false, int weightsAttributeId = -1, - int jointsAttributeId = -1 + int jointsAttributeId = -1, + bool forceUnityLayout = false #if UNITY_EDITOR ,bool sync = false #endif ) { var encodedDataPtr = GetUnsafeReadOnlyIntPtr(encodedData); - return await ConvertDracoMeshToUnity(mesh, encodedDataPtr, encodedData.Length, requireNormals, requireTangents, weightsAttributeId, jointsAttributeId + return await ConvertDracoMeshToUnity( + mesh, + encodedDataPtr, + encodedData.Length, + requireNormals, + requireTangents, + weightsAttributeId, + jointsAttributeId, + forceUnityLayout #if UNITY_EDITOR - ,sync + ,sync #endif ); } @@ -228,7 +308,8 @@ async Task ConvertDracoMeshToUnity( bool requireNormals, bool requireTangents, int weightsAttributeId = -1, - int jointsAttributeId = -1 + int jointsAttributeId = -1, + bool forceUnityLayout = false #if UNITY_EDITOR ,bool sync = false #endif @@ -240,7 +321,8 @@ async Task ConvertDracoMeshToUnity( bool requireNormals, bool requireTangents, int weightsAttributeId = -1, - int jointsAttributeId = -1 + int jointsAttributeId = -1, + bool forceUnityLayout = false #if UNITY_EDITOR ,bool sync = false #endif @@ -275,9 +357,23 @@ async Task ConvertDracoMeshToUnity( requireNormals = true; } #if DRACO_MESH_DATA - dracoNative.CreateMesh(out result.calculateNormals, requireNormals, requireTangents, weightsAttributeId, jointsAttributeId); + dracoNative.CreateMesh( + out result.calculateNormals, + requireNormals, + requireTangents, + weightsAttributeId, + jointsAttributeId, + forceUnityLayout + ); #else - dracoNative.CreateMesh(out var calculateNormals, requireNormals, requireTangents, weightsAttributeId, jointsAttributeId); + dracoNative.CreateMesh( + out var calculateNormals, + requireNormals, + requireTangents, + weightsAttributeId, + jointsAttributeId, + forceUnityLayout + ); #endif #if UNITY_EDITOR diff --git a/Runtime/Scripts/DracoNative.cs b/Runtime/Scripts/DracoNative.cs index 1f5fe3e..696cc1b 100644 --- a/Runtime/Scripts/DracoNative.cs +++ b/Runtime/Scripts/DracoNative.cs @@ -179,7 +179,8 @@ void CalculateVertexParams( bool requireTangents, int weightsAttributeId, int jointsAttributeId, - out bool calculateNormals + out bool calculateNormals, + bool forceUnityLayout = false ) { Profiler.BeginSample("CalculateVertexParams"); @@ -258,29 +259,33 @@ bool CreateAttributeMapById(VertexAttribute type, int id, DracoMesh* draco, out if (requireTangents) { attributes.Add(new CalculatedAttributeMap(VertexAttribute.Tangent, VertexAttributeFormat.Float32, 4, 4 )); } - CreateAttributeMaps(AttributeType.COLOR, 1, dracoMesh); - var hasTexCoords = CreateAttributeMaps(AttributeType.TEX_COORD, 8, dracoMesh); + var hasTexCoordOrColor = CreateAttributeMaps(AttributeType.COLOR, 1, dracoMesh); + hasTexCoordOrColor |= CreateAttributeMaps(AttributeType.TEX_COORD, 8, dracoMesh); - var hasBlend = false; + var hasSkinning = false; if (weightsAttributeId >= 0) { if (CreateAttributeMapById(VertexAttribute.BlendWeight, weightsAttributeId, dracoMesh, out var map)) { // BLENDHACK: Don't add bone weights, as they won't exist after Mesh.SetBoneWeights // attributes.Add(map); boneWeightMap = map; - hasBlend = true; + hasSkinning = true; } } if (jointsAttributeId >= 0) { if (CreateAttributeMapById(VertexAttribute.BlendIndices, jointsAttributeId, dracoMesh, out var map)) { attributes.Add(map); boneIndexMap = map; - hasBlend = true; + hasSkinning = true; } } streamStrides = new int[maxStreamCount]; streamMemberCount = new int[maxStreamCount]; var streamIndex = 0; + + // skinning requires SkinnedMeshRenderer layout + forceUnityLayout |= hasSkinning; + foreach (var attributeMap in attributes) { // Stream assignment: // Positions get a dedicated stream (0) @@ -288,24 +293,37 @@ bool CreateAttributeMapById(VertexAttribute type, int id, DracoMesh* draco, out // If blend weights or blend indices are present, they land on stream 1 // while the rest is combined in stream 0 - // TODO: BLENDHACK; - // A potentially following Mesh.SetBoneWeights changes stream assignment again! - // Maybe it would be better to rebuild Unity's logic? + + // Mesh layout SkinnedMeshRenderer (used for skinning and blend shapes) + // requires: + // stream 0: position,normal,tangent + // stream 1: UVs,colors + // stream 2: blend weights/indices switch (attributeMap.attribute) { case VertexAttribute.Position: // Attributes that define/change the position go to stream 0 streamIndex = 0; break; - default: - // The rest to stream 1, but not if blend weights/joints are present - // In this case putting everything in stream 0 (except blend weights/joints) proved to work - streamIndex = hasBlend ? 0 : 1; + case VertexAttribute.Normal: + case VertexAttribute.Tangent: + streamIndex = forceUnityLayout ? 0 : 1; + break; + case VertexAttribute.TexCoord0: + case VertexAttribute.TexCoord1: + case VertexAttribute.TexCoord2: + case VertexAttribute.TexCoord3: + case VertexAttribute.TexCoord4: + case VertexAttribute.TexCoord5: + case VertexAttribute.TexCoord6: + case VertexAttribute.TexCoord7: + case VertexAttribute.Color: + streamIndex = 1; break; case VertexAttribute.BlendWeight: case VertexAttribute.BlendIndices: // Special case: blend weights/joints always have a special stream - streamIndex = 1; + streamIndex = hasTexCoordOrColor ? 2 : 1; break; } #if !DRACO_MESH_DATA @@ -465,12 +483,13 @@ public JobHandle DecodeVertexData( return releaseDreacoMeshJobHandle; } - public void CreateMesh( + internal void CreateMesh( out bool calculateNormals, bool requireNormals = false, bool requireTangents = false, int weightsAttributeId = -1, - int jointsAttributeId = -1 + int jointsAttributeId = -1, + bool forceUnityLayout = false ) { Profiler.BeginSample("CreateMesh"); @@ -478,7 +497,15 @@ public void CreateMesh( var dracoMesh = (DracoMesh*)dracoTempResources[meshPtrIndex]; allocator = dracoMesh->numVertices > persistentDataThreshold ? Allocator.Persistent : Allocator.TempJob; - CalculateVertexParams(dracoMesh, requireNormals, requireTangents, weightsAttributeId, jointsAttributeId, out calculateNormals); + CalculateVertexParams( + dracoMesh, + requireNormals, + requireTangents, + weightsAttributeId, + jointsAttributeId, + out calculateNormals, + forceUnityLayout + ); Profiler.BeginSample("SetParameters"); #if DRACO_MESH_DATA diff --git a/package.json b/package.json index e8c6d25..fccc409 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "com.atteneder.draco", - "version": "3.0.3", + "version": "3.1.0", "displayName": "Draco 3D Data Compression", "description": "Draco is an open-source library for compressing and decompressing 3D geometric meshes and point clouds. It is intended to improve the storage and transmission of 3D graphics. This package allows you to apply Draco compression to meshes, import Draco files and load them at runtime.", "unity": "2019.3",