< Summary

Class:DCL.AvatarRenderer
Assembly:AvatarShape
File(s):/tmp/workspace/unity-renderer/unity-renderer/Assets/Scripts/MainScripts/DCL/Components/Avatar/AvatarRenderer.cs
Covered lines:97
Uncovered lines:267
Coverable lines:364
Total lines:763
Line coverage:26.6% (97 of 364)
Covered branches:0
Total branches:0

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity NPath complexity Sequence coverage
AvatarRenderer()0%110100%
AvatarRenderer()0%110100%
Awake()0%220100%
ApplyModel(...)0%12.699064.29%
InitializeImpostor()0%30500%
StopLoadingCoroutines()0%220100%
CleanupAvatar()0%99096.77%
CleanUpUnusedItems()0%30500%
LoadAvatar()0%3052.965904.9%
PrepareGpuSkinning()0%2100%
SetupHairAndSkinColors()0%6200%
OnWearableLoadingSuccess(...)0%12300%
OnBodyShapeLoadingFail(...)0%42600%
OnWearableLoadingFail(...)0%56700%
SetWearableBones()0%6200%
UpdateExpression()0%20400%
SetExpression(...)0%110100%
AddWearableController(...)0%56700%
CopyFrom(...)0%2100%
SetGOVisibility(...)0%220100%
SetRendererEnabled(...)0%6200%
SetImpostorVisibility(...)0%3.143075%
SetImpostorForward(...)0%2100%
SetImpostorColor(...)0%2100%
SetThrottling(...)0%6200%
SetAvatarFade(...)0%30500%
SetImpostorFade(...)0%6200%
HideAll()0%6200%
SetFacialFeaturesVisible(...)0%20400%
SetSSAOEnabled(...)0%20400%
MergeAvatar(...)0%12300%
CleanMergedAvatar()0%110100%
ResetImpostor()0%3.033085.71%
LateUpdate()0%3.333066.67%
OnDestroy()0%110100%

File(s)

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

