< Summary

Class:DCL.CombineLayerUtils
Assembly:AvatarMeshCombiner
File(s):/tmp/workspace/unity-renderer/unity-renderer/Assets/Scripts/MainScripts/DCL/Components/Avatar/AvatarMeshCombiner/CombineLayerUtils.cs
Covered lines:78
Uncovered lines:21
Coverable lines:99
Total lines:337
Line coverage:78.7% (78 of 99)
Covered branches:0
Total branches:0

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity NPath complexity Sequence coverage
CombineLayerUtils()0%330100%
Slice(...)0%15.2710062.5%
SubsliceLayerByTextures(...)0%10.979071.05%
SliceByRenderState(...)0%7.017095%
GetMapIds(...)0%770100%
IsOpaque(...)0%6.176083.33%
IsOpaque(...)0%110100%
GetCullMode(...)0%4.123050%
GetCullMode(...)0%110100%
GetCullModeWithoutCullOff(...)0%220100%

File(s)

/tmp/workspace/unity-renderer/unity-renderer/Assets/Scripts/MainScripts/DCL/Components/Avatar/AvatarMeshCombiner/CombineLayerUtils.cs

#LineLine coverage
 1using System;
 2using System.Collections.Generic;
 3using System.Collections.ObjectModel;
 4using System.Linq;
 5using DCL.Helpers;
 6using DCL.Shaders;
 7using UnityEngine;
 8using UnityEngine.Rendering;
 9
 10namespace DCL
 11{
 12    public static class CombineLayerUtils
 13    {
 14        // This heuristic forces double-sided opaque objects to have backface culling.
 15        // As many wearables are incorrectly modeled as double-sided, this greatly increases
 16        // the cases of avatars rendered with one draw call. Temporarily disabled until some wearables are fixed.
 117        public static bool ENABLE_CULL_OPAQUE_HEURISTIC = false;
 18
 119        private static bool VERBOSE = false;
 20        private const int MAX_TEXTURE_ID_COUNT = 12;
 121        private static ILogger logger = new Logger(Debug.unityLogger.logHandler) { filterLogType = VERBOSE ? LogType.Log
 122        private static readonly int[] textureIds = new int[] { ShaderUtils.BaseMap, ShaderUtils.EmissionMap };
 23
 24        /// <summary>
 25        /// This method takes a skinned mesh renderer list and turns it into a series of CombineLayer elements.<br/>
 26        ///
 27        /// Each CombineLayer element represents a combining group, and the renderers are grouped using a set of criteri
 28        ///
 29        /// <ul>
 30        /// <li>Each layer must correspond to renderers that share the same cull mode and blend state.</li>
 31        /// <li>Each layer can't contain renderers that sum up over MAX_TEXTURE_ID_COUNT textures.</li>
 32        /// </ul>
 33        /// Those layers can later be used to combine meshes as efficiently as possible by encoding their map
 34        /// samplers to UV attributes. This allows the usage of a special shader that can branch samplers according to
 35        /// the UV data. By encoding the samplers this way, materials that use different textures and uniform values
 36        /// can be grouped together, and thus, meshes can be further combined.
 37        /// </summary>
 38        /// <param name="renderers">List of renderers to slice.</param>
 39        /// <returns>List of CombineLayer objects that can be used to produce a highly optimized combined mesh.</returns
 40        internal static List<CombineLayer> Slice(SkinnedMeshRenderer[] renderers)
 41        {
 2042            logger.Log("Slice Start!");
 43
 2044            var rawLayers = SliceByRenderState(renderers);
 45
 2046            logger.Log($"Preparing slice. Found {rawLayers.Count} groups.");
 47
 48            //
 49            // Now, we sub-slice the rawLayers.
 50            // A single rawLayer will be sub-sliced if the textures exceed the sampler limit (12 in this case).
 51            // Also, in this step the textureToId map is populated.
 52            //
 2053            List<CombineLayer> result = new List<CombineLayer>();
 54
 11055            for (int i = 0; i < rawLayers.Count; i++)
 56            {
 3557                var rawLayer = rawLayers[i];
 3558                logger.Log($"Processing group {i}. Renderer count: {rawLayer.renderers.Count}. cullMode: {rawLayer.cullM
 3559                result.AddRange(SubsliceLayerByTextures(rawLayer));
 60            }
 61
 62            // No valid materials were found
 2063            if (result.Count == 1 && result[0].textureToId.Count == 0 && result[0].renderers.Count == 0)
 64            {
 065                logger.Log("Slice End Fail!");
 066                return null;
 67            }
 68
 5569            result = result.Where(x => x.renderers != null && x.renderers.Count > 0).ToList();
 70
 2071            if (VERBOSE)
 72            {
 073                int layInd = 0;
 74
 075                foreach (var layer in result)
 76                {
 077                    string rendererNames = layer.renderers
 078                                                .Select((x) => $"{x.transform.parent.name}")
 079                                                .Aggregate((i, j) => i + "\n" + j);
 80
 081                    logger.Log($"Layer index: {layInd} ... renderer count: {layer.renderers.Count} ... textures found: {
 082                    layInd++;
 83                }
 84            }
 85
 2086            logger.Log("Slice End Success!");
 2087            return result;
 88        }
 89
 90        /// <summary>
 91        /// <p>
 92        /// This method takes a single CombineLayer and sub-slices it according to the texture count of the
 93        /// contained renderers of the given layer.
 94        /// </p>
 95        /// <p>
 96        /// The resulting layers will have their <i>textureToId</i> field populated with the found textures.
 97        /// The <i>textureToId</i> int value is what will have to be passed over the uv attributes of the combined meshe
 98        /// </p>
 99        /// </summary>
 100        /// <param name="layer">The CombineLayer layer to subdivide and populate by the ids.</param>
 101        /// <returns>A list that at least is guaranteed to contain the given layer.
 102        /// If the given layer exceeds the max texture count, more than a layer can be returned.
 103        /// </returns>
 104        internal static List<CombineLayer> SubsliceLayerByTextures(CombineLayer layer)
 105        {
 37106            var result = new List<CombineLayer>();
 37107            int textureId = 0;
 108
 37109            bool shouldAddLayerToResult = true;
 37110            CombineLayer currentResultLayer = null;
 111
 300112            for (int rendererIndex = 0; rendererIndex < layer.renderers.Count; rendererIndex++)
 113            {
 113114                var r = layer.renderers[rendererIndex];
 115
 113116                if (shouldAddLayerToResult)
 117                {
 38118                    shouldAddLayerToResult = false;
 38119                    textureId = 0;
 120
 38121                    currentResultLayer = new CombineLayer
 122                    {
 123                        cullMode = layer.cullMode,
 124                        isOpaque = layer.isOpaque
 125                    };
 126
 38127                    result.Add(currentResultLayer);
 128                }
 129
 113130                var mats = r.sharedMaterials;
 131
 113132                var mapIdsToInsert = GetMapIds(
 133                    new ReadOnlyDictionary<Texture2D, int>(currentResultLayer.textureToId),
 134                    mats,
 135                    textureId);
 136
 137                // The renderer is too big to fit in a single layer? (This should never happen).
 113138                if (mapIdsToInsert.Count > MAX_TEXTURE_ID_COUNT)
 139                {
 0140                    logger.Log(LogType.Warning, "The renderer is too big to fit in a single layer? (This should never ha
 0141                    shouldAddLayerToResult = true;
 0142                    continue;
 143                }
 144
 145                // The renderer can fit in a single layer.
 146                // But can't fit in this one, as previous renderers filled this layer out.
 113147                if (textureId + mapIdsToInsert.Count > MAX_TEXTURE_ID_COUNT)
 148                {
 0149                    rendererIndex--;
 0150                    shouldAddLayerToResult = true;
 0151                    continue;
 152                }
 153
 154                // put GetMapIds result into currentLayer id map.
 523155                foreach (var kvp in mapIdsToInsert) { currentResultLayer.textureToId[kvp.Key] = kvp.Value; }
 156
 113157                currentResultLayer.renderers.Add(r);
 158
 113159                textureId += mapIdsToInsert.Count;
 160
 114161                if (textureId >= MAX_TEXTURE_ID_COUNT) { shouldAddLayerToResult = true; }
 162            }
 163
 37164            if (VERBOSE)
 165            {
 0166                for (int i = 0; i < result.Count; i++)
 167                {
 0168                    var c = result[i];
 0169                    Debug.Log($"layer {i} - {c}");
 170                }
 171            }
 172
 37173            return result;
 174        }
 175
 176        /// <summary>
 177        /// <p>
 178        /// This method takes a skinned mesh renderer list and turns it into a series of CombineLayer elements.
 179        /// Each CombineLayer element represents a combining group, and the renderers are grouped using a set of criteri
 180        /// </p>
 181        /// <p>
 182        /// For SliceByRenderState, the returned CombineLayer list will be grouped according to shared cull mode and
 183        /// blend state.
 184        /// </p>
 185        /// </summary>
 186        /// <param name="renderers">List of renderers to slice.</param>
 187        /// <returns>List of CombineLayer objects that can be used to produce a highly optimized combined mesh.</returns
 188        internal static List<CombineLayer> SliceByRenderState(SkinnedMeshRenderer[] renderers)
 189        {
 23190            List<CombineLayer> result = new List<CombineLayer>();
 191
 192            // Group renderers on opaque and transparent materials
 23193            var rendererByOpaqueMode = renderers.GroupBy(IsOpaque);
 194
 195            // Then, make subgroups to divide them between culling modes
 122196            foreach (var byOpaqueMode in rendererByOpaqueMode)
 197            {
 198                // For opaque renderers, we replace the CullOff value by CullBack to reduce group count,
 199                // This workarounds many opaque wearables that use Culling Off by mistake.
 38200                Func<SkinnedMeshRenderer, CullMode> getCullModeFunc = null;
 201
 38202                if (ENABLE_CULL_OPAQUE_HEURISTIC) { getCullModeFunc = byOpaqueMode.Key ? new Func<SkinnedMeshRenderer, C
 38203                else { getCullModeFunc = GetCullMode; }
 204
 38205                var rendererByCullingMode = byOpaqueMode.GroupBy(getCullModeFunc);
 206
 162207                foreach (var byCulling in rendererByCullingMode)
 208                {
 43209                    var byCullingRenderers = byCulling.ToList();
 210
 43211                    CombineLayer layer = new CombineLayer();
 43212                    result.Add(layer);
 43213                    layer.cullMode = byCulling.Key;
 43214                    layer.isOpaque = byOpaqueMode.Key;
 43215                    layer.renderers = byCullingRenderers;
 216                }
 217            }
 218
 219            /*
 220            * The grouping outcome ends up like this:
 221            *
 222            *                 Opaque           Transparent
 223            *             /     |     \        /    |    \
 224            *          Back - Front - Off - Back - Front - Off -> rendererGroups
 225            */
 226
 23227            return result;
 228        }
 229
 230        /// <summary>
 231        ///
 232        /// </summary>
 233        /// <param name="refDict"></param>
 234        /// <param name="mats"></param>
 235        /// <param name="startingId"></param>
 236        /// <returns></returns>
 237        internal static Dictionary<Texture2D, int> GetMapIds(ReadOnlyDictionary<Texture2D, int> refDict, in Material[] m
 238        {
 115239            var result = new Dictionary<Texture2D, int>();
 240
 468241            for (int i = 0; i < mats.Length; i++)
 242            {
 119243                var mat = mats[i];
 244
 119245                if (mat == null)
 246                    continue;
 247
 714248                for (int texIdIndex = 0; texIdIndex < textureIds.Length; texIdIndex++)
 249                {
 238250                    var texture = (Texture2D)mat.GetTexture(textureIds[texIdIndex]);
 251
 238252                    if (texture == null)
 253                        continue;
 254
 145255                    if (refDict.ContainsKey(texture) || result.ContainsKey(texture))
 256                        continue;
 257
 109258                    result.Add(texture, startingId);
 109259                    startingId++;
 260                }
 261            }
 262
 115263            return result;
 264        }
 265
 266        /// <summary>
 267        /// Determines if the given renderer is going to be enqueued at the opaque section of the rendering pipeline.
 268        /// </summary>
 269        /// <param name="material">Material to be checked</param>
 270        /// <returns>True if its opaque</returns>
 271        internal static bool IsOpaque(Material material)
 272        {
 136273            if (material == null)
 0274                return true;
 275
 136276            bool hasZWrite = material.HasProperty(ShaderUtils.ZWrite);
 277
 278            // NOTE(Kinerius): Since GLTFast materials doesn't have ZWrite property, we check if the shader name is opaq
 136279            bool hasOpaqueName = material.shader.name.ToLower().Contains("opaque");
 280
 136281            bool isTransparent = (!hasZWrite && !hasOpaqueName) || (hasZWrite && (int)material.GetFloat(ShaderUtils.ZWri
 282
 136283            return !isTransparent;
 284        }
 285
 286        /// <summary>
 287        /// Determines if the given renderer is going to be enqueued at the opaque section of the rendering pipeline.
 288        /// </summary>
 289        /// <param name="renderer">Renderer to be checked.</param>
 290        /// <returns>True if its opaque</returns>
 130291        internal static bool IsOpaque(Renderer renderer) => IsOpaque(renderer.sharedMaterials[0]);
 292
 293        /// <summary>
 294        ///
 295        /// </summary>
 296        /// <param name="material"></param>
 297        /// <returns></returns>
 298        internal static CullMode GetCullMode(Material material)
 299        {
 136300            if (material.HasProperty(ShaderUtils.Cull))
 301            {
 136302                CullMode result = (CullMode)material.GetInt(ShaderUtils.Cull);
 136303                return result;
 304            }
 305
 306            // GLTFast materials dont have culling, instead they have the "Double Sided" check toggled on "double" suffi
 0307            if (material.shader.name.Contains("double")) { return CullMode.Off; }
 308
 0309            return CullMode.Back;
 310        }
 311
 312        /// <summary>
 313        ///
 314        /// </summary>
 315        /// <param name="renderer"></param>
 316        /// <returns></returns>
 317        internal static CullMode GetCullMode(Renderer renderer)
 318        {
 130319            return GetCullMode(renderer.sharedMaterials[0]);
 320        }
 321
 322        /// <summary>
 323        ///
 324        /// </summary>
 325        /// <param name="renderer"></param>
 326        /// <returns></returns>
 327        internal static CullMode GetCullModeWithoutCullOff(Renderer renderer)
 328        {
 3329            CullMode result = GetCullMode(renderer.sharedMaterials[0]);
 330
 3331            if (result == CullMode.Off)
 1332                result = CullMode.Back;
 333
 3334            return result;
 335        }
 336    }
 337}