| | 1 | | using System.Collections.Generic; |
| | 2 | | using System.Data.Common; |
| | 3 | | using System.Linq; |
| | 4 | | using Unity.Collections; |
| | 5 | | using UnityEditor; |
| | 6 | | using UnityEngine; |
| | 7 | | using UnityEngine.Rendering; |
| | 8 | |
|
| | 9 | | namespace DCL |
| | 10 | | { |
| | 11 | | /// <summary> |
| | 12 | | /// This class is used by the AvatarMeshCombiner to combine meshes. Each layer represents a new generated sub-mesh. |
| | 13 | | /// </summary> |
| | 14 | | public class CombineLayer |
| | 15 | | { |
| | 16 | | public List<SkinnedMeshRenderer> renderers = new List<SkinnedMeshRenderer>(); |
| | 17 | | public Dictionary<Texture2D, int> textureToId = new Dictionary<Texture2D, int>(); |
| | 18 | | public CullMode cullMode; |
| | 19 | | public bool isOpaque; |
| | 20 | |
|
| | 21 | | public override string ToString() |
| | 22 | | { |
| | 23 | | string rendererString = $"renderer count: {renderers?.Count ?? 0}"; |
| | 24 | | string textureIdString = "texture ids: {"; |
| | 25 | |
|
| | 26 | | foreach ( var kvp in textureToId ) |
| | 27 | | { |
| | 28 | | textureIdString += $" tx hash: {kvp.Key.GetHashCode()} id: {kvp.Value} "; |
| | 29 | | } |
| | 30 | |
|
| | 31 | | textureIdString += "}"; |
| | 32 | |
|
| | 33 | | return $"cullMode: {cullMode} - isOpaque: {isOpaque} - {rendererString} - {textureIdString}"; |
| | 34 | | } |
| | 35 | | } |
| | 36 | |
|
| | 37 | | /// <summary> |
| | 38 | | /// This class is used to determine the original materials uniform properties |
| | 39 | | /// will be passed on to UV values. texturePointers and emissionColors are bound to UV channels. |
| | 40 | | /// Colors are bound to the color channel. |
| | 41 | | /// </summary> |
| | 42 | | public class FlattenedMaterialsData |
| | 43 | | { |
| 0 | 44 | | public List<Material> materials = new List<Material>(); |
| | 45 | | public Vector3[] texturePointers; |
| | 46 | | public Vector4[] colors; |
| | 47 | | public Vector4[] emissionColors; |
| | 48 | | } |
| | 49 | |
|
| | 50 | | /// <summary> |
| | 51 | | /// This utility class can combine avatar meshes into a single optimal mesh. |
| | 52 | | /// </summary> |
| | 53 | | public static class AvatarMeshCombiner |
| | 54 | | { |
| | 55 | | public struct Output |
| | 56 | | { |
| | 57 | | public bool isValid; |
| | 58 | | public Mesh mesh; |
| | 59 | | public Material[] materials; |
| | 60 | | } |
| | 61 | |
|
| | 62 | | // These must match the channels defined in the material shader. |
| | 63 | | // Channels must be defined with the TEXCOORD semantic. |
| | 64 | | private const int TEXTURE_POINTERS_UV_CHANNEL_INDEX = 2; |
| | 65 | | private const int EMISSION_COLORS_UV_CHANNEL_INDEX = 3; |
| | 66 | |
|
| | 67 | | /// <summary> |
| | 68 | | /// CombineSkinnedMeshes combines a list of skinned mesh renderers that share the same bone structure into a |
| | 69 | | /// single mesh with the fewest numbers of sub-meshes as possible. It will return a list of materials that match |
| | 70 | | /// the number of sub-meshes to be used with a Renderer component. |
| | 71 | | /// <br/> |
| | 72 | | /// The sub-meshes are divided according to the following constraints: |
| | 73 | | /// <ul> |
| | 74 | | /// <li>Culling mode</li> |
| | 75 | | /// <li>Transparency or opacity of each renderer to be combined</li> |
| | 76 | | /// <li>Texture count of the accumulated renderers, only including emission and albedo textures.</li> |
| | 77 | | /// </ul> |
| | 78 | | /// </summary> |
| | 79 | | /// <param name="bindPoses">Bindposes that will be used by all the renderers.</param> |
| | 80 | | /// <param name="bones">Bones that will be used by all the renderers.</param> |
| | 81 | | /// <param name="renderers">Renderers to be combined.</param> |
| | 82 | | /// <param name="materialAsset">Material asset to be used in the resulting Output object. This Material will be |
| | 83 | | /// <returns>An Output object with the mesh and materials. Output.isValid will return true if the combining is s |
| | 84 | | public static Output CombineSkinnedMeshes(Matrix4x4[] bindPoses, Transform[] bones, SkinnedMeshRenderer[] render |
| | 85 | | { |
| | 86 | | Output result = new Output(); |
| | 87 | |
|
| | 88 | | // |
| | 89 | | // Reset bones to put character in T pose. Renderers are going to be baked later. |
| | 90 | | // This is a workaround, it had to be done because renderers original matrices don't match the T pose. |
| | 91 | | // We need wearables in T pose to properly combine the avatar mesh. |
| | 92 | | // |
| | 93 | | AvatarMeshCombinerUtils.ResetBones(bindPoses, bones); |
| | 94 | |
|
| | 95 | | // |
| | 96 | | // Get combined layers. Layers are groups of renderers that have a id -> tex mapping. |
| | 97 | | // |
| | 98 | | // This id is going to get written to uv channels so the material can use up to 12 textures |
| | 99 | | // in a single draw call. |
| | 100 | | // |
| | 101 | | // Layers are divided accounting for the 12 textures limit and transparency/opaque limit. |
| | 102 | | // |
| | 103 | | List<CombineLayer> layers = CombineLayerUtils.Slice( renderers ); |
| | 104 | |
|
| | 105 | | if ( layers == null ) |
| | 106 | | { |
| | 107 | | result.isValid = false; |
| | 108 | | return result; |
| | 109 | | } |
| | 110 | |
|
| | 111 | | // Here, the final combined mesh is generated. This mesh still doesn't have the UV encoded |
| | 112 | | // samplers and some needed attributes. Those will be added below. |
| | 113 | | var combineInstancesData = AvatarMeshCombinerUtils.ComputeCombineInstancesData( layers ); |
| | 114 | | Mesh finalMesh = AvatarMeshCombinerUtils.CombineMeshesWithLayers(combineInstancesData, layers); |
| | 115 | |
|
| | 116 | | // Note about bindPoses and boneWeights reassignment: |
| | 117 | | // |
| | 118 | | // This has to be done because CombineMeshes doesn't identify different meshes |
| | 119 | | // with boneWeights that correspond to the same bones. Also, bindposes are |
| | 120 | | // stacked and repeated for each mesh when they shouldn't. |
| | 121 | | // |
| | 122 | | // This is OK when combining multiple SkinnedMeshRenderers that animate independently, |
| | 123 | | // but not in our use case. |
| | 124 | |
|
| | 125 | | finalMesh.bindposes = bindPoses; |
| | 126 | |
|
| | 127 | | var boneWeights = AvatarMeshCombinerUtils.ComputeBoneWeights( layers ); |
| | 128 | | finalMesh.boneWeights = boneWeights; |
| | 129 | |
|
| | 130 | | var flattenedMaterialsData = AvatarMeshCombinerUtils.FlattenMaterials( layers, materialAsset ); |
| | 131 | | finalMesh.SetUVs(EMISSION_COLORS_UV_CHANNEL_INDEX, flattenedMaterialsData.emissionColors); |
| | 132 | | finalMesh.SetUVs(TEXTURE_POINTERS_UV_CHANNEL_INDEX, flattenedMaterialsData.texturePointers); |
| | 133 | |
|
| | 134 | | var tempArray = new NativeArray<Vector4>(flattenedMaterialsData.colors.Length, Allocator.Temp); |
| | 135 | | tempArray.CopyFrom(flattenedMaterialsData.colors); |
| | 136 | | finalMesh.SetColors(tempArray); |
| | 137 | | tempArray.Dispose(); |
| | 138 | | // Each layer corresponds with a subMesh. This is to take advantage of the sharedMaterials array. |
| | 139 | | // |
| | 140 | | // When a renderer has many sub-meshes, each materials array element correspond to the sub-mesh of |
| | 141 | | // the same index. Each layer needs to be renderer with its own material, so it becomes very useful. |
| | 142 | | // |
| | 143 | | var subMeshDescriptors = AvatarMeshCombinerUtils.ComputeSubMeshes( layers ); |
| | 144 | | if ( subMeshDescriptors.Count > 1 ) |
| | 145 | | { |
| | 146 | | finalMesh.subMeshCount = subMeshDescriptors.Count; |
| | 147 | | finalMesh.SetSubMeshes(subMeshDescriptors); |
| | 148 | | } |
| | 149 | |
|
| | 150 | | finalMesh.Optimize(); |
| | 151 | |
|
| | 152 | | result.mesh = finalMesh; |
| | 153 | | result.materials = flattenedMaterialsData.materials.ToArray(); |
| | 154 | | result.isValid = true; |
| | 155 | |
|
| | 156 | | return result; |
| | 157 | | } |
| | 158 | | } |
| | 159 | | } |