#LineLine coverage
 1using System;
 2using System.Collections;
 3using System.Collections.Generic;
 4using System.Linq;
 5using DCL.Helpers;
 6using GPUSkinning;
 7using UnityEngine;
 8using static WearableLiterals;
 9
 10namespace DCL
 11{
 12    public class AvatarRenderer : MonoBehaviour, IAvatarRenderer
 13    {
 114        private static readonly int BASE_COLOR_PROPERTY = Shader.PropertyToID("_BaseColor");
 15
 16        private const int MAX_RETRIES = 5;
 17
 18        public Material defaultMaterial;
 19        public Material eyeMaterial;
 20        public Material eyebrowMaterial;
 21        public Material mouthMaterial;
 22        public MeshRenderer impostorRenderer;
 23        public MeshFilter impostorMeshFilter;
 24
 25        private AvatarModel model;
 26        private AvatarMeshCombinerHelper avatarMeshCombiner;
 27        private SimpleGPUSkinning gpuSkinning = null;
 28        private GPUSkinningThrottler gpuSkinningThrottler = null;
 107729        private int gpuSkinningFramesBetweenUpdates = 1;
 30        private bool initializedImpostor = false;
 31
 32        private Renderer mainMeshRenderer
 33        {
 34            get
 35            {
 036                if (gpuSkinning != null)
 037                    return gpuSkinning.renderer;
 038                return avatarMeshCombiner.renderer;
 39            }
 40        }
 41
 42        public event Action<IAvatarRenderer.VisualCue> OnVisualCue;
 43        public event Action OnSuccessEvent;
 44        public event Action<float> OnImpostorAlphaValueUpdate;
 45        public event Action<float> OnAvatarAlphaValueUpdate;
 46        public event Action<bool> OnFailEvent;
 47
 48        internal BodyShapeController bodyShapeController;
 107749        internal Dictionary<WearableItem, WearableController> wearableControllers = new Dictionary<WearableItem, Wearabl
 50        internal FacialFeatureController eyesController;
 51        internal FacialFeatureController eyebrowsController;
 52        internal FacialFeatureController mouthController;
 53        internal AvatarAnimatorLegacy animator;
 54        internal StickersController stickersController;
 55
 107756        private long lastStickerTimestamp = -1;
 57
 58        public bool isLoading;
 059        public bool isReady => bodyShapeController != null && bodyShapeController.isReady && wearableControllers != null
 060        public float maxY { get; private set; } = 0;
 61
 62        private Coroutine loadCoroutine;
 63        private AssetPromise_Texture bodySnapshotTexturePromise;
 107764        private List<string> wearablesInUse = new List<string>();
 65        private bool isDestroyed = false;
 66
 67        private void Awake()
 68        {
 85069            animator = GetComponent<AvatarAnimatorLegacy>();
 85070            stickersController = GetComponent<StickersController>();
 85071            avatarMeshCombiner = new AvatarMeshCombinerHelper();
 85072            avatarMeshCombiner.prepareMeshForGpuSkinning = true;
 85073            avatarMeshCombiner.uploadMeshToGpu = true;
 74
 85075            if (impostorRenderer != null)
 66176                SetImpostorVisibility(false);
 85077        }
 78
 79        public void ApplyModel(AvatarModel model, Action onSuccess, Action onFail)
 80        {
 14081            if ( this.model != null )
 82            {
 9083                if (model != null && this.model.Equals(model))
 84                {
 5985                    onSuccess?.Invoke();
 4386                    return;
 87                }
 88
 3189                bool wearablesChanged = !this.model.HaveSameWearablesAndColors(model);
 3190                bool expressionsChanged = !this.model.HaveSameExpressions(model);
 91
 3192                if (!wearablesChanged && expressionsChanged)
 93                {
 094                    this.model.expressionTriggerId = model.expressionTriggerId;
 095                    this.model.expressionTriggerTimestamp = model.expressionTriggerTimestamp;
 096                    this.model.stickerTriggerId = model.stickerTriggerId;
 097                    this.model.stickerTriggerTimestamp = model.stickerTriggerTimestamp;
 098                    UpdateExpression();
 099                    onSuccess?.Invoke();
 0100                    return;
 101                }
 102            }
 103
 81104            this.model = new AvatarModel();
 81105            this.model.CopyFrom(model);
 106
 81107            ResetImpostor();
 108
 109            // TODO(Brian): Find a better approach than overloading callbacks like this. This code is not readable.
 110            void onSuccessWrapper()
 111            {
 0112                onSuccess?.Invoke();
 0113                this.OnSuccessEvent -= onSuccessWrapper;
 0114            }
 115
 81116            this.OnSuccessEvent += onSuccessWrapper;
 117
 118            void onFailWrapper(bool isFatalError)
 119            {
 6120                onFail?.Invoke();
 6121                this.OnFailEvent -= onFailWrapper;
 6122            }
 123
 81124            this.OnFailEvent += onFailWrapper;
 125
 81126            isLoading = false;
 127
 81128            if (model == null)
 129            {
 0130                CleanupAvatar();
 0131                this.OnSuccessEvent?.Invoke();
 0132                return;
 133            }
 134
 81135            StopLoadingCoroutines();
 136
 81137            isLoading = true;
 81138            loadCoroutine = CoroutineStarter.Start(LoadAvatar());
 81139        }
 140
 141        public void InitializeImpostor()
 142        {
 0143            initializedImpostor = true;
 144
 145            // The fetched snapshot can take its time so it's better to assign a generic impostor first.
 0146            AvatarRendererHelpers.RandomizeAndApplyGenericImpostor(impostorMeshFilter.mesh, impostorRenderer.material);
 147
 0148            UserProfile userProfile = null;
 0149            if (!string.IsNullOrEmpty(model?.id))
 0150                userProfile = UserProfileController.GetProfileByUserId(model.id);
 151
 0152            if (userProfile != null)
 153            {
 0154                bodySnapshotTexturePromise = new AssetPromise_Texture(userProfile.bodySnapshotURL);
 0155                bodySnapshotTexturePromise.OnSuccessEvent += asset => AvatarRendererHelpers.SetImpostorTexture(asset.tex
 0156                AssetPromiseKeeper_Texture.i.Keep(bodySnapshotTexturePromise);
 157            }
 0158        }
 159
 160        void StopLoadingCoroutines()
 161        {
 1595162            if (loadCoroutine != null)
 80163                CoroutineStarter.Stop(loadCoroutine);
 164
 1595165            loadCoroutine = null;
 1595166        }
 167
 168        public void CleanupAvatar()
 169        {
 1514170            StopLoadingCoroutines();
 1514171            if (!isDestroyed)
 172            {
 665173                SetGOVisibility(true);
 665174                if (impostorRenderer != null)
 665175                    SetImpostorVisibility(false);
 176            }
 177
 1514178            avatarMeshCombiner.Dispose();
 1514179            gpuSkinningThrottler = null;
 1514180            gpuSkinning = null;
 1514181            eyebrowsController?.CleanUp();
 1514182            eyebrowsController = null;
 183
 1514184            eyesController?.CleanUp();
 1514185            eyesController = null;
 186
 1514187            mouthController?.CleanUp();
 1514188            mouthController = null;
 189
 1514190            bodyShapeController?.CleanUp();
 1514191            bodyShapeController = null;
 192
 1514193            using (var iterator = wearableControllers.GetEnumerator())
 194            {
 1514195                while (iterator.MoveNext())
 196                {
 0197                    iterator.Current.Value.CleanUp();
 198                }
 1514199            }
 200
 1514201            wearableControllers.Clear();
 1514202            model = null;
 1514203            isLoading = false;
 1514204            OnFailEvent = null;
 1514205            OnSuccessEvent = null;
 206
 1514207            CleanMergedAvatar();
 208
 1514209            ResetImpostor();
 210
 1514211            CatalogController.RemoveWearablesInUse(wearablesInUse);
 1514212            wearablesInUse.Clear();
 1514213            OnVisualCue?.Invoke(IAvatarRenderer.VisualCue.CleanedUp);
 1470214        }
 215
 216        void CleanUpUnusedItems()
 217        {
 0218            if (model.wearables == null)
 0219                return;
 220
 0221            var ids = wearableControllers.Keys.ToArray();
 222
 0223            for (var i = 0; i < ids.Length; i++)
 224            {
 0225                var currentId = ids[i];
 0226                var wearable = wearableControllers[currentId];
 227
 0228                if (!model.wearables.Contains(wearable.id) || !wearable.IsLoadedForBodyShape(model.bodyShape))
 229                {
 0230                    wearable.CleanUp();
 0231                    wearableControllers.Remove(currentId);
 232                }
 233            }
 0234        }
 235
 236        // TODO(Brian): Pure functions should be extracted from this big LoadAvatar() method and unit-test separately.
 237        //              The current approach has tech debt that's getting very expensive and is costing many hours of de
 238        //              Avatar Loading should be a self contained operation that doesn't depend on pool management and A
 239        //              lifecycle like it does now.
 240        private IEnumerator LoadAvatar()
 241        {
 242            // TODO(Brian): This is an ugly hack, all the loading should be performed
 243            //              without being afraid of the gameObject active state.
 162244            yield return new WaitUntil(() => gameObject.activeSelf);
 245
 6246            bool loadSoftFailed = false;
 247
 6248            WearableItem resolvedBody = null;
 249
 250            // TODO(Brian): Evaluate using UniTask<T> here instead of Helpers.Promise.
 6251            Helpers.Promise<WearableItem> avatarBodyPromise = null;
 6252            if (!string.IsNullOrEmpty(model.bodyShape))
 253            {
 0254                avatarBodyPromise = CatalogController.RequestWearable(model.bodyShape);
 0255            }
 256            else
 257            {
 6258                OnFailEvent?.Invoke(true);
 6259                yield break;
 260            }
 261
 0262            List<WearableItem> resolvedWearables = new List<WearableItem>();
 263
 264            // TODO(Brian): Evaluate using UniTask<T> here instead of Helpers.Promise.
 0265            List<Helpers.Promise<WearableItem>> avatarWearablePromises = new List<Helpers.Promise<WearableItem>>();
 0266            if (model.wearables != null)
 267            {
 0268                for (int i = 0; i < model.wearables.Count; i++)
 269                {
 0270                    avatarWearablePromises.Add(CatalogController.RequestWearable(model.wearables[i]));
 271                }
 272            }
 273
 274            // In this point, all the requests related to the avatar's wearables have been collected and sent to the Cat
 275            // From here we wait for the response of the requested wearables and process them.
 0276            if (avatarBodyPromise != null)
 277            {
 0278                yield return avatarBodyPromise;
 279
 0280                if (!string.IsNullOrEmpty(avatarBodyPromise.error))
 281                {
 0282                    Debug.LogError(avatarBodyPromise.error);
 0283                    loadSoftFailed = true;
 0284                }
 285                else
 286                {
 0287                    resolvedBody = avatarBodyPromise.value;
 0288                    wearablesInUse.Add(avatarBodyPromise.value.id);
 289                }
 290            }
 291
 0292            if (resolvedBody == null)
 293            {
 0294                isLoading = false;
 0295                OnFailEvent?.Invoke(true);
 0296                yield break;
 297            }
 298
 299            // TODO(Brian): Evaluate using UniTask<T> here instead of Helpers.Promise.
 0300            List<Helpers.Promise<WearableItem>> replacementPromises = new List<Helpers.Promise<WearableItem>>();
 301
 0302            foreach (var avatarWearablePromise in avatarWearablePromises)
 303            {
 0304                yield return avatarWearablePromise;
 305
 0306                if (!string.IsNullOrEmpty(avatarWearablePromise.error))
 307                {
 0308                    Debug.LogError(avatarWearablePromise.error);
 0309                    loadSoftFailed = true;
 0310                }
 311                else
 312                {
 0313                    WearableItem wearableItem = avatarWearablePromise.value;
 0314                    wearablesInUse.Add(wearableItem.id);
 315
 0316                    if (wearableItem.GetRepresentation(model.bodyShape) != null)
 317                    {
 0318                        resolvedWearables.Add(wearableItem);
 0319                    }
 320                    else
 321                    {
 0322                        model.wearables.Remove(wearableItem.id);
 0323                        string defaultReplacement = DefaultWearables.GetDefaultWearable(model.bodyShape, wearableItem.da
 0324                        if (!string.IsNullOrEmpty(defaultReplacement))
 325                        {
 0326                            model.wearables.Add(defaultReplacement);
 0327                            replacementPromises.Add(CatalogController.RequestWearable(defaultReplacement));
 328                        }
 329                    }
 330                }
 0331            }
 332
 0333            foreach (var wearablePromise in replacementPromises)
 334            {
 0335                yield return wearablePromise;
 336
 0337                if (!string.IsNullOrEmpty(wearablePromise.error))
 338                {
 0339                    Debug.LogError(wearablePromise.error);
 0340                    loadSoftFailed = true;
 0341                }
 342                else
 343                {
 0344                    WearableItem wearableItem = wearablePromise.value;
 0345                    wearablesInUse.Add(wearableItem.id);
 0346                    resolvedWearables.Add(wearableItem);
 347                }
 0348            }
 349
 0350            bool bodyIsDirty = false;
 0351            if (bodyShapeController != null && bodyShapeController.id != model?.bodyShape)
 352            {
 0353                bodyShapeController.CleanUp();
 0354                bodyShapeController = null;
 0355                bodyIsDirty = true;
 356            }
 357
 0358            if (bodyShapeController == null)
 359            {
 0360                HideAll();
 361
 0362                bodyShapeController = new BodyShapeController(resolvedBody);
 0363                eyesController = FacialFeatureController.CreateDefaultFacialFeature(bodyShapeController.bodyShapeId, Cat
 0364                eyebrowsController = FacialFeatureController.CreateDefaultFacialFeature(bodyShapeController.bodyShapeId,
 0365                mouthController = FacialFeatureController.CreateDefaultFacialFeature(bodyShapeController.bodyShapeId, Ca
 366            }
 367
 368            //TODO(Brian): This logic should be performed in a testeable pure function instead of this inline approach.
 369            //             Moreover, this function should work against data, not wearableController instances.
 0370            bool wearablesIsDirty = false;
 0371            HashSet<string> unusedCategories = new HashSet<string>(Categories.ALL);
 0372            int wearableCount = resolvedWearables.Count;
 0373            for (int index = 0; index < wearableCount; index++)
 374            {
 0375                WearableItem wearable = resolvedWearables[index];
 0376                if (wearable == null)
 377                    continue;
 378
 0379                unusedCategories.Remove(wearable.data.category);
 0380                if (wearableControllers.ContainsKey(wearable))
 381                {
 0382                    if (!wearableControllers[wearable].IsLoadedForBodyShape(bodyShapeController.bodyShapeId))
 0383                        wearableControllers[wearable].CleanUp();
 0384                }
 385                else
 386                {
 0387                    AddWearableController(wearable);
 0388                    if (wearable.data.category != Categories.EYES && wearable.data.category != Categories.MOUTH && weara
 0389                        wearablesIsDirty = true;
 390                }
 391            }
 392
 0393            if ( eyesController == null && !unusedCategories.Contains(Categories.EYES))
 0394                unusedCategories.Add(Categories.EYES);
 395
 0396            if ( mouthController == null && !unusedCategories.Contains(Categories.MOUTH))
 0397                unusedCategories.Add(Categories.MOUTH);
 398
 0399            if ( eyebrowsController == null && !unusedCategories.Contains(Categories.EYEBROWS))
 0400                unusedCategories.Add(Categories.EYEBROWS);
 401
 0402            foreach (var category in unusedCategories)
 403            {
 404                switch (category)
 405                {
 406                    case Categories.EYES:
 0407                        eyesController = FacialFeatureController.CreateDefaultFacialFeature(bodyShapeController.bodyShap
 0408                        break;
 409                    case Categories.MOUTH:
 0410                        mouthController = FacialFeatureController.CreateDefaultFacialFeature(bodyShapeController.bodySha
 0411                        break;
 412                    case Categories.EYEBROWS:
 0413                        eyebrowsController = FacialFeatureController.CreateDefaultFacialFeature(bodyShapeController.body
 414                        break;
 415                }
 416            }
 417
 0418            HashSet<string> hiddenList = WearableItem.CompoundHidesList(bodyShapeController.bodyShapeId, resolvedWearabl
 0419            if (!bodyShapeController.isReady)
 420            {
 0421                bodyShapeController.Load(bodyShapeController.bodyShapeId, transform, OnWearableLoadingSuccess, OnBodySha
 422            }
 423
 0424            foreach (WearableController wearable in wearableControllers.Values)
 425            {
 0426                if (bodyIsDirty)
 0427                    wearable.boneRetargetingDirty = true;
 428
 0429                wearable.Load(bodyShapeController.bodyShapeId, transform, OnWearableLoadingSuccess, x => OnWearableLoadi
 0430                yield return null;
 431            }
 432
 433            // TODO(Brian): Evaluate using UniTask<T> instead of this way.
 0434            yield return new WaitUntil(() => bodyShapeController.isReady && wearableControllers.Values.All(x => x.isRead
 435
 0436            eyesController.Load(bodyShapeController, model.eyeColor);
 0437            eyebrowsController.Load(bodyShapeController, model.hairColor);
 0438            mouthController.Load(bodyShapeController, model.skinColor);
 439
 0440            yield return eyesController;
 0441            yield return eyebrowsController;
 0442            yield return mouthController;
 443
 0444            if (bodyIsDirty || wearablesIsDirty)
 445            {
 0446                OnVisualCue?.Invoke(IAvatarRenderer.VisualCue.Loaded);
 447            }
 448
 449            // TODO(Brian): unusedCategories and hiddenList management is a double negative PITA.
 450            //              The load process should define how the avatar should look like before
 451            //              loading it and put this information in a positive list
 452            //              (i.e. not negative, because leads to double negative checks).
 0453            bodyShapeController.SetActiveParts(unusedCategories.Contains(Categories.LOWER_BODY), unusedCategories.Contai
 0454            bodyShapeController.UpdateVisibility(hiddenList);
 0455            foreach (WearableController wearableController in wearableControllers.Values)
 456            {
 0457                wearableController.UpdateVisibility(hiddenList);
 458            }
 459
 0460            CleanUpUnusedItems();
 461
 0462            isLoading = false;
 463
 0464            SetupHairAndSkinColors();
 0465            SetWearableBones();
 466
 467            // TODO(Brian): Expression and sticker update shouldn't be part of avatar loading code!!!! Refactor me pleas
 0468            UpdateExpression();
 469
 0470            var allRenderers = wearableControllers.SelectMany( x => x.Value.GetRenderers() ).ToList();
 0471            allRenderers.AddRange( bodyShapeController.GetRenderers() );
 0472            bool mergeSuccess = MergeAvatar(allRenderers);
 473
 0474            if (mergeSuccess)
 0475                PrepareGpuSkinning();
 476            else
 0477                loadSoftFailed = true;
 478
 0479            maxY = allRenderers.Max(x =>
 480            {
 0481                Bounds bounds = x.bounds;
 0482                return bounds.center.y + bounds.extents.y - transform.position.y;
 483            });
 484
 485            // TODO(Brian): The loadSoftFailed flow is too convoluted--you never know which objects are nulled or empty
 486            //              before reaching this branching statement. The failure should be caught with a throw or other
 487            //              proper language feature.
 0488            if (loadSoftFailed)
 489            {
 0490                OnFailEvent?.Invoke(false);
 0491            }
 492            else
 493            {
 0494                OnSuccessEvent?.Invoke();
 495            }
 0496        }
 497
 498        private void PrepareGpuSkinning()
 499        {
 500            // Sample the animation manually and force an update in the GPUSkinning to avoid giant avatars
 0501            animator.SetIdleFrame();
 0502            animator.animation.Sample();
 503
 0504            gpuSkinning = new SimpleGPUSkinning(
 505                avatarMeshCombiner.renderer,
 506                false); // Bind poses are encoded by the AvatarMeshCombiner before making the mesh unreadable.
 507
 0508            gpuSkinningThrottler = new GPUSkinningThrottler(gpuSkinning);
 0509            gpuSkinningThrottler.SetThrottling(gpuSkinningFramesBetweenUpdates);
 0510        }
 511
 512        void SetupHairAndSkinColors()
 513        {
 0514            bodyShapeController.SetupHairAndSkinColors(model.skinColor, model.hairColor);
 515
 0516            foreach ( var wearable in wearableControllers )
 517            {
 0518                wearable.Value.SetupHairAndSkinColors(model.skinColor, model.hairColor);
 519            }
 0520        }
 521
 522        void OnWearableLoadingSuccess(WearableController wearableController)
 523        {
 0524            if (wearableController == null || model == null)
 525            {
 0526                Debug.LogWarning($"WearableSuccess was called wrongly: IsWearableControllerNull=>{wearableController == 
 0527                OnWearableLoadingFail(wearableController, 0);
 528            }
 0529        }
 530
 531        void OnBodyShapeLoadingFail(WearableController wearableController)
 532        {
 0533            Debug.LogError($"Avatar: {model?.name}  -  Failed loading bodyshape: {wearableController?.id}");
 0534            CleanupAvatar();
 0535            OnFailEvent?.Invoke(true);
 0536        }
 537
 538        void OnWearableLoadingFail(WearableController wearableController, int retriesCount = MAX_RETRIES)
 539        {
 0540            if (retriesCount <= 0)
 541            {
 0542                Debug.LogError($"Avatar: {model?.name}  -  Failed loading wearable: {wearableController?.id}");
 0543                CleanupAvatar();
 0544                OnFailEvent?.Invoke(false);
 0545                return;
 546            }
 547
 0548            wearableController.Load(bodyShapeController.id, transform, OnWearableLoadingSuccess, x => OnWearableLoadingF
 0549        }
 550
 551        private void SetWearableBones()
 552        {
 553            // NOTE(Brian): Set bones/rootBone of all wearables to be the same of the baseBody,
 554            //              so all of them are animated together.
 0555            using (var enumerator = wearableControllers.GetEnumerator())
 556            {
 0557                while (enumerator.MoveNext())
 558                {
 0559                    enumerator.Current.Value.SetAnimatorBones(bodyShapeController.bones, bodyShapeController.rootBone);
 560                }
 0561            }
 0562        }
 563
 564        private void UpdateExpression()
 565        {
 0566            SetExpression(model.expressionTriggerId, model.expressionTriggerTimestamp);
 567
 0568            if (lastStickerTimestamp != model.stickerTriggerTimestamp && model.stickerTriggerId != null)
 569            {
 0570                lastStickerTimestamp = model.stickerTriggerTimestamp;
 571
 0572                if ( stickersController != null )
 0573                    stickersController.PlaySticker(model.stickerTriggerId);
 574            }
 0575        }
 576
 577        public void SetExpression(string id, long timestamp)
 578        {
 2579            model.expressionTriggerId = id;
 2580            model.expressionTriggerTimestamp = timestamp;
 2581            animator.SetExpressionValues(id, timestamp);
 2582        }
 583
 584        private void AddWearableController(WearableItem wearable)
 585        {
 0586            if (wearable == null)
 0587                return;
 0588            switch (wearable.data.category)
 589            {
 590                case Categories.EYES:
 0591                    eyesController = new FacialFeatureController(wearable, eyeMaterial);
 0592                    break;
 593                case Categories.EYEBROWS:
 0594                    eyebrowsController = new FacialFeatureController(wearable, eyebrowMaterial);
 0595                    break;
 596                case Categories.MOUTH:
 0597                    mouthController = new FacialFeatureController(wearable, mouthMaterial);
 0598                    break;
 599                case Categories.BODY_SHAPE:
 600                    break;
 601
 602                default:
 0603                    var wearableController = new WearableController(wearable);
 0604                    wearableControllers.Add(wearable, wearableController);
 605                    break;
 606            }
 0607        }
 608
 609        //TODO: Remove/replace once the class is easily mockable.
 610        protected void CopyFrom(AvatarRenderer original)
 611        {
 0612            this.wearableControllers = original.wearableControllers;
 0613            this.mouthController = original.mouthController;
 0614            this.bodyShapeController = original.bodyShapeController;
 0615            this.eyebrowsController = original.eyebrowsController;
 0616            this.eyesController = original.eyesController;
 0617        }
 618
 619        public void SetGOVisibility(bool newVisibility)
 620        {
 621            //NOTE(Brian): Avatar being loaded needs the renderer.enabled as false until the loading finishes.
 622            //             So we can' manipulate the values because it'd show an incomplete avatar. Its easier to just d
 722623            if (gameObject.activeSelf != newVisibility)
 17624                gameObject.SetActive(newVisibility);
 722625        }
 626
 627        public void SetRendererEnabled(bool newVisibility)
 628        {
 0629            if (mainMeshRenderer == null)
 0630                return;
 631
 0632            mainMeshRenderer.enabled = newVisibility;
 0633        }
 634
 635        public void SetImpostorVisibility(bool impostorVisibility)
 636        {
 1326637            if (impostorVisibility && !initializedImpostor)
 0638                InitializeImpostor();
 639
 1326640            impostorRenderer.gameObject.SetActive(impostorVisibility);
 1326641        }
 642
 0643        public void SetImpostorForward(Vector3 newForward) { impostorRenderer.transform.forward = newForward; }
 644
 0645        public void SetImpostorColor(Color newColor) { AvatarRendererHelpers.SetImpostorTintColor(impostorRenderer.mater
 646
 647        public void SetThrottling(int framesBetweenUpdates)
 648        {
 0649            gpuSkinningFramesBetweenUpdates = framesBetweenUpdates;
 0650            gpuSkinningThrottler?.SetThrottling(gpuSkinningFramesBetweenUpdates);
 0651        }
 652
 653        public void SetAvatarFade(float avatarFade)
 654        {
 0655            if (bodyShapeController == null || !bodyShapeController.isReady)
 0656                return;
 657
 0658            Material[] mats = mainMeshRenderer.sharedMaterials;
 0659            for (int j = 0; j < mats.Length; j++)
 660            {
 0661                mats[j].SetFloat(ShaderUtils.DitherFade, avatarFade);
 662            }
 663
 0664            OnAvatarAlphaValueUpdate?.Invoke(avatarFade);
 0665        }
 666
 667        public void SetImpostorFade(float impostorFade)
 668        {
 669            //TODO implement dither in Unlit shader
 0670            Color current = impostorRenderer.material.GetColor(BASE_COLOR_PROPERTY);
 0671            current.a = impostorFade;
 0672            impostorRenderer.material.SetColor(BASE_COLOR_PROPERTY, current);
 673
 0674            OnImpostorAlphaValueUpdate?.Invoke(impostorFade);
 0675        }
 676
 677        private void HideAll()
 678        {
 679            // TODO: Cache this somewhere (maybe when the LoadAvatar finishes) instead of fetching this on every call
 0680            Renderer[] renderers = gameObject.GetComponentsInChildren<Renderer>();
 681
 0682            for (int i = 0; i < renderers.Length; i++)
 683            {
 0684                renderers[i].enabled = false;
 685            }
 0686        }
 687
 688        public void SetFacialFeaturesVisible(bool visible)
 689        {
 0690            if (bodyShapeController == null || !bodyShapeController.isReady)
 0691                return;
 692
 0693            if (isLoading)
 0694                return;
 695
 0696            bodyShapeController.SetFacialFeaturesVisible(visible, true);
 0697        }
 698
 699        public void SetSSAOEnabled(bool ssaoEnabled)
 700        {
 0701            if ( isLoading )
 0702                return;
 703
 0704            Material[] mats = mainMeshRenderer.sharedMaterials;
 705
 0706            for (int j = 0; j < mats.Length; j++)
 707            {
 0708                if (ssaoEnabled)
 0709                    mats[j].DisableKeyword("_SSAO_OFF");
 710                else
 0711                    mats[j].EnableKeyword("_SSAO_OFF");
 712            }
 0713        }
 714
 715        private bool MergeAvatar(IEnumerable<SkinnedMeshRenderer> allRenderers)
 716        {
 0717            var renderersToCombine = allRenderers.Where((r) => !r.transform.parent.gameObject.name.Contains("Mask")).ToL
 718
 0719            var featureFlags = DataStore.i.featureFlags.flags.Get();
 0720            avatarMeshCombiner.useCullOpaqueHeuristic = featureFlags.IsFeatureEnabled("cull-opaque-heuristic");
 721
 0722            bool success = avatarMeshCombiner.Combine(
 723                bodyShapeController.upperBodyRenderer,
 724                renderersToCombine.ToArray(),
 725                defaultMaterial);
 726
 0727            if ( success )
 728            {
 0729                avatarMeshCombiner.container.transform.SetParent( transform, true );
 0730                avatarMeshCombiner.container.transform.localPosition = Vector3.zero;
 731            }
 732
 0733            return success;
 734        }
 735
 3028736        private void CleanMergedAvatar() { avatarMeshCombiner.Dispose(); }
 737
 738        private void ResetImpostor()
 739        {
 1595740            if (impostorRenderer == null)
 268741                return;
 742
 1327743            if (bodySnapshotTexturePromise != null)
 0744                AssetPromiseKeeper_Texture.i.Forget(bodySnapshotTexturePromise);
 745
 1327746            AvatarRendererHelpers.ResetImpostor(impostorMeshFilter.mesh, impostorRenderer.material);
 747
 1327748            initializedImpostor = false;
 1327749        }
 750
 751        private void LateUpdate()
 752        {
 20842753            if (gpuSkinning != null && mainMeshRenderer.enabled)
 0754                gpuSkinningThrottler.TryUpdate();
 20842755        }
 756
 757        protected virtual void OnDestroy()
 758        {
 849759            isDestroyed = true;
 849760            CleanupAvatar();
 849761        }
 762    }
 763}