< Summary

Class:DCL.AvatarRenderer
Assembly:AvatarShape
File(s):/tmp/workspace/unity-renderer/unity-renderer/Assets/Scripts/MainScripts/DCL/Components/Avatar/AvatarRenderer.cs
Covered lines:87
Uncovered lines:254
Coverable lines:341
Total lines:707
Line coverage:25.5% (87 of 341)
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%15.4610062.07%
InitializeImpostor()0%30500%
StopLoadingCoroutines()0%220100%
CleanupAvatar()0%10.0210093.75%
CleanUpUnusedItems()0%30500%
LoadAvatar()0%2466.695304.93%
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%110100%
SetImpostorForward(...)0%2100%
SetImpostorColor(...)0%2100%
SetThrottling(...)0%6200%
SetAvatarFade(...)0%20400%
SetImpostorFade(...)0%6200%
HideAll()0%6200%
SetFacialFeaturesVisible(...)0%20400%
SetSSAOEnabled(...)0%20400%
MergeAvatar(...)0%12300%
CleanMergedAvatar()0%110100%
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        private const int MAX_RETRIES = 5;
 16
 17        public Material defaultMaterial;
 18        public Material eyeMaterial;
 19        public Material eyebrowMaterial;
 20        public Material mouthMaterial;
 21
 22        public MeshRenderer lodRenderer;
 23        public MeshFilter lodMeshFilter;
 24
 25        private AvatarModel model;
 26        private AvatarMeshCombinerHelper avatarMeshCombiner;
 27        private SimpleGPUSkinning gpuSkinning = null;
 28        private GPUSkinningThrottler gpuSkinningThrottler = null;
 107029        private int gpuSkinningFramesBetweenUpdates = 1;
 30
 31        private Renderer mainMeshRenderer
 32        {
 33            get
 34            {
 035                if (gpuSkinning != null)
 036                    return gpuSkinning.renderer;
 037                return avatarMeshCombiner.renderer;
 38            }
 39        }
 40
 41        public event Action<IAvatarRenderer.VisualCue> OnVisualCue;
 42        public event Action OnSuccessEvent;
 43        public event Action<float> OnImpostorAlphaValueUpdate;
 44        public event Action<bool> OnFailEvent;
 45
 46        internal BodyShapeController bodyShapeController;
 107047        internal Dictionary<WearableItem, WearableController> wearableControllers = new Dictionary<WearableItem, Wearabl
 48        internal FacialFeatureController eyesController;
 49        internal FacialFeatureController eyebrowsController;
 50        internal FacialFeatureController mouthController;
 51        internal AvatarAnimatorLegacy animator;
 52        internal StickersController stickersController;
 53
 107054        private long lastStickerTimestamp = -1;
 55
 56        public bool isLoading;
 057        public bool isReady => bodyShapeController != null && bodyShapeController.isReady && wearableControllers != null
 58
 59        private Coroutine loadCoroutine;
 107060        private List<string> wearablesInUse = new List<string>();
 61        private AssetPromise_Texture bodySnapshotTexturePromise;
 62        private bool isDestroyed = false;
 63
 64        private void Awake()
 65        {
 84266            animator = GetComponent<AvatarAnimatorLegacy>();
 84267            stickersController = GetComponent<StickersController>();
 84268            avatarMeshCombiner = new AvatarMeshCombinerHelper();
 69
 84270            if (lodRenderer != null)
 65371                SetImpostorVisibility(false);
 84272        }
 73
 74        public void ApplyModel(AvatarModel model, Action onSuccess, Action onFail)
 75        {
 12476            if ( this.model != null )
 77            {
 7578                if (model != null && this.model.Equals(model))
 79                {
 4580                    onSuccess?.Invoke();
 3281                    return;
 82                }
 83
 3084                bool wearablesChanged = !this.model.HaveSameWearablesAndColors(model);
 3085                bool expressionsChanged = !this.model.HaveSameExpressions(model);
 86
 3087                if (!wearablesChanged && expressionsChanged)
 88                {
 089                    this.model.expressionTriggerId = model.expressionTriggerId;
 090                    this.model.expressionTriggerTimestamp = model.expressionTriggerTimestamp;
 091                    this.model.stickerTriggerId = model.stickerTriggerId;
 092                    this.model.stickerTriggerTimestamp = model.stickerTriggerTimestamp;
 093                    UpdateExpression();
 094                    onSuccess?.Invoke();
 095                    return;
 96                }
 97            }
 98
 7999            this.model = new AvatarModel();
 79100            this.model.CopyFrom(model);
 79101            if (bodySnapshotTexturePromise != null)
 0102                AssetPromiseKeeper_Texture.i.Forget(bodySnapshotTexturePromise);
 103
 104            // TODO(Brian): Find a better approach than overloading callbacks like this. This code is not readable.
 105            void onSuccessWrapper()
 106            {
 0107                onSuccess?.Invoke();
 0108                this.OnSuccessEvent -= onSuccessWrapper;
 0109            }
 110
 79111            this.OnSuccessEvent += onSuccessWrapper;
 112
 113            void onFailWrapper(bool isFatalError)
 114            {
 6115                onFail?.Invoke();
 6116                this.OnFailEvent -= onFailWrapper;
 6117            }
 118
 79119            this.OnFailEvent += onFailWrapper;
 120
 79121            isLoading = false;
 122
 79123            if (model == null)
 124            {
 0125                CleanupAvatar();
 0126                this.OnSuccessEvent?.Invoke();
 0127                return;
 128            }
 129
 79130            StopLoadingCoroutines();
 79131            isLoading = true;
 79132            loadCoroutine = CoroutineStarter.Start(LoadAvatar());
 79133        }
 134
 135        public void InitializeImpostor()
 136        {
 137            // The fetched snapshot can take its time so it's better to assign a generic impostor first.
 0138            AvatarRendererHelpers.RandomizeAndApplyGenericImpostor(lodMeshFilter.mesh);
 139
 0140            UserProfile userProfile = null;
 0141            if (!string.IsNullOrEmpty(model?.id))
 0142                userProfile = UserProfileController.GetProfileByUserId(model.id);
 143
 0144            if (userProfile != null)
 145            {
 0146                bodySnapshotTexturePromise = new AssetPromise_Texture(userProfile.bodySnapshotURL);
 0147                bodySnapshotTexturePromise.OnSuccessEvent += asset => AvatarRendererHelpers.SetImpostorTexture(asset.tex
 0148                bodySnapshotTexturePromise.OnFailEvent += asset => AvatarRendererHelpers.RandomizeAndApplyGenericImposto
 0149                AssetPromiseKeeper_Texture.i.Keep(bodySnapshotTexturePromise);
 150            }
 0151        }
 152
 153        void StopLoadingCoroutines()
 154        {
 1575155            if (loadCoroutine != null)
 78156                CoroutineStarter.Stop(loadCoroutine);
 157
 1575158            loadCoroutine = null;
 1575159        }
 160
 161        public void CleanupAvatar()
 162        {
 1496163            StopLoadingCoroutines();
 1496164            if (!isDestroyed)
 165            {
 655166                SetGOVisibility(true);
 655167                if (lodRenderer != null)
 655168                    SetImpostorVisibility(false);
 169            }
 170
 1496171            avatarMeshCombiner.Dispose();
 1496172            gpuSkinningThrottler = null;
 1496173            gpuSkinning = null;
 1496174            eyebrowsController?.CleanUp();
 1496175            eyebrowsController = null;
 176
 1496177            eyesController?.CleanUp();
 1496178            eyesController = null;
 179
 1496180            mouthController?.CleanUp();
 1496181            mouthController = null;
 182
 1496183            bodyShapeController?.CleanUp();
 1496184            bodyShapeController = null;
 185
 1496186            using (var iterator = wearableControllers.GetEnumerator())
 187            {
 1496188                while (iterator.MoveNext())
 189                {
 0190                    iterator.Current.Value.CleanUp();
 191                }
 1496192            }
 193
 1496194            wearableControllers.Clear();
 1496195            model = null;
 1496196            isLoading = false;
 1496197            OnFailEvent = null;
 1496198            OnSuccessEvent = null;
 199
 1496200            CleanMergedAvatar();
 201
 1496202            if (bodySnapshotTexturePromise != null)
 0203                AssetPromiseKeeper_Texture.i.Forget(bodySnapshotTexturePromise);
 204
 1496205            CatalogController.RemoveWearablesInUse(wearablesInUse);
 1496206            wearablesInUse.Clear();
 1496207            OnVisualCue?.Invoke(IAvatarRenderer.VisualCue.CleanedUp);
 1452208        }
 209
 210        void CleanUpUnusedItems()
 211        {
 0212            if (model.wearables == null)
 0213                return;
 214
 0215            var ids = wearableControllers.Keys.ToArray();
 216
 0217            for (var i = 0; i < ids.Length; i++)
 218            {
 0219                var currentId = ids[i];
 0220                var wearable = wearableControllers[currentId];
 221
 0222                if (!model.wearables.Contains(wearable.id) || !wearable.IsLoadedForBodyShape(model.bodyShape))
 223                {
 0224                    wearable.CleanUp();
 0225                    wearableControllers.Remove(currentId);
 226                }
 227            }
 0228        }
 229
 230        // TODO(Brian): Pure functions should be extracted from this big LoadAvatar() method and unit-test separately.
 231        //              The current approach has tech debt that's getting very expensive and is costing many hours of de
 232        //              Avatar Loading should be a self contained operation that doesn't depend on pool management and A
 233        //              lifecycle like it does now.
 234        private IEnumerator LoadAvatar()
 235        {
 236            // TODO(Brian): This is an ugly hack, all the loading should be performed
 237            //              without being afraid of the gameObject active state.
 158238            yield return new WaitUntil(() => gameObject.activeSelf);
 239
 6240            bool loadSoftFailed = false;
 241
 6242            WearableItem resolvedBody = null;
 243
 244            // TODO(Brian): Evaluate using UniTask<T> here instead of Helpers.Promise.
 6245            Helpers.Promise<WearableItem> avatarBodyPromise = null;
 6246            if (!string.IsNullOrEmpty(model.bodyShape))
 247            {
 0248                avatarBodyPromise = CatalogController.RequestWearable(model.bodyShape);
 0249            }
 250            else
 251            {
 6252                OnFailEvent?.Invoke(true);
 6253                yield break;
 254            }
 255
 0256            List<WearableItem> resolvedWearables = new List<WearableItem>();
 257
 258            // TODO(Brian): Evaluate using UniTask<T> here instead of Helpers.Promise.
 0259            List<Helpers.Promise<WearableItem>> avatarWearablePromises = new List<Helpers.Promise<WearableItem>>();
 0260            if (model.wearables != null)
 261            {
 0262                for (int i = 0; i < model.wearables.Count; i++)
 263                {
 0264                    avatarWearablePromises.Add(CatalogController.RequestWearable(model.wearables[i]));
 265                }
 266            }
 267
 268            // In this point, all the requests related to the avatar's wearables have been collected and sent to the Cat
 269            // From here we wait for the response of the requested wearables and process them.
 0270            if (avatarBodyPromise != null)
 271            {
 0272                yield return avatarBodyPromise;
 273
 0274                if (!string.IsNullOrEmpty(avatarBodyPromise.error))
 275                {
 0276                    Debug.LogError(avatarBodyPromise.error);
 0277                    loadSoftFailed = true;
 0278                }
 279                else
 280                {
 0281                    resolvedBody = avatarBodyPromise.value;
 0282                    wearablesInUse.Add(avatarBodyPromise.value.id);
 283                }
 284            }
 285
 0286            if (resolvedBody == null)
 287            {
 0288                isLoading = false;
 0289                OnFailEvent?.Invoke(true);
 0290                yield break;
 291            }
 292
 293            // TODO(Brian): Evaluate using UniTask<T> here instead of Helpers.Promise.
 0294            List<Helpers.Promise<WearableItem>> replacementPromises = new List<Helpers.Promise<WearableItem>>();
 295
 0296            foreach (var avatarWearablePromise in avatarWearablePromises)
 297            {
 0298                yield return avatarWearablePromise;
 299
 0300                if (!string.IsNullOrEmpty(avatarWearablePromise.error))
 301                {
 0302                    Debug.LogError(avatarWearablePromise.error);
 0303                    loadSoftFailed = true;
 0304                }
 305                else
 306                {
 0307                    WearableItem wearableItem = avatarWearablePromise.value;
 0308                    wearablesInUse.Add(wearableItem.id);
 309
 0310                    if (wearableItem.GetRepresentation(model.bodyShape) != null)
 311                    {
 0312                        resolvedWearables.Add(wearableItem);
 0313                    }
 314                    else
 315                    {
 0316                        model.wearables.Remove(wearableItem.id);
 0317                        string defaultReplacement = DefaultWearables.GetDefaultWearable(model.bodyShape, wearableItem.da
 0318                        if (!string.IsNullOrEmpty(defaultReplacement))
 319                        {
 0320                            model.wearables.Add(defaultReplacement);
 0321                            replacementPromises.Add(CatalogController.RequestWearable(defaultReplacement));
 322                        }
 323                    }
 324                }
 0325            }
 326
 0327            foreach (var wearablePromise in replacementPromises)
 328            {
 0329                yield return wearablePromise;
 330
 0331                if (!string.IsNullOrEmpty(wearablePromise.error))
 332                {
 0333                    Debug.LogError(wearablePromise.error);
 0334                    loadSoftFailed = true;
 0335                }
 336                else
 337                {
 0338                    WearableItem wearableItem = wearablePromise.value;
 0339                    wearablesInUse.Add(wearableItem.id);
 0340                    resolvedWearables.Add(wearableItem);
 341                }
 0342            }
 343
 0344            bool bodyIsDirty = false;
 0345            if (bodyShapeController != null && bodyShapeController.id != model?.bodyShape)
 346            {
 0347                bodyShapeController.CleanUp();
 0348                bodyShapeController = null;
 0349                bodyIsDirty = true;
 350            }
 351
 0352            if (bodyShapeController == null)
 353            {
 0354                HideAll();
 0355                bodyShapeController = new BodyShapeController(resolvedBody);
 0356                eyesController = FacialFeatureController.CreateDefaultFacialFeature(bodyShapeController.bodyShapeId, Cat
 0357                eyebrowsController = FacialFeatureController.CreateDefaultFacialFeature(bodyShapeController.bodyShapeId,
 0358                mouthController = FacialFeatureController.CreateDefaultFacialFeature(bodyShapeController.bodyShapeId, Ca
 359            }
 360
 361            //TODO(Brian): This logic should be performed in a testeable pure function instead of this inline approach.
 362            //             Moreover, this function should work against data, not wearableController instances.
 0363            bool wearablesIsDirty = false;
 0364            HashSet<string> unusedCategories = new HashSet<string>(Categories.ALL);
 0365            int wearableCount = resolvedWearables.Count;
 0366            for (int index = 0; index < wearableCount; index++)
 367            {
 0368                WearableItem wearable = resolvedWearables[index];
 0369                if (wearable == null)
 370                    continue;
 371
 0372                unusedCategories.Remove(wearable.data.category);
 0373                if (wearableControllers.ContainsKey(wearable))
 374                {
 0375                    if (!wearableControllers[wearable].IsLoadedForBodyShape(bodyShapeController.bodyShapeId))
 0376                        wearableControllers[wearable].CleanUp();
 0377                }
 378                else
 379                {
 0380                    AddWearableController(wearable);
 0381                    if (wearable.data.category != Categories.EYES && wearable.data.category != Categories.MOUTH && weara
 0382                        wearablesIsDirty = true;
 383                }
 384            }
 385
 0386            foreach (var category in unusedCategories)
 387            {
 388                switch (category)
 389                {
 390                    case Categories.EYES:
 0391                        eyesController = FacialFeatureController.CreateDefaultFacialFeature(bodyShapeController.bodyShap
 0392                        break;
 393                    case Categories.MOUTH:
 0394                        mouthController = FacialFeatureController.CreateDefaultFacialFeature(bodyShapeController.bodySha
 0395                        break;
 396                    case Categories.EYEBROWS:
 0397                        eyebrowsController = FacialFeatureController.CreateDefaultFacialFeature(bodyShapeController.body
 398                        break;
 399                }
 400            }
 401
 0402            HashSet<string> hiddenList = WearableItem.CompoundHidesList(bodyShapeController.bodyShapeId, resolvedWearabl
 0403            if (!bodyShapeController.isReady)
 404            {
 0405                bodyShapeController.Load(bodyShapeController.bodyShapeId, transform, OnWearableLoadingSuccess, OnBodySha
 406            }
 407
 0408            foreach (WearableController wearable in wearableControllers.Values)
 409            {
 0410                if (bodyIsDirty)
 0411                    wearable.boneRetargetingDirty = true;
 412
 0413                wearable.Load(bodyShapeController.bodyShapeId, transform, OnWearableLoadingSuccess, x => OnWearableLoadi
 0414                yield return null;
 415            }
 416
 417            // TODO(Brian): Evaluate using UniTask<T> instead of this way.
 0418            yield return new WaitUntil(() => bodyShapeController.isReady && wearableControllers.Values.All(x => x.isRead
 419
 0420            eyesController.Load(bodyShapeController, model.eyeColor);
 0421            eyebrowsController.Load(bodyShapeController, model.hairColor);
 0422            mouthController.Load(bodyShapeController, model.skinColor);
 423
 0424            yield return eyesController;
 0425            yield return eyebrowsController;
 0426            yield return mouthController;
 427
 0428            if (bodyIsDirty || wearablesIsDirty)
 429            {
 0430                OnVisualCue?.Invoke(IAvatarRenderer.VisualCue.Loaded);
 431            }
 432
 433            // TODO(Brian): unusedCategories and hiddenList management is a double negative PITA.
 434            //              The load process should define how the avatar should look like before
 435            //              loading it and put this information in a positive list
 436            //              (i.e. not negative, because leads to double negative checks).
 0437            bodyShapeController.SetActiveParts(unusedCategories.Contains(Categories.LOWER_BODY), unusedCategories.Contai
 0438            bodyShapeController.UpdateVisibility(hiddenList);
 0439            foreach (WearableController wearableController in wearableControllers.Values)
 440            {
 0441                wearableController.UpdateVisibility(hiddenList);
 442            }
 443
 0444            CleanUpUnusedItems();
 445
 0446            isLoading = false;
 447
 0448            SetupHairAndSkinColors();
 0449            SetWearableBones();
 450
 451            // TODO(Brian): Expression and sticker update shouldn't be part of avatar loading code!!!! Refactor me pleas
 0452            UpdateExpression();
 453
 0454            var allRenderers = wearableControllers.SelectMany( x => x.Value.GetRenderers() ).ToList();
 0455            allRenderers.AddRange( bodyShapeController.GetRenderers() );
 0456            bool mergeSuccess = MergeAvatar(allRenderers);
 457
 0458            if (mergeSuccess)
 459            {
 0460                gpuSkinning = new SimpleGPUSkinning(avatarMeshCombiner.renderer);
 461                // Sample the animation manually and force an update in the GPUSkinning to avoid giant avatars
 0462                animator.SetIdleFrame();
 0463                animator.animation.Sample();
 0464                gpuSkinning.Update(true);
 465
 0466                gpuSkinningThrottler = new GPUSkinningThrottler(gpuSkinning);
 0467                gpuSkinningThrottler.SetThrottling(gpuSkinningFramesBetweenUpdates);
 0468            }
 469            else
 0470                loadSoftFailed = true;
 471
 472            // TODO(Brian): The loadSoftFailed flow is too convoluted--you never know which objects are nulled or empty
 473            //              before reaching this branching statement. The failure should be caught with a throw or other
 474            //              proper language feature.
 0475            if (loadSoftFailed)
 476            {
 0477                OnFailEvent?.Invoke(false);
 0478            }
 479            else
 480            {
 0481                OnSuccessEvent?.Invoke();
 482            }
 0483        }
 484
 485        void SetupHairAndSkinColors()
 486        {
 0487            bodyShapeController.SetupHairAndSkinColors(model.skinColor, model.hairColor);
 488
 0489            foreach ( var wearable in wearableControllers )
 490            {
 0491                wearable.Value.SetupHairAndSkinColors(model.skinColor, model.hairColor);
 492            }
 0493        }
 494
 495        void OnWearableLoadingSuccess(WearableController wearableController)
 496        {
 0497            if (wearableController == null || model == null)
 498            {
 0499                Debug.LogWarning($"WearableSuccess was called wrongly: IsWearableControllerNull=>{wearableController == 
 0500                OnWearableLoadingFail(wearableController, 0);
 501            }
 0502        }
 503
 504        void OnBodyShapeLoadingFail(WearableController wearableController)
 505        {
 0506            Debug.LogError($"Avatar: {model?.name}  -  Failed loading bodyshape: {wearableController?.id}");
 0507            CleanupAvatar();
 0508            OnFailEvent?.Invoke(true);
 0509        }
 510
 511        void OnWearableLoadingFail(WearableController wearableController, int retriesCount = MAX_RETRIES)
 512        {
 0513            if (retriesCount <= 0)
 514            {
 0515                Debug.LogError($"Avatar: {model?.name}  -  Failed loading wearable: {wearableController?.id}");
 0516                CleanupAvatar();
 0517                OnFailEvent?.Invoke(false);
 0518                return;
 519            }
 520
 0521            wearableController.Load(bodyShapeController.id, transform, OnWearableLoadingSuccess, x => OnWearableLoadingF
 0522        }
 523
 524        private void SetWearableBones()
 525        {
 526            // NOTE(Brian): Set bones/rootBone of all wearables to be the same of the baseBody,
 527            //              so all of them are animated together.
 0528            using (var enumerator = wearableControllers.GetEnumerator())
 529            {
 0530                while (enumerator.MoveNext())
 531                {
 0532                    enumerator.Current.Value.SetAnimatorBones(bodyShapeController.bones, bodyShapeController.rootBone);
 533                }
 0534            }
 0535        }
 536
 537        private void UpdateExpression()
 538        {
 0539            SetExpression(model.expressionTriggerId, model.expressionTriggerTimestamp);
 540
 0541            if (lastStickerTimestamp != model.stickerTriggerTimestamp && model.stickerTriggerId != null)
 542            {
 0543                lastStickerTimestamp = model.stickerTriggerTimestamp;
 544
 0545                if ( stickersController != null )
 0546                    stickersController.PlayEmote(model.stickerTriggerId);
 547            }
 0548        }
 549
 550        public void SetExpression(string id, long timestamp)
 551        {
 2552            model.expressionTriggerId = id;
 2553            model.expressionTriggerTimestamp = timestamp;
 2554            animator.SetExpressionValues(id, timestamp);
 2555        }
 556
 557        private void AddWearableController(WearableItem wearable)
 558        {
 0559            if (wearable == null)
 0560                return;
 0561            switch (wearable.data.category)
 562            {
 563                case Categories.EYES:
 0564                    eyesController = new FacialFeatureController(wearable, eyeMaterial);
 0565                    break;
 566                case Categories.EYEBROWS:
 0567                    eyebrowsController = new FacialFeatureController(wearable, eyebrowMaterial);
 0568                    break;
 569                case Categories.MOUTH:
 0570                    mouthController = new FacialFeatureController(wearable, mouthMaterial);
 0571                    break;
 572                case Categories.BODY_SHAPE:
 573                    break;
 574
 575                default:
 0576                    var wearableController = new WearableController(wearable);
 0577                    wearableControllers.Add(wearable, wearableController);
 578                    break;
 579            }
 0580        }
 581
 582        //TODO: Remove/replace once the class is easily mockable.
 583        protected void CopyFrom(AvatarRenderer original)
 584        {
 0585            this.wearableControllers = original.wearableControllers;
 0586            this.mouthController = original.mouthController;
 0587            this.bodyShapeController = original.bodyShapeController;
 0588            this.eyebrowsController = original.eyebrowsController;
 0589            this.eyesController = original.eyesController;
 0590        }
 591
 592        public void SetGOVisibility(bool newVisibility)
 593        {
 594            //NOTE(Brian): Avatar being loaded needs the renderer.enabled as false until the loading finishes.
 595            //             So we can' manipulate the values because it'd show an incomplete avatar. Its easier to just d
 711596            if (gameObject.activeSelf != newVisibility)
 16597                gameObject.SetActive(newVisibility);
 711598        }
 599
 600        public void SetRendererEnabled(bool newVisibility)
 601        {
 0602            if (mainMeshRenderer == null)
 0603                return;
 604
 0605            mainMeshRenderer.enabled = newVisibility;
 0606        }
 607
 2616608        public void SetImpostorVisibility(bool impostorVisibility) { lodRenderer.gameObject.SetActive(impostorVisibility
 609
 0610        public void SetImpostorForward(Vector3 newForward) { lodRenderer.transform.forward = newForward; }
 611
 0612        public void SetImpostorColor(Color newColor) { AvatarRendererHelpers.SetImpostorTintColor(lodRenderer.material, 
 613        public void SetThrottling(int framesBetweenUpdates)
 614        {
 0615            gpuSkinningFramesBetweenUpdates = framesBetweenUpdates;
 0616            gpuSkinningThrottler?.SetThrottling(gpuSkinningFramesBetweenUpdates);
 0617        }
 618
 619        public void SetAvatarFade(float avatarFade)
 620        {
 0621            if (bodyShapeController == null || !bodyShapeController.isReady)
 0622                return;
 623
 0624            Material[] mats = mainMeshRenderer.sharedMaterials;
 0625            for (int j = 0; j < mats.Length; j++)
 626            {
 0627                mats[j].SetFloat(ShaderUtils.DitherFade, avatarFade);
 628            }
 0629        }
 630
 631        public void SetImpostorFade(float impostorFade)
 632        {
 633            //TODO implement dither in Unlit shader
 0634            Color current = lodRenderer.material.GetColor(BASE_COLOR_PROPERTY);
 0635            current.a = impostorFade;
 0636            lodRenderer.material.SetColor(BASE_COLOR_PROPERTY, current);
 637
 0638            OnImpostorAlphaValueUpdate?.Invoke(impostorFade);
 0639        }
 640
 641        private void HideAll()
 642        {
 643            // TODO: Cache this somewhere (maybe when the LoadAvatar finishes) instead of fetching this on every call
 0644            Renderer[] renderers = gameObject.GetComponentsInChildren<Renderer>();
 645
 0646            for (int i = 0; i < renderers.Length; i++)
 647            {
 0648                renderers[i].enabled = false;
 649            }
 0650        }
 651
 652        public void SetFacialFeaturesVisible(bool visible)
 653        {
 0654            if (bodyShapeController == null || !bodyShapeController.isReady)
 0655                return;
 656
 0657            if (isLoading)
 0658                return;
 659
 0660            bodyShapeController.SetFacialFeaturesVisible(visible, true);
 0661        }
 662
 663        public void SetSSAOEnabled(bool ssaoEnabled)
 664        {
 0665            if ( isLoading )
 0666                return;
 667
 0668            Material[] mats = mainMeshRenderer.sharedMaterials;
 669
 0670            for (int j = 0; j < mats.Length; j++)
 671            {
 0672                if (ssaoEnabled)
 0673                    mats[j].DisableKeyword("_SSAO_OFF");
 674                else
 0675                    mats[j].EnableKeyword("_SSAO_OFF");
 676            }
 0677        }
 678
 679        private bool MergeAvatar(IEnumerable<SkinnedMeshRenderer> allRenderers)
 680        {
 0681            var renderersToCombine = allRenderers.Where((r) => !r.transform.parent.gameObject.name.Contains("Mask")).ToL
 0682            bool success = avatarMeshCombiner.Combine(bodyShapeController.upperBodyRenderer, renderersToCombine.ToArray(
 683
 0684            if ( success )
 685            {
 0686                avatarMeshCombiner.container.transform.SetParent( transform, true );
 0687                avatarMeshCombiner.container.transform.localPosition = Vector3.zero;
 688            }
 689
 0690            return success;
 691        }
 692
 2992693        void CleanMergedAvatar() { avatarMeshCombiner.Dispose(); }
 694
 695        private void LateUpdate()
 696        {
 18399697            if (gpuSkinning != null && mainMeshRenderer.enabled)
 0698                gpuSkinningThrottler.TryUpdate();
 18399699        }
 700
 701        protected virtual void OnDestroy()
 702        {
 841703            isDestroyed = true;
 841704            CleanupAvatar();
 841705        }
 706    }
 707}