#if UNITY_EDITOR using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Text.RegularExpressions; using GPUECSAnimationBaker.Engine.AnimatorSystem; using GpuEcsAnimationBaker.Engine.Data; using Unity.Collections; using Unity.Mathematics; using UnityEditor; using UnityEngine; namespace GPUECSAnimationBaker.Engine.Baker { public static class GpuEcsAnimationBakerServices { private static readonly int AnimatedBoneMatrices = Shader.PropertyToID("_AnimatedBoneMatrices"); private static readonly int EnableAnimation = Shader.PropertyToID("_EnableAnimation"); public static bool ValidateAnimationBakerData(GpuEcsAnimationBakerData bakerData, GameObject sourceModel, out string errors) { StringBuilder sbErrors = new StringBuilder(); if (bakerData.animations.Length == 0) sbErrors.AppendLine("At least one animation must be baked."); foreach (AnimationData animation in bakerData.animations) { if (string.IsNullOrWhiteSpace(animation.animationID)) sbErrors.AppendLine("Animation ID is mandatory"); if (!Regex.IsMatch(animation.animationID, @"^[a-zA-Z_][a-zA-Z0-9_]+$")) sbErrors.AppendLine("Animation ID must be only letters, numbers or underscore, must not start with number"); if (string.IsNullOrWhiteSpace(animation.animatorStateName)) sbErrors.AppendLine("Animation State Name is mandatory"); if (animation.animationType == AnimationTypes.SingleClip) { if (animation.singleClipData.animationClip == null) sbErrors.AppendLine("Animation Clip is mandatory"); } else if (animation.animationType == AnimationTypes.DualClipBlend) { if (string.IsNullOrWhiteSpace(animation.dualClipBlendData.blendParameterName)) sbErrors.AppendLine("Blend parameter name is mandatory"); if (animation.dualClipBlendData.clip1.animationClip == null) sbErrors.AppendLine("Animation Clip 1 is mandatory"); if (animation.dualClipBlendData.clip1.animationClip == null) sbErrors.AppendLine("Animation Clip 2 is mandatory"); if (animation.dualClipBlendData.nbrOfInBetweenSamples < 2) sbErrors.AppendLine("Nbr of in between samples must be at least 2"); if (animation.dualClipBlendData.nbrOfInBetweenSamples > 100) sbErrors.AppendLine("Nbr of in between samples is maximum 100"); } foreach (AnimatorParameter parameterValue in animation.additionalAnimatorParameterValues) { if (string.IsNullOrWhiteSpace(parameterValue.parameterName)) sbErrors.AppendLine("Additional animator parameter name is mandatory"); } } bool foundDouble = false; for (int i = 0; i < bakerData.animations.Length; i++) { for (int j = i + 1; j < bakerData.animations.Length; j++) { if (bakerData.animations[i].animationID == bakerData.animations[j].animationID) { foundDouble = true; break; } } if(foundDouble) break; } if(foundDouble) sbErrors.AppendLine("Animation IDs must be unique"); foreach (AttachmentAnchor attachmentAnchor in bakerData.attachmentAnchors) { if (string.IsNullOrWhiteSpace(attachmentAnchor.attachmentAnchorID)) sbErrors.AppendLine("Attachment Anchor ID is mandatory"); if(attachmentAnchor.attachmentAnchorTransform == null) sbErrors.AppendLine("Attachment Anchor reference transform is mandatory"); else if(!hasParent(attachmentAnchor.attachmentAnchorTransform, sourceModel.transform)) sbErrors.AppendLine($"Attachment Anchor reference transform must be nested inside {sourceModel.name}"); } if(bakerData.boneUsage.numberOfBonesPerVertex < 1) sbErrors.AppendLine("Nbr of bones per vertex must be at least 1"); foreach (BoneUsagePerLoD boneUsagePerLOD in bakerData.boneUsage.boneUsagesPerLoD) { if(boneUsagePerLOD.maxNumberOfBonesPerVertex < 1) sbErrors.AppendLine("Nbr of bones per vertex must be at least 1"); } errors = sbErrors.ToString(); return sbErrors.Length == 0; } private static bool hasParent(Transform child, Transform parent) { if (child.parent == null) return false; if (child.parent.gameObject == parent.gameObject) return true; else return hasParent(child.parent, parent); } public static GameObject GenerateAnimationObject(string assetPath, GpuEcsAnimationBakerData bakerData, string animatorName, string generatedAssetsFolder, string nameSuffixAsset = "_GpuEcsAnimator", string nameSuffixAnimationIDsEnum = "_AnimationIDs", string nameSuffixAnimationInitializerBehaviour = "_AnimationInitializerBehaviour", string nameSuffixAnimationEventIDsEnum = "_AnimationEventIDs", string nameSuffixAnimationAnchorIDsEnum = "_AttachmentAnchorIDs", string nameSuffixAttachmentInitializerBehaviour = "_AttachmentInitializerBehaviour", string meshPartSuffix = "Mesh", string animationMatricesTexturePartSuffix = "AnimationMatricesTexture", string materialPartSuffix = "Material" ) { GameObject sourceModel = AssetDatabase.LoadAssetAtPath(assetPath); GameObject refModel = PrefabUtility.LoadPrefabContents(assetPath); Debug.Log($"Generating Animation object for {assetPath}"); GameObject animationObject = GenerateAnimationObjectFromModel(refModel, sourceModel, bakerData, animatorName, generatedAssetsFolder, nameSuffixAsset, nameSuffixAnimationIDsEnum, nameSuffixAnimationInitializerBehaviour, nameSuffixAnimationEventIDsEnum, nameSuffixAnimationAnchorIDsEnum, nameSuffixAttachmentInitializerBehaviour, meshPartSuffix, animationMatricesTexturePartSuffix, materialPartSuffix); PrefabUtility.UnloadPrefabContents(refModel); return animationObject; } public static GameObject GenerateAnimationObjectFromModel(GameObject refModel, GameObject sourceModel, GpuEcsAnimationBakerData bakerData, string animatorName, string generatedAssetsFolder, string nameSuffixAsset = "_GpuEcsAnimator", string nameSuffixAnimationIDsEnum = "_AnimationIDs", string nameSuffixAnimationInitializerBehaviour = "_AnimationInitializerBehaviour", string nameSuffixAnimationEventIDsEnum = "_AnimationEventIDs", string nameSuffixAnimationAnchorIDsEnum = "_AttachmentAnchorIDs", string nameSuffixAttachmentInitializerBehaviour = "_AttachmentInitializerBehaviour", string meshPartSuffix = "Mesh", string animationMatricesTexturePartSuffix = "AnimationMatricesTexture", string materialPartSuffix = "Material" ) { if(!ValidateAnimationBakerData(bakerData, sourceModel, out string errors)) { Debug.LogError(errors); return null; } string targetAssetPath = Path.Combine(generatedAssetsFolder, $"{animatorName}{nameSuffixAsset}.prefab"); string targetAnimationEnumAssetPath = Path.Combine(generatedAssetsFolder, $"{animatorName}{nameSuffixAnimationIDsEnum}.cs"); string targetAnimationInitializerBehaviourAssetPath = Path.Combine(generatedAssetsFolder, $"{animatorName}{nameSuffixAnimationInitializerBehaviour}.cs"); string targetAnimationEventEnumAssetPath = Path.Combine(generatedAssetsFolder, $"{animatorName}{nameSuffixAnimationEventIDsEnum}.cs"); string targetAttachmentAnchorEnumAssetPath = Path.Combine(generatedAssetsFolder, $"{animatorName}{nameSuffixAnimationAnchorIDsEnum}.cs"); string targetAttachmentInitializerBehaviourAssetPath = Path.Combine(generatedAssetsFolder, $"{animatorName}{nameSuffixAttachmentInitializerBehaviour}.cs"); refModel.transform.position = Vector3.zero; refModel.transform.rotation = quaternion.identity; Animator refModelAnimator = refModel.GetComponent(); refModelAnimator.cullingMode = AnimatorCullingMode.AlwaysAnimate; LODGroup refModelLoDGroup = refModel.GetComponent(); LOD[] refModelLoDs = refModelLoDGroup == null ? null : refModelLoDGroup.GetLODs(); List animationMatricesTexturesCache = new List(); List ecsGpuMaterialsCache = new List(); GameObject test = AssetDatabase.LoadAssetAtPath(targetAssetPath); if (test == null) { GameObject empty = new GameObject(refModel.name); PrefabUtility.SaveAsPrefabAsset(empty, targetAssetPath); GameObject.DestroyImmediate(empty, allowDestroyingAssets:true); } GameObject target = PrefabUtility.LoadPrefabContents(targetAssetPath); target.transform.localScale = refModel.transform.localScale; GameObject[] toDestroy = new GameObject[target.transform.childCount]; for (int childIndex = 0; childIndex < target.transform.childCount; childIndex++) toDestroy[childIndex] = target.transform.GetChild(childIndex).gameObject; foreach(GameObject go in toDestroy) Object.DestroyImmediate(go, allowDestroyingAssets: true); RemoveComponent(target); RemoveComponent(target); RemoveComponent(target); RemoveComponent(target); RemoveComponent(target); GpuEcsAnimatorBehaviour gpuEcsAnimator = AddGpuEcsAnimationBehaviour(target, bakerData, out string[] eventNames); gpuEcsAnimator.transformUsageFlags = bakerData.transformUsageFlagsParent; Dictionary sourceToTargetMapping = new Dictionary(); ProcessAnimationObjectRecursive(refModel, target, sourceToTargetMapping, animatorName, meshPartSuffix, animationMatricesTexturePartSuffix, materialPartSuffix, refModelAnimator, refModelLoDs, animationMatricesTexturesCache, ecsGpuMaterialsCache, bakerData, gpuEcsAnimator.animations, gpuEcsAnimator.totalNbrOfFrames, generatedAssetsFolder, gpuEcsAnimator); if (refModelLoDs != null) { LODGroup targetLoDGroup = target.AddComponent(); LOD[] targetLoDs = new LOD[refModelLoDs.Length]; for (int lodIndex = 0; lodIndex < refModelLoDs.Length; lodIndex++) { targetLoDs[lodIndex] = refModelLoDs[lodIndex]; Renderer[] refModelRenderers = refModelLoDs[lodIndex].renderers; for (int rendererIndex = 0; rendererIndex < refModelRenderers.Length; rendererIndex++) { targetLoDs[lodIndex].renderers[rendererIndex] = sourceToTargetMapping[refModelRenderers[rendererIndex].gameObject].GetComponent(); } } targetLoDGroup.SetLODs(targetLoDs); } if(bakerData.generateAnimationIdsEnum) GenerateAnimationsEnumCode(bakerData, targetAnimationEnumAssetPath, targetAnimationInitializerBehaviourAssetPath); if(bakerData.generateAnimationEventIdsEnum) GenerateAnimationEventsEnumCode(bakerData, targetAnimationEventEnumAssetPath, eventNames ); gpuEcsAnimator.nbrOfAttachmentAnchors = bakerData.attachmentAnchors.Length; gpuEcsAnimator.attachmentAnchorData = BakeAttachmentAnchorTransforms(generatedAssetsFolder, sourceModel, animatorName, refModelAnimator, bakerData, gpuEcsAnimator.animations, gpuEcsAnimator.totalNbrOfFrames,gpuEcsAnimator); if(bakerData.generateAttachmentAnchorIdsEnum) GenerateAttachmentAnchorsEnumCode(bakerData, targetAttachmentAnchorEnumAssetPath, targetAttachmentInitializerBehaviourAssetPath); PrefabUtility.SaveAsPrefabAsset(target, targetAssetPath); PrefabUtility.UnloadPrefabContents(target); return AssetDatabase.LoadAssetAtPath(targetAssetPath); } private const string enumFileTemplate = "namespace GPUECSAnimationBaker.Engine.AnimatorSystem\n" + "{\n" + " public enum @ENUMNAME@\n" + " {\n" + "@IDLIST@\n" + " }\n" + "}"; private const string behaviourFileTemplate = "namespace GPUECSAnimationBaker.Engine.AnimatorSystem\n" + "{\n" + " public class @CLASSNAME_@ENUMNAME@_Behaviour : @CLASSNAMEBehaviour<@ENUMNAME@> { } \n" + "}"; private static void GenerateAnimationsEnumCode(GpuEcsAnimationBakerData bakerData, string targetEnumAssetPath, string targetEnumBehaviourAssetPath) { GenerateEnumCodeFiles($"AnimationIds{bakerData.animationIdsEnumName}", "GpuEcsAnimatorInitializer", bakerData.animations, (animationData) => animationData.animationID, targetEnumAssetPath, targetEnumBehaviourAssetPath); } private static void GenerateAnimationEventsEnumCode(GpuEcsAnimationBakerData bakerData, string targetEnumAssetPath, string[] eventNames) { GenerateEnumCodeFiles($"AnimationEventIds{bakerData.animationEventIdsEnumName}", null, eventNames, (name) => name, targetEnumAssetPath, null); } private static void GenerateAttachmentAnchorsEnumCode(GpuEcsAnimationBakerData bakerData, string targetEnumAssetPath, string targetEnumBehaviourAssetPath) { GenerateEnumCodeFiles($"AnchorIds{bakerData.attachmentAnchorIdsEnumName}", "GpuEcsAttachmentInitializer", bakerData.attachmentAnchors, (attachmentAnchor) => attachmentAnchor.attachmentAnchorID, targetEnumAssetPath, targetEnumBehaviourAssetPath); } private static void GenerateEnumCodeFiles(string enumName, string className, T[] list, System.Func listIdGetter, string targetEnumAssetPath, string targetEnumBehaviourAssetPath) { StringBuilder idList = new StringBuilder(); for (int index = 0; index < list.Length; index++) { idList.Append($" {listIdGetter(list[index])} = {index.ToString()}"); if (index < list.Length - 1) { idList.AppendLine(","); } } string enumCodeText = enumFileTemplate .Replace("@ENUMNAME@", enumName) .Replace("@IDLIST@", idList.ToString()); string enumCodePath = Path.Combine(Application.dataPath, "../" + targetEnumAssetPath); File.WriteAllText(enumCodePath, enumCodeText); if (targetEnumBehaviourAssetPath != null) { string enumBehaviourText = behaviourFileTemplate .Replace("@CLASSNAME", className) .Replace("@ENUMNAME@", enumName); string enumBehaviourPath = Path.Combine(Application.dataPath, "../" + targetEnumBehaviourAssetPath); File.WriteAllText(enumBehaviourPath, enumBehaviourText); } AssetDatabase.Refresh(); } private static GpuEcsAnimatorBehaviour AddGpuEcsAnimationBehaviour(GameObject target, GpuEcsAnimationBakerData bakerData, out string[] eventNames) { GpuEcsAnimatorBehaviour gpuEcsAnimator = target.AddComponent(); gpuEcsAnimator.animations = new GpuEcsAnimationData[bakerData.animations.Length]; // gpuEcsAnimator.BackAnimationDatas= new AnimationData[bakerData.animations.Length]; List occurences = new List(); List foundEvents = new List(); int currentFrameIndex = 0; int currentEventOccurenceId = 0; int currentFoundEventId = 0; for (int animationIndex = 0; animationIndex < bakerData.animations.Length; animationIndex++) { AnimationData animationData = bakerData.animations[animationIndex]; int nbrOfFramesPerSample = 0; int nbrOfInBetweenSamples = 0; float blendTimeCorrection = 1; int startEventOccurenceId = currentEventOccurenceId; int nbrOfEventOccurenceIds = 0; if (animationData.animationType == AnimationTypes.SingleClip) { SingleClipData singleClipData = animationData.singleClipData; nbrOfFramesPerSample = (int)(singleClipData.animationClip.length * GlobalConstants.SampleFrameRate) + 1; nbrOfInBetweenSamples = 1; blendTimeCorrection = 1; AddAnimationEvents(bakerData, animationData, occurences, ref currentEventOccurenceId, ref nbrOfEventOccurenceIds, foundEvents, ref currentFoundEventId, singleClipData.animationClip); } else if (animationData.animationType == AnimationTypes.DualClipBlend) { DualClipBlendData dualClipBlendData = animationData.dualClipBlendData; int clip1NbrOfFrames = (int)(dualClipBlendData.clip1.animationClip.length * GlobalConstants.SampleFrameRate) + 1; int clip2NbrOfFrames = (int)(dualClipBlendData.clip2.animationClip.length * GlobalConstants.SampleFrameRate) + 1; nbrOfFramesPerSample = math.max(clip1NbrOfFrames, clip2NbrOfFrames); blendTimeCorrection = dualClipBlendData.clip1.animationClip.length / dualClipBlendData.clip2.animationClip.length; nbrOfInBetweenSamples = dualClipBlendData.nbrOfInBetweenSamples; AddAnimationEvents(bakerData, animationData, occurences, ref currentEventOccurenceId, ref nbrOfEventOccurenceIds, foundEvents, ref currentFoundEventId, dualClipBlendData.clip1.animationClip); AddAnimationEvents(bakerData, animationData, occurences, ref currentEventOccurenceId, ref nbrOfEventOccurenceIds, foundEvents, ref currentFoundEventId, dualClipBlendData.clip2.animationClip); } bool loop = animationData.loop; gpuEcsAnimator.animations[animationIndex] = new GpuEcsAnimationData() { startFrameIndex = currentFrameIndex, nbrOfFramesPerSample = nbrOfFramesPerSample, nbrOfInBetweenSamples = nbrOfInBetweenSamples, blendTimeCorrection = blendTimeCorrection, startEventOccurenceId = startEventOccurenceId, nbrOfEventOccurenceIds = nbrOfEventOccurenceIds, loop = loop, animationID = animationData.animationID, stateName = animationData.animatorStateName }; currentFrameIndex += nbrOfFramesPerSample * nbrOfInBetweenSamples; } gpuEcsAnimator.totalNbrOfFrames = currentFrameIndex; gpuEcsAnimator.animationEventOccurences = occurences.ToArray(); eventNames = bakerData.usePredefinedAnimationEventIds ? bakerData.predefinedAnimationEventIds : foundEvents.ToArray(); return gpuEcsAnimator; } private static void AddAnimationEvents(GpuEcsAnimationBakerData bakerData, AnimationData animationData, List occurences, ref int currentEventOccurenceId, ref int nbrOfEventOccurenceIds, List foundEvents, ref int currentFoundEventId, AnimationClip animationClip) { AnimationEvent[] animationEvents = AnimationUtility.GetAnimationEvents(animationClip); foreach (AnimationEvent animationEvent in animationEvents) { int eventId = -1; if (bakerData.usePredefinedAnimationEventIds) { eventId = bakerData.predefinedAnimationEventIds.ToList().FindIndex((n) => n.Equals(animationEvent.stringParameter)); if(eventId == -1) Debug.LogWarning( $"Found event {animationEvent.stringParameter} that is not in the predefined event Ids list, so it will be ignored."); } else { string name = $"{animationData.animationID}_{animationEvent.stringParameter}"; eventId = foundEvents.FindIndex((n) => n.Equals(name)); if (eventId == -1) { eventId = currentFoundEventId; foundEvents.Add(name); currentFoundEventId++; } } if (eventId != -1) { occurences.Add(new GpuEcsAnimationEventOccurence() { eventId = eventId, eventNormalizedTime = animationEvent.time / animationClip.length, }); currentEventOccurenceId++; nbrOfEventOccurenceIds++; } } } private static void ProcessAnimationObjectRecursive(GameObject sourceNode, GameObject targetNode, Dictionary sourceToTargetMapping, string animatorName, string meshPartSuffix, string animationMatricesTexturePartSuffix, string materialPartSuffix, Animator refModelAnimator, LOD[] refModelLoDs, List animationMatricesTexturesCache, List ecsGpuMaterialsCache, GpuEcsAnimationBakerData bakerData, GpuEcsAnimationData[] animations, int totalNbrOfFrames, string generatedAssetsFolder, GpuEcsAnimatorBehaviour gpuEcsAnimator) { sourceToTargetMapping.Add(sourceNode, targetNode); SkinnedMeshRenderer existingSkinnedMeshRenderer = sourceNode.GetComponent(); if (existingSkinnedMeshRenderer != null) { Texture2D animationMatricesTexture = BakeAndSaveAnimationMatricesTexture( animationMatricesTexturesCache, generatedAssetsFolder, animatorName, animationMatricesTexturePartSuffix, refModelAnimator, bakerData, existingSkinnedMeshRenderer, animations, totalNbrOfFrames); Mesh newMesh = BakeAndSaveBoneWeightsIntoMesh(refModelLoDs, generatedAssetsFolder, animatorName, meshPartSuffix, bakerData, existingSkinnedMeshRenderer); MeshFilter meshFilter = targetNode.AddComponent(); MeshRenderer meshRenderer = targetNode.AddComponent(); Material newMaterial = CreateAndSaveEcsGpuMaterial(ecsGpuMaterialsCache, generatedAssetsFolder, animatorName, materialPartSuffix, existingSkinnedMeshRenderer); newMaterial.SetTexture(AnimatedBoneMatrices, animationMatricesTexture); meshRenderer.sharedMaterial = newMaterial; meshFilter.sharedMesh = newMesh; GpuEcsAnimatedMeshBehaviour gpuEcsAnimatedMesh = targetNode.AddComponent(); gpuEcsAnimatedMesh.animator = gpuEcsAnimator; gpuEcsAnimatedMesh.transformUsageFlags = bakerData.transformUsageFlagsChildren; } for (int childIndex = 0; childIndex < sourceNode.transform.childCount; childIndex++) { GameObject sourceChild = sourceNode.transform.GetChild(childIndex).gameObject; if (sourceChild.gameObject.activeSelf && !CheckEmptyRecursive(sourceChild)) { GameObject targetChild = new GameObject(sourceChild.name); targetChild.transform.parent = targetNode.transform; targetChild.transform.localPosition = sourceChild.transform.localPosition; targetChild.transform.localRotation = sourceChild.transform.localRotation; targetChild.transform.localScale = sourceChild.transform.localScale; ProcessAnimationObjectRecursive(sourceChild, targetChild, sourceToTargetMapping, animatorName, meshPartSuffix, animationMatricesTexturePartSuffix, materialPartSuffix, refModelAnimator, refModelLoDs, animationMatricesTexturesCache, ecsGpuMaterialsCache, bakerData, animations, totalNbrOfFrames, generatedAssetsFolder, gpuEcsAnimator); } } } private static void RemoveComponent(GameObject gameObject) where T : Component { T component = gameObject.GetComponent(); if(component != null) Object.DestroyImmediate(component, allowDestroyingAssets:true); } private static bool CheckEmptyRecursive(GameObject node) { Component[] components = node.GetComponents(); bool empty = !components.Any(c => (c is SkinnedMeshRenderer)); if (empty) { for (int childIndex = 0; childIndex < node.transform.childCount; childIndex++) { empty = CheckEmptyRecursive(node.transform.GetChild(childIndex).gameObject); if (!empty) break; } } return empty; } private static Mesh BakeAndSaveBoneWeightsIntoMesh(LOD[] refModelLoDs, string generatedAssetsFolder, string animatorName, string meshPartSuffix, GpuEcsAnimationBakerData bakerData, SkinnedMeshRenderer skinnedMeshRenderer) { int maxNumberOfBonesPerVertex = GetMaxNumberOfBonesPerVertex(refModelLoDs, bakerData, skinnedMeshRenderer); Mesh newMesh = BakeBoneWeightsIntoMesh(skinnedMeshRenderer.sharedMesh, maxNumberOfBonesPerVertex); SavePartAsAsset(newMesh, generatedAssetsFolder, animatorName, skinnedMeshRenderer, meshPartSuffix, "mesh"); return newMesh; } private static Texture2D BakeAndSaveAnimationMatricesTexture(List cache, string generatedAssetsFolder, string animatorName, string animationMatricesTexturePartSuffix, Animator refModelAnimator, GpuEcsAnimationBakerData bakerData, SkinnedMeshRenderer skinnedMeshRenderer, GpuEcsAnimationData[] animations, int totalNbrOfFrames) { if (!CheckAnimationMatricesTextureInCache(cache, skinnedMeshRenderer, out Texture2D animationMatricesTexture)) { animationMatricesTexture = BakeAnimationMatricesTexture(skinnedMeshRenderer, refModelAnimator, bakerData, animations, totalNbrOfFrames); SavePartAsAsset(animationMatricesTexture, generatedAssetsFolder, animatorName, skinnedMeshRenderer, animationMatricesTexturePartSuffix, "asset"); cache.Add(new AnimationMatricesTexture() { texture = animationMatricesTexture, skinnedMeshRenderer = skinnedMeshRenderer }); } return animationMatricesTexture; } private static Material CreateAndSaveEcsGpuMaterial(List cache, string generatedAssetsFolder, string animatorName, string materialPartSuffix, SkinnedMeshRenderer skinnedMeshRenderer) { if (!CheckEcsGpuMaterialInCache(cache, skinnedMeshRenderer, out Material ecsGpuMaterial)) { ecsGpuMaterial = Object.Instantiate(skinnedMeshRenderer.sharedMaterial); ecsGpuMaterial.shader = Shader.Find("Shader Graphs/GPUECSAnimator_URP_SimpleShader"); ecsGpuMaterial.SetFloat(EnableAnimation, 0); ecsGpuMaterial.enableInstancing = true; SavePartAsAsset(ecsGpuMaterial, generatedAssetsFolder, animatorName, skinnedMeshRenderer, materialPartSuffix, "mat"); cache.Add(new EcsGpuMaterial() { material = ecsGpuMaterial, skinnedMeshRenderer = skinnedMeshRenderer }); } return ecsGpuMaterial; } private static string GenerateAssetPath(string generatedAssetsFolder, string animatorName, string assetName, string assetFileExtension) { return Path.Combine(generatedAssetsFolder, $"{animatorName}_{assetName}.{assetFileExtension}"); } private static void SavePartAsAsset(Object asset, string generatedAssetsFolder, string animatorName, SkinnedMeshRenderer skinnedMeshRenderer, string partName, string assetFileExtension) { string assetName = $"{partName}_{skinnedMeshRenderer.name}"; string assetPath = GenerateAssetPath(generatedAssetsFolder, animatorName, assetName, assetFileExtension); AssetDatabase.CreateAsset(asset, assetPath); } private static int GetMaxNumberOfBonesPerVertex(LOD[] refModelLoDs, GpuEcsAnimationBakerData bakerData, SkinnedMeshRenderer skinnedMeshRenderer) { int maxNumberOfBonesPerVertex = bakerData.boneUsage.numberOfBonesPerVertex; if (refModelLoDs != null) { for (int lodIndex = 0; lodIndex < refModelLoDs.Length; lodIndex++) { if (refModelLoDs[lodIndex].renderers.Any(r => r == (Renderer)skinnedMeshRenderer)) { BoneUsagePerLoD boneUsagePerLoD = bakerData.boneUsage.boneUsagesPerLoD .SingleOrDefault(b => b.lodIndex == lodIndex); if (boneUsagePerLoD != null) maxNumberOfBonesPerVertex = boneUsagePerLoD.maxNumberOfBonesPerVertex; break; } } } return maxNumberOfBonesPerVertex; } private static bool CheckAnimationMatricesTextureInCache(List cache, SkinnedMeshRenderer skinnedMeshRenderer, out Texture2D foundTexture) { bool found = false; foundTexture = null; foreach (AnimationMatricesTexture cachedTexture in cache) { if (CheckSkinnedMeshRendererHasSameBones(cachedTexture.skinnedMeshRenderer, skinnedMeshRenderer)) { found = true; foundTexture = cachedTexture.texture; break; } } return found; } private static bool CheckEcsGpuMaterialInCache(List cache, SkinnedMeshRenderer skinnedMeshRenderer, out Material foundMaterial) { bool found = false; foundMaterial = null; foreach (EcsGpuMaterial cachedEcsGpuMaterial in cache) { if (cachedEcsGpuMaterial.skinnedMeshRenderer.sharedMaterial == skinnedMeshRenderer.sharedMaterial && CheckSkinnedMeshRendererHasSameBones(cachedEcsGpuMaterial.skinnedMeshRenderer, skinnedMeshRenderer)) { found = true; foundMaterial = cachedEcsGpuMaterial.material; break; } } return found; } private static bool CheckSkinnedMeshRendererHasSameBones(SkinnedMeshRenderer skinnedMeshRendererA, SkinnedMeshRenderer skinnedMeshRendererB) { bool hasSameBones = false; if (skinnedMeshRendererA.bones.Length == skinnedMeshRendererB.bones.Length) { hasSameBones = true; for (int i = 0; i < skinnedMeshRendererA.bones.Length; i++) { if (!CheckMatrixEquality(skinnedMeshRendererA.bones[i].localToWorldMatrix, skinnedMeshRendererB.bones[i].localToWorldMatrix)) { hasSameBones = false; break; } if (!CheckMatrixEquality(skinnedMeshRendererA.sharedMesh.bindposes[i], skinnedMeshRendererB.sharedMesh.bindposes[i])) { hasSameBones = false; break; } } } return hasSameBones; } private static bool CheckMatrixEquality(Matrix4x4 m1, Matrix4x4 m2) { return m1.m00 == m2.m00 && m1.m01 == m2.m01 && m1.m02 == m2.m02 && m1.m03 == m2.m03 && m1.m10 == m2.m10 && m1.m11 == m2.m11 && m1.m12 == m2.m12 && m1.m13 == m2.m13 && m1.m20 == m2.m20 && m1.m21 == m2.m21 && m1.m22 == m2.m22 && m1.m23 == m2.m23 && m1.m30 == m2.m30 && m1.m31 == m2.m31 && m1.m32 == m2.m32 && m1.m33 == m2.m33; } private static void IterateOverAllFramesThroughAnimator( Animator refModelAnimator, GpuEcsAnimationBakerData bakerData, GpuEcsAnimationData[] animations, System.Action actionPerFrame) { refModelAnimator.speed = 0; int currentFrameIndex = 0; for (int animationIndex = 0; animationIndex < bakerData.animations.Length; animationIndex++) { AnimationData animationData = bakerData.animations[animationIndex]; GpuEcsAnimationData gpuEcsAnimationData = animations[animationIndex]; foreach (AnimatorParameter parameterValue in animationData.additionalAnimatorParameterValues) { if(parameterValue.parameterType == AnimatorParameterTypes.Bool) refModelAnimator.SetBool(parameterValue.parameterName, parameterValue.boolValue); else if(parameterValue.parameterType == AnimatorParameterTypes.Float) refModelAnimator.SetFloat(parameterValue.parameterName, parameterValue.floatValue); else if(parameterValue.parameterType == AnimatorParameterTypes.Integer) refModelAnimator.SetInteger(parameterValue.parameterName, parameterValue.intValue); } for (int sampleIndex = 0; sampleIndex < gpuEcsAnimationData.nbrOfInBetweenSamples; sampleIndex++) { if (animationData.animationType == AnimationTypes.DualClipBlend) { DualClipBlendData dualClipBlendData = animationData.dualClipBlendData; float sampleRatio = (float)sampleIndex / (float)(gpuEcsAnimationData.nbrOfInBetweenSamples - 1); float blendValue = dualClipBlendData.clip1.parameterValue + (dualClipBlendData.clip2.parameterValue - dualClipBlendData.clip1.parameterValue) * sampleRatio; refModelAnimator.SetFloat(dualClipBlendData.blendParameterName, blendValue); } for (int frameIndex = 0; frameIndex < gpuEcsAnimationData.nbrOfFramesPerSample; frameIndex++) { float progressRatio = (float)frameIndex / (float)(gpuEcsAnimationData.nbrOfFramesPerSample - 1); refModelAnimator.Play(animationData.animatorStateName, -1, progressRatio); foreach(AnimatorState animatorState in animationData.additionalAnimatorStatesPerLayer) refModelAnimator.Play(animatorState.stateName, animatorState.layer, progressRatio); refModelAnimator.Update(0); actionPerFrame(currentFrameIndex); currentFrameIndex++; } } } } private static GpuEcsAttachmentAnchorData BakeAttachmentAnchorTransforms(string generatedAssetsFolder, GameObject sourceModel, string animatorName, Animator refModelAnimator, GpuEcsAnimationBakerData bakerData, GpuEcsAnimationData[] animations, int totalNbrOfFrames,GpuEcsAnimatorBehaviour gpuEcsAnimator) { int nbrOfAttachments = bakerData.attachmentAnchors.Length; if (nbrOfAttachments == 0) return null; else { GpuEcsAttachmentAnchorData gpuEcsAttachmentAnchorData = ScriptableObject.CreateInstance(); gpuEcsAttachmentAnchorData.anchorTransforms = new float4x4[totalNbrOfFrames * nbrOfAttachments]; gpuEcsAnimator.anchorNames.Clear(); for (int attachmentAnchorIndex = 0; attachmentAnchorIndex < nbrOfAttachments; attachmentAnchorIndex++) { AttachmentAnchor attachmentAnchor = bakerData.attachmentAnchors[attachmentAnchorIndex]; gpuEcsAnimator.anchorNames.Add(attachmentAnchor.attachmentAnchorID); Transform referenceAnchorTransform = attachmentAnchor.attachmentAnchorTransform; Stack siblingIndexStack = new Stack(); GetSiblingIndexStack(referenceAnchorTransform, sourceModel.transform, siblingIndexStack); Transform anchorTransform = refModelAnimator.transform; while (siblingIndexStack.Count > 0) anchorTransform = anchorTransform.GetChild(siblingIndexStack.Pop()); Debug.Assert(anchorTransform.name.Equals(referenceAnchorTransform.name)); int baseIndex = attachmentAnchorIndex * totalNbrOfFrames; IterateOverAllFramesThroughAnimator(refModelAnimator, bakerData, animations, (currentFrameIndex) => { gpuEcsAttachmentAnchorData.anchorTransforms[baseIndex + currentFrameIndex] = anchorTransform.localToWorldMatrix; }); } string assetPath = GenerateAssetPath(generatedAssetsFolder, animatorName, "AttachmentAnchors", "asset"); AssetDatabase.CreateAsset(gpuEcsAttachmentAnchorData, assetPath); return gpuEcsAttachmentAnchorData; } } private static void GetSiblingIndexStack(Transform current, Transform root, Stack currentStack) { if(current.gameObject == root.gameObject) return; currentStack.Push(current.GetSiblingIndex()); GetSiblingIndexStack(current.parent, root, currentStack); } private static Texture2D BakeAnimationMatricesTexture(SkinnedMeshRenderer skinnedMeshRenderer, Animator refModelAnimator, GpuEcsAnimationBakerData bakerData, GpuEcsAnimationData[] animations, int totalNbrOfFrames) { int boneCount = skinnedMeshRenderer.bones.Length; int animationMatricesTextureWidth = boneCount * 3; NativeArray animatedBoneMatricesTextureData = new NativeArray( totalNbrOfFrames * animationMatricesTextureWidth, Allocator.Temp); float4x4 invSkinnedMeshRendererLocalToWorld = math.inverse(skinnedMeshRenderer.transform.localToWorldMatrix); IterateOverAllFramesThroughAnimator(refModelAnimator, bakerData, animations, (currentFrameIndex) => { for (int boneIndex = 0; boneIndex < boneCount; boneIndex++) { Transform boneTransform = skinnedMeshRenderer.bones[boneIndex]; if (boneTransform != null) { float4x4 matrix = math.mul( math.mul(invSkinnedMeshRendererLocalToWorld, boneTransform.localToWorldMatrix), skinnedMeshRenderer.sharedMesh.bindposes[boneIndex] ); Debug.Assert(Mathf.Approximately(matrix[0][3], 0f) && Mathf.Approximately(matrix[1][3], 0f) && Mathf.Approximately(matrix[2][3], 0f) && Mathf.Approximately(matrix[3][3], 1f), "matrix row 4 must be 0,0,0,1"); int matrixIndex = (currentFrameIndex * animationMatricesTextureWidth) + (boneIndex * 3); for (int i = 0; i < 3; i++) animatedBoneMatricesTextureData[matrixIndex + i] = new half4( (half)matrix[0][i], (half)matrix[1][i], (half)matrix[2][i], (half)matrix[3][i]); } } }); Texture2D animationMatricesTexture = new Texture2D(animationMatricesTextureWidth, totalNbrOfFrames, TextureFormat.RGBAHalf, false); animationMatricesTexture.SetPixelData(animatedBoneMatricesTextureData, 0); animationMatricesTexture.Apply(); animatedBoneMatricesTextureData.Dispose(); return animationMatricesTexture; } private static Mesh BakeBoneWeightsIntoMesh(Mesh sourceMesh, int maxNumberOfBonesPerVertex) { Mesh mesh = GameObject.Instantiate(sourceMesh); int vertexCount = mesh.vertices.Length; NativeArray uvs1 = new NativeArray(vertexCount, Allocator.Temp); NativeArray uvs2 = new NativeArray(vertexCount, Allocator.Temp); NativeArray uvs3 = new NativeArray(vertexCount, Allocator.Temp); NativeArray bonesPerVertex = mesh.GetBonesPerVertex(); NativeArray boneWeightsSource = mesh.GetAllBoneWeights(); int startBoneWeightIndex = 0; for (int vertIndex = 0; vertIndex < vertexCount; vertIndex++) { float totalWeight = 0f; float totalWeightCapped = 0f; int numberOfBonesForThisVertex = bonesPerVertex[vertIndex]; int boneWeightIndexTemp = startBoneWeightIndex; for (int i = 0; i < numberOfBonesForThisVertex; i++) { BoneWeight1 currentBoneWeight = boneWeightsSource[boneWeightIndexTemp]; totalWeight += currentBoneWeight.weight; if (i < maxNumberOfBonesPerVertex) totalWeightCapped += currentBoneWeight.weight; if (i > 0) Debug.Assert(boneWeightsSource[boneWeightIndexTemp - 1].weight >= currentBoneWeight.weight); boneWeightIndexTemp++; } Debug.Assert(Mathf.Approximately(1f, totalWeight)); float weightMultiplier = totalWeight / totalWeightCapped; int nbrOfBonesToBake = math.min(maxNumberOfBonesPerVertex, numberOfBonesForThisVertex); totalWeight = 0f; boneWeightIndexTemp = startBoneWeightIndex; float4 uv1 = float4.zero; float4 uv2 = float4.zero; float4 uv3 = float4.zero; for (int i = 0; i < nbrOfBonesToBake; i++) { BoneWeight1 currentBoneWeight = boneWeightsSource[boneWeightIndexTemp]; float adjustedWeight = currentBoneWeight.weight * weightMultiplier; totalWeight += adjustedWeight; boneWeightIndexTemp++; if (i == 0) uv1 = new float4(currentBoneWeight.boneIndex, adjustedWeight, uv1.z, uv1.w); else if (i == 1) uv1 = new float4(uv1.x, uv1. y, currentBoneWeight.boneIndex, adjustedWeight); else if (i == 2) uv2 = new float4(currentBoneWeight.boneIndex, adjustedWeight, uv2.z, uv2.w); else if (i == 3) uv2 = new float4(uv2.x, uv2. y, currentBoneWeight.boneIndex, adjustedWeight); else if (i == 4) uv3 = new float4(currentBoneWeight.boneIndex, adjustedWeight, uv3.z, uv3.w); else if (i == 5) uv3 = new float4(uv3.x, uv3. y, currentBoneWeight.boneIndex, adjustedWeight); } Debug.Assert(Mathf.Approximately(1f, totalWeight)); uvs1[vertIndex] = uv1; uvs2[vertIndex] = uv2; uvs3[vertIndex] = uv3; startBoneWeightIndex += numberOfBonesForThisVertex; } mesh.SetUVs(1, uvs1); mesh.SetUVs(2, uvs2); mesh.SetUVs(3, uvs3); uvs1.Dispose(); uvs2.Dispose(); uvs3.Dispose(); bonesPerVertex.Dispose(); boneWeightsSource.Dispose(); return mesh; } } } #endif