< 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:260
Coverable lines:357
Total lines:751
Line coverage:27.1% (97 of 357)
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%3050.125904.93%
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%20400%
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
 23        public MeshRenderer impostorRenderer;
 24        public MeshFilter impostorMeshFilter;
 25
 26        private AvatarModel model;
 27        private AvatarMeshCombinerHelper avatarMeshCombiner;
 28        private SimpleGPUSkinning gpuSkinning = null;
 29        private GPUSkinningThrottler gpuSkinningThrottler = null;
 107230        private int gpuSkinningFramesBetweenUpdates = 1;
 31        private bool initializedImpostor = false;
 32
 33        private Renderer mainMeshRenderer
 34        {
 35            get
 36            {
 037                if (gpuSkinning != null)
 038                    return gpuSkinning.renderer;
 039                return avatarMeshCombiner.renderer;
 40            }
 41        }
 42
 43        public event Action<IAvatarRenderer.VisualCue> OnVisualCue;
 44        public event Action OnSuccessEvent;
 45        public event Action<float> OnImpostorAlphaValueUpdate;
 46        public event Action<bool> OnFailEvent;
 47
 48        internal BodyShapeController bodyShapeController;
 107249        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
 107256        private long lastStickerTimestamp = -1;
 57
 58        public bool isLoading;
 059        public bool isReady => bodyShapeController != null && bodyShapeController.isReady && wearableControllers != null
 60
 61        private Coroutine loadCoroutine;
 62        private AssetPromise_Texture bodySnapshotTexturePromise;
 107263        private List<string> wearablesInUse = new List<string>();
 64        private bool isDestroyed = false;
 65
 66        private void Awake()
 67        {
 84468            animator = GetComponent<AvatarAnimatorLegacy>();
 84469            stickersController = GetComponent<StickersController>();
 84470            avatarMeshCombiner = new AvatarMeshCombinerHelper();
 84471            avatarMeshCombiner.prepareMeshForGpuSkinning = true;
 84472            avatarMeshCombiner.uploadMeshToGpu = true;
 73
 84474            if (impostorRenderer != null)
 65475                SetImpostorVisibility(false);
 84476        }
 77
 78        public void ApplyModel(AvatarModel model, Action onSuccess, Action onFail)
 79        {
 13780            if ( this.model != null )
 81            {
 8782                if (model != null && this.model.Equals(model))
 83                {
 5684                    onSuccess?.Invoke();
 4385                    return;
 86                }
 87
 3188                bool wearablesChanged = !this.model.HaveSameWearablesAndColors(model);
 3189                bool expressionsChanged = !this.model.HaveSameExpressions(model);
 90
 3191                if (!wearablesChanged && expressionsChanged)
 92                {
 093                    this.model.expressionTriggerId = model.expressionTriggerId;
 094                    this.model.expressionTriggerTimestamp = model.expressionTriggerTimestamp;
 095                    this.model.stickerTriggerId = model.stickerTriggerId;
 096                    this.model.stickerTriggerTimestamp = model.stickerTriggerTimestamp;
 097                    UpdateExpression();
 098                    onSuccess?.Invoke();
 099                    return;
 100                }
 101            }
 102
 81103            this.model = new AvatarModel();
 81104            this.model.CopyFrom(model);
 105
 81106            ResetImpostor();
 107
 108            // TODO(Brian): Find a better approach than overloading callbacks like this. This code is not readable.
 109            void onSuccessWrapper()
 110            {
 0111                onSuccess?.Invoke();
 0112                this.OnSuccessEvent -= onSuccessWrapper;
 0113            }
 114
 81115            this.OnSuccessEvent += onSuccessWrapper;
 116
 117            void onFailWrapper(bool isFatalError)
 118            {
 6119                onFail?.Invoke();
 6120                this.OnFailEvent -= onFailWrapper;
 6121            }
 122
 81123            this.OnFailEvent += onFailWrapper;
 124
 81125            isLoading = false;
 126
 81127            if (model == null)
 128            {
 0129                CleanupAvatar();
 0130                this.OnSuccessEvent?.Invoke();
 0131                return;
 132            }
 133
 81134            StopLoadingCoroutines();
 135
 81136            isLoading = true;
 81137            loadCoroutine = CoroutineStarter.Start(LoadAvatar());
 81138        }
 139
 140        public void InitializeImpostor()
 141        {
 0142            initializedImpostor = true;
 143
 144            // The fetched snapshot can take its time so it's better to assign a generic impostor first.
 0145            AvatarRendererHelpers.RandomizeAndApplyGenericImpostor(impostorMeshFilter.mesh, impostorRenderer.material);
 146
 0147            UserProfile userProfile = null;
 0148            if (!string.IsNullOrEmpty(model?.id))
 0149                userProfile = UserProfileController.GetProfileByUserId(model.id);
 150
 0151            if (userProfile != null)
 152            {
 0153                bodySnapshotTexturePromise = new AssetPromise_Texture(userProfile.bodySnapshotURL);
 0154                bodySnapshotTexturePromise.OnSuccessEvent += asset => AvatarRendererHelpers.SetImpostorTexture(asset.tex
 0155                AssetPromiseKeeper_Texture.i.Keep(bodySnapshotTexturePromise);
 156            }
 0157        }
 158
 159        void StopLoadingCoroutines()
 160        {
 1582161            if (loadCoroutine != null)
 80162                CoroutineStarter.Stop(loadCoroutine);
 163
 1582164            loadCoroutine = null;
 1582165        }
 166
 167        public void CleanupAvatar()
 168        {
 1501169            StopLoadingCoroutines();
 1501170            if (!isDestroyed)
 171            {
 658172                SetGOVisibility(true);
 658173                if (impostorRenderer != null)
 658174                    SetImpostorVisibility(false);
 175            }
 176
 1501177            avatarMeshCombiner.Dispose();
 1501178            gpuSkinningThrottler = null;
 1501179            gpuSkinning = null;
 1501180            eyebrowsController?.CleanUp();
 1501181            eyebrowsController = null;
 182
 1501183            eyesController?.CleanUp();
 1501184            eyesController = null;
 185
 1501186            mouthController?.CleanUp();
 1501187            mouthController = null;
 188
 1501189            bodyShapeController?.CleanUp();
 1501190            bodyShapeController = null;
 191
 1501192            using (var iterator = wearableControllers.GetEnumerator())
 193            {
 1501194                while (iterator.MoveNext())
 195                {
 0196                    iterator.Current.Value.CleanUp();
 197                }
 1501198            }
 199
 1501200            wearableControllers.Clear();
 1501201            model = null;
 1501202            isLoading = false;
 1501203            OnFailEvent = null;
 1501204            OnSuccessEvent = null;
 205
 1501206            CleanMergedAvatar();
 207
 1501208            ResetImpostor();
 209
 1501210            CatalogController.RemoveWearablesInUse(wearablesInUse);
 1501211            wearablesInUse.Clear();
 1501212            OnVisualCue?.Invoke(IAvatarRenderer.VisualCue.CleanedUp);
 1457213        }
 214
 215        void CleanUpUnusedItems()
 216        {
 0217            if (model.wearables == null)
 0218                return;
 219
 0220            var ids = wearableControllers.Keys.ToArray();
 221
 0222            for (var i = 0; i < ids.Length; i++)
 223            {
 0224                var currentId = ids[i];
 0225                var wearable = wearableControllers[currentId];
 226
 0227                if (!model.wearables.Contains(wearable.id) || !wearable.IsLoadedForBodyShape(model.bodyShape))
 228                {
 0229                    wearable.CleanUp();
 0230                    wearableControllers.Remove(currentId);
 231                }
 232            }
 0233        }
 234
 235        // TODO(Brian): Pure functions should be extracted from this big LoadAvatar() method and unit-test separately.
 236        //              The current approach has tech debt that's getting very expensive and is costing many hours of de
 237        //              Avatar Loading should be a self contained operation that doesn't depend on pool management and A
 238        //              lifecycle like it does now.
 239        private IEnumerator LoadAvatar()
 240        {
 241            // TODO(Brian): This is an ugly hack, all the loading should be performed
 242            //              without being afraid of the gameObject active state.
 162243            yield return new WaitUntil(() => gameObject.activeSelf);
 244
 6245            bool loadSoftFailed = false;
 246
 6247            WearableItem resolvedBody = null;
 248
 249            // TODO(Brian): Evaluate using UniTask<T> here instead of Helpers.Promise.
 6250            Helpers.Promise<WearableItem> avatarBodyPromise = null;
 6251            if (!string.IsNullOrEmpty(model.bodyShape))
 252            {
 0253                avatarBodyPromise = CatalogController.RequestWearable(model.bodyShape);
 0254            }
 255            else
 256            {
 6257                OnFailEvent?.Invoke(true);
 6258                yield break;
 259            }
 260
 0261            List<WearableItem> resolvedWearables = new List<WearableItem>();
 262
 263            // TODO(Brian): Evaluate using UniTask<T> here instead of Helpers.Promise.
 0264            List<Helpers.Promise<WearableItem>> avatarWearablePromises = new List<Helpers.Promise<WearableItem>>();
 0265            if (model.wearables != null)
 266            {
 0267                for (int i = 0; i < model.wearables.Count; i++)
 268                {
 0269                    avatarWearablePromises.Add(CatalogController.RequestWearable(model.wearables[i]));
 270                }
 271            }
 272
 273            // In this point, all the requests related to the avatar's wearables have been collected and sent to the Cat
 274            // From here we wait for the response of the requested wearables and process them.
 0275            if (avatarBodyPromise != null)
 276            {
 0277                yield return avatarBodyPromise;
 278
 0279                if (!string.IsNullOrEmpty(avatarBodyPromise.error))
 280                {
 0281                    Debug.LogError(avatarBodyPromise.error);
 0282                    loadSoftFailed = true;
 0283                }
 284                else
 285                {
 0286                    resolvedBody = avatarBodyPromise.value;
 0287                    wearablesInUse.Add(avatarBodyPromise.value.id);
 288                }
 289            }
 290
 0291            if (resolvedBody == null)
 292            {
 0293                isLoading = false;
 0294                OnFailEvent?.Invoke(true);
 0295                yield break;
 296            }
 297
 298            // TODO(Brian): Evaluate using UniTask<T> here instead of Helpers.Promise.
 0299            List<Helpers.Promise<WearableItem>> replacementPromises = new List<Helpers.Promise<WearableItem>>();
 300
 0301            foreach (var avatarWearablePromise in avatarWearablePromises)
 302            {
 0303                yield return avatarWearablePromise;
 304
 0305                if (!string.IsNullOrEmpty(avatarWearablePromise.error))
 306                {
 0307                    Debug.LogError(avatarWearablePromise.error);
 0308                    loadSoftFailed = true;
 0309                }
 310                else
 311                {
 0312                    WearableItem wearableItem = avatarWearablePromise.value;
 0313                    wearablesInUse.Add(wearableItem.id);
 314
 0315                    if (wearableItem.GetRepresentation(model.bodyShape) != null)
 316                    {
 0317                        resolvedWearables.Add(wearableItem);
 0318                    }
 319                    else
 320                    {
 0321                        model.wearables.Remove(wearableItem.id);
 0322                        string defaultReplacement = DefaultWearables.GetDefaultWearable(model.bodyShape, wearableItem.da
 0323                        if (!string.IsNullOrEmpty(defaultReplacement))
 324                        {
 0325                            model.wearables.Add(defaultReplacement);
 0326                            replacementPromises.Add(CatalogController.RequestWearable(defaultReplacement));
 327                        }
 328                    }
 329                }
 0330            }
 331
 0332            foreach (var wearablePromise in replacementPromises)
 333            {
 0334                yield return wearablePromise;
 335
 0336                if (!string.IsNullOrEmpty(wearablePromise.error))
 337                {
 0338                    Debug.LogError(wearablePromise.error);
 0339                    loadSoftFailed = true;
 0340                }
 341                else
 342                {
 0343                    WearableItem wearableItem = wearablePromise.value;
 0344                    wearablesInUse.Add(wearableItem.id);
 0345                    resolvedWearables.Add(wearableItem);
 346                }
 0347            }
 348
 0349            bool bodyIsDirty = false;
 0350            if (bodyShapeController != null && bodyShapeController.id != model?.bodyShape)
 351            {
 0352                bodyShapeController.CleanUp();
 0353                bodyShapeController = null;
 0354                bodyIsDirty = true;
 355            }
 356
 0357            if (bodyShapeController == null)
 358            {
 0359                HideAll();
 360
 0361                bodyShapeController = new BodyShapeController(resolvedBody);
 0362                eyesController = FacialFeatureController.CreateDefaultFacialFeature(bodyShapeController.bodyShapeId, Cat
 0363                eyebrowsController = FacialFeatureController.CreateDefaultFacialFeature(bodyShapeController.bodyShapeId,
 0364                mouthController = FacialFeatureController.CreateDefaultFacialFeature(bodyShapeController.bodyShapeId, Ca
 365            }
 366
 367            //TODO(Brian): This logic should be performed in a testeable pure function instead of this inline approach.
 368            //             Moreover, this function should work against data, not wearableController instances.
 0369            bool wearablesIsDirty = false;
 0370            HashSet<string> unusedCategories = new HashSet<string>(Categories.ALL);
 0371            int wearableCount = resolvedWearables.Count;
 0372            for (int index = 0; index < wearableCount; index++)
 373            {
 0374                WearableItem wearable = resolvedWearables[index];
 0375                if (wearable == null)
 376                    continue;
 377
 0378                unusedCategories.Remove(wearable.data.category);
 0379                if (wearableControllers.ContainsKey(wearable))
 380                {
 0381                    if (!wearableControllers[wearable].IsLoadedForBodyShape(bodyShapeController.bodyShapeId))
 0382                        wearableControllers[wearable].CleanUp();
 0383                }
 384                else
 385                {
 0386                    AddWearableController(wearable);
 0387                    if (wearable.data.category != Categories.EYES && wearable.data.category != Categories.MOUTH && weara
 0388                        wearablesIsDirty = true;
 389                }
 390            }
 391
 0392            if ( eyesController == null && !unusedCategories.Contains(Categories.EYES))
 0393                unusedCategories.Add(Categories.EYES);
 394
 0395            if ( mouthController == null && !unusedCategories.Contains(Categories.MOUTH))
 0396                unusedCategories.Add(Categories.MOUTH);
 397
 0398            if ( eyebrowsController == null && !unusedCategories.Contains(Categories.EYEBROWS))
 0399                unusedCategories.Add(Categories.EYEBROWS);
 400
 0401            foreach (var category in unusedCategories)
 402            {
 403                switch (category)
 404                {
 405                    case Categories.EYES:
 0406                        eyesController = FacialFeatureController.CreateDefaultFacialFeature(bodyShapeController.bodyShap
 0407                        break;
 408                    case Categories.MOUTH:
 0409                        mouthController = FacialFeatureController.CreateDefaultFacialFeature(bodyShapeController.bodySha
 0410                        break;
 411                    case Categories.EYEBROWS:
 0412                        eyebrowsController = FacialFeatureController.CreateDefaultFacialFeature(bodyShapeController.body
 413                        break;
 414                }
 415            }
 416
 0417            HashSet<string> hiddenList = WearableItem.CompoundHidesList(bodyShapeController.bodyShapeId, resolvedWearabl
 0418            if (!bodyShapeController.isReady)
 419            {
 0420                bodyShapeController.Load(bodyShapeController.bodyShapeId, transform, OnWearableLoadingSuccess, OnBodySha
 421            }
 422
 0423            foreach (WearableController wearable in wearableControllers.Values)
 424            {
 0425                if (bodyIsDirty)
 0426                    wearable.boneRetargetingDirty = true;
 427
 0428                wearable.Load(bodyShapeController.bodyShapeId, transform, OnWearableLoadingSuccess, x => OnWearableLoadi
 0429                yield return null;
 430            }
 431
 432            // TODO(Brian): Evaluate using UniTask<T> instead of this way.
 0433            yield return new WaitUntil(() => bodyShapeController.isReady && wearableControllers.Values.All(x => x.isRead
 434
 0435            eyesController.Load(bodyShapeController, model.eyeColor);
 0436            eyebrowsController.Load(bodyShapeController, model.hairColor);
 0437            mouthController.Load(bodyShapeController, model.skinColor);
 438
 0439            yield return eyesController;
 0440            yield return eyebrowsController;
 0441            yield return mouthController;
 442
 0443            if (bodyIsDirty || wearablesIsDirty)
 444            {
 0445                OnVisualCue?.Invoke(IAvatarRenderer.VisualCue.Loaded);
 446            }
 447
 448            // TODO(Brian): unusedCategories and hiddenList management is a double negative PITA.
 449            //              The load process should define how the avatar should look like before
 450            //              loading it and put this information in a positive list
 451            //              (i.e. not negative, because leads to double negative checks).
 0452            bodyShapeController.SetActiveParts(unusedCategories.Contains(Categories.LOWER_BODY), unusedCategories.Contai
 0453            bodyShapeController.UpdateVisibility(hiddenList);
 0454            foreach (WearableController wearableController in wearableControllers.Values)
 455            {
 0456                wearableController.UpdateVisibility(hiddenList);
 457            }
 458
 0459            CleanUpUnusedItems();
 460
 0461            isLoading = false;
 462
 0463            SetupHairAndSkinColors();
 0464            SetWearableBones();
 465
 466            // TODO(Brian): Expression and sticker update shouldn't be part of avatar loading code!!!! Refactor me pleas
 0467            UpdateExpression();
 468
 0469            var allRenderers = wearableControllers.SelectMany( x => x.Value.GetRenderers() ).ToList();
 0470            allRenderers.AddRange( bodyShapeController.GetRenderers() );
 0471            bool mergeSuccess = MergeAvatar(allRenderers);
 472
 0473            if (mergeSuccess)
 0474                PrepareGpuSkinning();
 475            else
 0476                loadSoftFailed = true;
 477
 478            // TODO(Brian): The loadSoftFailed flow is too convoluted--you never know which objects are nulled or empty
 479            //              before reaching this branching statement. The failure should be caught with a throw or other
 480            //              proper language feature.
 0481            if (loadSoftFailed)
 482            {
 0483                OnFailEvent?.Invoke(false);
 0484            }
 485            else
 486            {
 0487                OnSuccessEvent?.Invoke();
 488            }
 0489        }
 490
 491        private void PrepareGpuSkinning()
 492        {
 493            // Sample the animation manually and force an update in the GPUSkinning to avoid giant avatars
 0494            animator.SetIdleFrame();
 0495            animator.animation.Sample();
 496
 0497            gpuSkinning = new SimpleGPUSkinning(
 498                avatarMeshCombiner.renderer,
 499                false); // Bind poses are encoded by the AvatarMeshCombiner before making the mesh unreadable.
 500
 0501            gpuSkinningThrottler = new GPUSkinningThrottler(gpuSkinning);
 0502            gpuSkinningThrottler.SetThrottling(gpuSkinningFramesBetweenUpdates);
 0503        }
 504
 505        void SetupHairAndSkinColors()
 506        {
 0507            bodyShapeController.SetupHairAndSkinColors(model.skinColor, model.hairColor);
 508
 0509            foreach ( var wearable in wearableControllers )
 510            {
 0511                wearable.Value.SetupHairAndSkinColors(model.skinColor, model.hairColor);
 512            }
 0513        }
 514
 515        void OnWearableLoadingSuccess(WearableController wearableController)
 516        {
 0517            if (wearableController == null || model == null)
 518            {
 0519                Debug.LogWarning($"WearableSuccess was called wrongly: IsWearableControllerNull=>{wearableController == 
 0520                OnWearableLoadingFail(wearableController, 0);
 521            }
 0522        }
 523
 524        void OnBodyShapeLoadingFail(WearableController wearableController)
 525        {
 0526            Debug.LogError($"Avatar: {model?.name}  -  Failed loading bodyshape: {wearableController?.id}");
 0527            CleanupAvatar();
 0528            OnFailEvent?.Invoke(true);
 0529        }
 530
 531        void OnWearableLoadingFail(WearableController wearableController, int retriesCount = MAX_RETRIES)
 532        {
 0533            if (retriesCount <= 0)
 534            {
 0535                Debug.LogError($"Avatar: {model?.name}  -  Failed loading wearable: {wearableController?.id}");
 0536                CleanupAvatar();
 0537                OnFailEvent?.Invoke(false);
 0538                return;
 539            }
 540
 0541            wearableController.Load(bodyShapeController.id, transform, OnWearableLoadingSuccess, x => OnWearableLoadingF
 0542        }
 543
 544        private void SetWearableBones()
 545        {
 546            // NOTE(Brian): Set bones/rootBone of all wearables to be the same of the baseBody,
 547            //              so all of them are animated together.
 0548            using (var enumerator = wearableControllers.GetEnumerator())
 549            {
 0550                while (enumerator.MoveNext())
 551                {
 0552                    enumerator.Current.Value.SetAnimatorBones(bodyShapeController.bones, bodyShapeController.rootBone);
 553                }
 0554            }
 0555        }
 556
 557        private void UpdateExpression()
 558        {
 0559            SetExpression(model.expressionTriggerId, model.expressionTriggerTimestamp);
 560
 0561            if (lastStickerTimestamp != model.stickerTriggerTimestamp && model.stickerTriggerId != null)
 562            {
 0563                lastStickerTimestamp = model.stickerTriggerTimestamp;
 564
 0565                if ( stickersController != null )
 0566                    stickersController.PlayEmote(model.stickerTriggerId);
 567            }
 0568        }
 569
 570        public void SetExpression(string id, long timestamp)
 571        {
 2572            model.expressionTriggerId = id;
 2573            model.expressionTriggerTimestamp = timestamp;
 2574            animator.SetExpressionValues(id, timestamp);
 2575        }
 576
 577        private void AddWearableController(WearableItem wearable)
 578        {
 0579            if (wearable == null)
 0580                return;
 0581            switch (wearable.data.category)
 582            {
 583                case Categories.EYES:
 0584                    eyesController = new FacialFeatureController(wearable, eyeMaterial);
 0585                    break;
 586                case Categories.EYEBROWS:
 0587                    eyebrowsController = new FacialFeatureController(wearable, eyebrowMaterial);
 0588                    break;
 589                case Categories.MOUTH:
 0590                    mouthController = new FacialFeatureController(wearable, mouthMaterial);
 0591                    break;
 592                case Categories.BODY_SHAPE:
 593                    break;
 594
 595                default:
 0596                    var wearableController = new WearableController(wearable);
 0597                    wearableControllers.Add(wearable, wearableController);
 598                    break;
 599            }
 0600        }
 601
 602        //TODO: Remove/replace once the class is easily mockable.
 603        protected void CopyFrom(AvatarRenderer original)
 604        {
 0605            this.wearableControllers = original.wearableControllers;
 0606            this.mouthController = original.mouthController;
 0607            this.bodyShapeController = original.bodyShapeController;
 0608            this.eyebrowsController = original.eyebrowsController;
 0609            this.eyesController = original.eyesController;
 0610        }
 611
 612        public void SetGOVisibility(bool newVisibility)
 613        {
 614            //NOTE(Brian): Avatar being loaded needs the renderer.enabled as false until the loading finishes.
 615            //             So we can' manipulate the values because it'd show an incomplete avatar. Its easier to just d
 715616            if (gameObject.activeSelf != newVisibility)
 17617                gameObject.SetActive(newVisibility);
 715618        }
 619
 620        public void SetRendererEnabled(bool newVisibility)
 621        {
 0622            if (mainMeshRenderer == null)
 0623                return;
 624
 0625            mainMeshRenderer.enabled = newVisibility;
 0626        }
 627
 628        public void SetImpostorVisibility(bool impostorVisibility)
 629        {
 1312630            if (impostorVisibility && !initializedImpostor)
 0631                InitializeImpostor();
 632
 1312633            impostorRenderer.gameObject.SetActive(impostorVisibility);
 1312634        }
 635
 0636        public void SetImpostorForward(Vector3 newForward) { impostorRenderer.transform.forward = newForward; }
 637
 0638        public void SetImpostorColor(Color newColor) { AvatarRendererHelpers.SetImpostorTintColor(impostorRenderer.mater
 639
 640        public void SetThrottling(int framesBetweenUpdates)
 641        {
 0642            gpuSkinningFramesBetweenUpdates = framesBetweenUpdates;
 0643            gpuSkinningThrottler?.SetThrottling(gpuSkinningFramesBetweenUpdates);
 0644        }
 645
 646        public void SetAvatarFade(float avatarFade)
 647        {
 0648            if (bodyShapeController == null || !bodyShapeController.isReady)
 0649                return;
 650
 0651            Material[] mats = mainMeshRenderer.sharedMaterials;
 0652            for (int j = 0; j < mats.Length; j++)
 653            {
 0654                mats[j].SetFloat(ShaderUtils.DitherFade, avatarFade);
 655            }
 0656        }
 657
 658        public void SetImpostorFade(float impostorFade)
 659        {
 660            //TODO implement dither in Unlit shader
 0661            Color current = impostorRenderer.material.GetColor(BASE_COLOR_PROPERTY);
 0662            current.a = impostorFade;
 0663            impostorRenderer.material.SetColor(BASE_COLOR_PROPERTY, current);
 664
 0665            OnImpostorAlphaValueUpdate?.Invoke(impostorFade);
 0666        }
 667
 668        private void HideAll()
 669        {
 670            // TODO: Cache this somewhere (maybe when the LoadAvatar finishes) instead of fetching this on every call
 0671            Renderer[] renderers = gameObject.GetComponentsInChildren<Renderer>();
 672
 0673            for (int i = 0; i < renderers.Length; i++)
 674            {
 0675                renderers[i].enabled = false;
 676            }
 0677        }
 678
 679        public void SetFacialFeaturesVisible(bool visible)
 680        {
 0681            if (bodyShapeController == null || !bodyShapeController.isReady)
 0682                return;
 683
 0684            if (isLoading)
 0685                return;
 686
 0687            bodyShapeController.SetFacialFeaturesVisible(visible, true);
 0688        }
 689
 690        public void SetSSAOEnabled(bool ssaoEnabled)
 691        {
 0692            if ( isLoading )
 0693                return;
 694
 0695            Material[] mats = mainMeshRenderer.sharedMaterials;
 696
 0697            for (int j = 0; j < mats.Length; j++)
 698            {
 0699                if (ssaoEnabled)
 0700                    mats[j].DisableKeyword("_SSAO_OFF");
 701                else
 0702                    mats[j].EnableKeyword("_SSAO_OFF");
 703            }
 0704        }
 705
 706        private bool MergeAvatar(IEnumerable<SkinnedMeshRenderer> allRenderers)
 707        {
 0708            var renderersToCombine = allRenderers.Where((r) => !r.transform.parent.gameObject.name.Contains("Mask")).ToL
 709
 0710            bool success = avatarMeshCombiner.Combine(
 711                bodyShapeController.upperBodyRenderer,
 712                renderersToCombine.ToArray(),
 713                defaultMaterial);
 714
 0715            if ( success )
 716            {
 0717                avatarMeshCombiner.container.transform.SetParent( transform, true );
 0718                avatarMeshCombiner.container.transform.localPosition = Vector3.zero;
 719            }
 720
 0721            return success;
 722        }
 723
 3002724        private void CleanMergedAvatar() { avatarMeshCombiner.Dispose(); }
 725
 726        private void ResetImpostor()
 727        {
 1582728            if (impostorRenderer == null)
 269729                return;
 730
 1313731            if (bodySnapshotTexturePromise != null)
 0732                AssetPromiseKeeper_Texture.i.Forget(bodySnapshotTexturePromise);
 733
 1313734            AvatarRendererHelpers.ResetImpostor(impostorMeshFilter.mesh, impostorRenderer.material);
 735
 1313736            initializedImpostor = false;
 1313737        }
 738
 739        private void LateUpdate()
 740        {
 20530741            if (gpuSkinning != null && mainMeshRenderer.enabled)
 0742                gpuSkinningThrottler.TryUpdate();
 20530743        }
 744
 745        protected virtual void OnDestroy()
 746        {
 843747            isDestroyed = true;
 843748            CleanupAvatar();
 843749        }
 750    }
 751}