< Summary

Class:DCL.AvatarRenderer
Assembly:AvatarShape
File(s):/tmp/workspace/unity-renderer/unity-renderer/Assets/Scripts/MainScripts/DCL/Components/Avatar/AvatarRenderer.cs
Covered lines:106
Uncovered lines:264
Coverable lines:370
Total lines:774
Line coverage:28.6% (106 of 370)
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%2383.8159012.59%
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%
GetBones()0%6200%

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;
 133129        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<Exception> OnFailEvent;
 47
 48        internal BodyShapeController bodyShapeController;
 133149        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
 133156        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;
 133164        private List<string> wearablesInUse = new List<string>();
 65        private bool isDestroyed = false;
 66
 67        private void Awake()
 68        {
 132669            animator = GetComponent<AvatarAnimatorLegacy>();
 132670            stickersController = GetComponent<StickersController>();
 132671            avatarMeshCombiner = new AvatarMeshCombinerHelper();
 132672            avatarMeshCombiner.prepareMeshForGpuSkinning = true;
 132673            avatarMeshCombiner.uploadMeshToGpu = true;
 74
 132675            if (impostorRenderer != null)
 67076                SetImpostorVisibility(false);
 132677        }
 78
 79        public void ApplyModel(AvatarModel model, Action onSuccess, Action onFail)
 80        {
 14381            if ( this.model != null )
 82            {
 7783                if (model != null && this.model.Equals(model))
 84                {
 4685                    onSuccess?.Invoke();
 4586                    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
 97104            this.model = new AvatarModel();
 97105            this.model.CopyFrom(model);
 106
 97107            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                OnSuccessEvent -= onSuccessWrapper;
 0114            }
 115
 97116            OnSuccessEvent += onSuccessWrapper;
 117
 118            void onFailWrapper(Exception exception)
 119            {
 20120                onFail?.Invoke();
 20121                OnFailEvent -= onFailWrapper;
 20122            }
 123
 97124            OnFailEvent += onFailWrapper;
 125
 97126            isLoading = false;
 127
 97128            if (model == null)
 129            {
 0130                CleanupAvatar();
 0131                this.OnSuccessEvent?.Invoke();
 0132                return;
 133            }
 134
 97135            StopLoadingCoroutines();
 136
 97137            isLoading = true;
 97138            loadCoroutine = CoroutineStarter.Start(LoadAvatar());
 97139        }
 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        {
 2097162            if (loadCoroutine != null)
 97163                CoroutineStarter.Stop(loadCoroutine);
 164
 2097165            loadCoroutine = null;
 2097166        }
 167
 168        public void CleanupAvatar()
 169        {
 2000170            StopLoadingCoroutines();
 2000171            if (!isDestroyed)
 172            {
 674173                SetGOVisibility(true);
 674174                if (impostorRenderer != null)
 674175                    SetImpostorVisibility(false);
 176            }
 177
 2000178            avatarMeshCombiner.Dispose();
 2000179            gpuSkinningThrottler = null;
 2000180            gpuSkinning = null;
 2000181            eyebrowsController?.CleanUp();
 2000182            eyebrowsController = null;
 183
 2000184            eyesController?.CleanUp();
 2000185            eyesController = null;
 186
 2000187            mouthController?.CleanUp();
 2000188            mouthController = null;
 189
 2000190            bodyShapeController?.CleanUp();
 2000191            bodyShapeController = null;
 192
 2000193            using (var iterator = wearableControllers.GetEnumerator())
 194            {
 2000195                while (iterator.MoveNext())
 196                {
 0197                    iterator.Current.Value.CleanUp();
 198                }
 2000199            }
 200
 2000201            wearableControllers.Clear();
 2000202            model = null;
 2000203            isLoading = false;
 2000204            OnFailEvent = null;
 2000205            OnSuccessEvent = null;
 206
 2000207            CleanMergedAvatar();
 208
 2000209            ResetImpostor();
 210
 2000211            CatalogController.RemoveWearablesInUse(wearablesInUse);
 2000212            wearablesInUse.Clear();
 2000213            OnVisualCue?.Invoke(IAvatarRenderer.VisualCue.CleanedUp);
 1954214        }
 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.
 194244            yield return new WaitUntil(() => gameObject.activeSelf);
 245
 21246            bool loadSoftFailed = false;
 247
 21248            WearableItem resolvedBody = null;
 249
 250            // TODO(Brian): Evaluate using UniTask<T> here instead of Helpers.Promise.
 21251            Promise<WearableItem> avatarBodyPromise = null;
 21252            if (!string.IsNullOrEmpty(model.bodyShape))
 253            {
 1254                avatarBodyPromise = CatalogController.RequestWearable(model.bodyShape);
 1255            }
 256            else
 257            {
 20258                OnFailEvent?.Invoke(new AvatarLoadFatalException("bodyShape is null"));
 20259                yield break;
 260            }
 261
 1262            List<WearableItem> resolvedWearables = new List<WearableItem>();
 263
 264            // TODO(Brian): Evaluate using UniTask<T> here instead of Helpers.Promise.
 1265            List<Promise<WearableItem>> avatarWearablePromises = new List<Promise<WearableItem>>();
 1266            if (model.wearables != null)
 267            {
 20268                for (int i = 0; i < model.wearables.Count; i++)
 269                {
 9270                    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.
 1276            if (avatarBodyPromise != null)
 277            {
 1278                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(new AvatarLoadFatalException("Could not resolve body for avatar"));
 0296                yield break;
 297            }
 298
 299            // TODO(Brian): Evaluate using UniTask<T> here instead of Helpers.Promise.
 0300            List<Promise<WearableItem>> replacementPromises = new List<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, error) => OnWear
 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(new Exception("loadSoftFailed: true"));
 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                var message = $"WearableSuccess was called wrongly: IsWearableControllerNull=>{wearableController == nul
 0527                Debug.LogWarning(message);
 0528                OnWearableLoadingFail(wearableController, new Exception(message), 0);
 529            }
 0530        }
 531
 532        void OnBodyShapeLoadingFail(WearableController wearableController, Exception error)
 533        {
 0534            var errorMessage = $"Avatar: {model?.name}  -  Failed loading bodyshape: {wearableController?.id}  -  Except
 0535            Debug.LogError(errorMessage);
 536            // cleaning up the avatar nulls OnFailEvent, so save it in a temporal variable and then execute it
 537            // so the fail stream doesnt die
 0538            var failEventBeforeClearing = OnFailEvent;
 0539            CleanupAvatar();
 0540            failEventBeforeClearing?.Invoke(new AvatarLoadFatalException(errorMessage));
 0541        }
 542
 543        void OnWearableLoadingFail(WearableController wearableController, Exception error, int retriesCount = MAX_RETRIE
 544        {
 0545            if (retriesCount <= 0)
 546            {
 0547                var errorMessage = $"Avatar: {model?.name}  -  Failed loading wearable: {wearableController?.id}  -  Exc
 0548                Debug.LogError(errorMessage);
 549                // cleaning up the avatar nulls OnFailEvent, so save it in a temporal variable and then execute it
 550                // so the fail stream doesnt die
 0551                var failEventBeforeClearing = OnFailEvent;
 0552                CleanupAvatar();
 0553                failEventBeforeClearing?.Invoke(new Exception(errorMessage));
 0554                return;
 555            }
 556
 0557            wearableController.Load(bodyShapeController.id, transform, OnWearableLoadingSuccess, (x, e) => OnWearableLoa
 0558        }
 559
 560        private void SetWearableBones()
 561        {
 562            // NOTE(Brian): Set bones/rootBone of all wearables to be the same of the baseBody,
 563            //              so all of them are animated together.
 0564            using (var enumerator = wearableControllers.GetEnumerator())
 565            {
 0566                while (enumerator.MoveNext())
 567                {
 0568                    enumerator.Current.Value.SetAnimatorBones(bodyShapeController.bones, bodyShapeController.rootBone);
 569                }
 0570            }
 0571        }
 572
 573        private void UpdateExpression()
 574        {
 0575            SetExpression(model.expressionTriggerId, model.expressionTriggerTimestamp);
 576
 0577            if (lastStickerTimestamp != model.stickerTriggerTimestamp && model.stickerTriggerId != null)
 578            {
 0579                lastStickerTimestamp = model.stickerTriggerTimestamp;
 580
 0581                if ( stickersController != null )
 0582                    stickersController.PlaySticker(model.stickerTriggerId);
 583            }
 0584        }
 585
 586        public void SetExpression(string id, long timestamp)
 587        {
 2588            model.expressionTriggerId = id;
 2589            model.expressionTriggerTimestamp = timestamp;
 2590            animator.SetExpressionValues(id, timestamp);
 2591        }
 592
 593        private void AddWearableController(WearableItem wearable)
 594        {
 0595            if (wearable == null)
 0596                return;
 0597            switch (wearable.data.category)
 598            {
 599                case Categories.EYES:
 0600                    eyesController = new FacialFeatureController(wearable, eyeMaterial);
 0601                    break;
 602                case Categories.EYEBROWS:
 0603                    eyebrowsController = new FacialFeatureController(wearable, eyebrowMaterial);
 0604                    break;
 605                case Categories.MOUTH:
 0606                    mouthController = new FacialFeatureController(wearable, mouthMaterial);
 0607                    break;
 608                case Categories.BODY_SHAPE:
 609                    break;
 610
 611                default:
 0612                    var wearableController = new WearableController(wearable);
 0613                    wearableControllers.Add(wearable, wearableController);
 614                    break;
 615            }
 0616        }
 617
 618        //TODO: Remove/replace once the class is easily mockable.
 619        protected void CopyFrom(AvatarRenderer original)
 620        {
 0621            this.wearableControllers = original.wearableControllers;
 0622            this.mouthController = original.mouthController;
 0623            this.bodyShapeController = original.bodyShapeController;
 0624            this.eyebrowsController = original.eyebrowsController;
 0625            this.eyesController = original.eyesController;
 0626        }
 627
 628        public void SetGOVisibility(bool newVisibility)
 629        {
 630            //NOTE(Brian): Avatar being loaded needs the renderer.enabled as false until the loading finishes.
 631            //             So we can' manipulate the values because it'd show an incomplete avatar. Its easier to just d
 717632            if (gameObject.activeSelf != newVisibility)
 41633                gameObject.SetActive(newVisibility);
 717634        }
 635
 636        public void SetRendererEnabled(bool newVisibility)
 637        {
 0638            if (mainMeshRenderer == null)
 0639                return;
 640
 0641            mainMeshRenderer.enabled = newVisibility;
 0642        }
 643
 644        public void SetImpostorVisibility(bool impostorVisibility)
 645        {
 1344646            if (impostorVisibility && !initializedImpostor)
 0647                InitializeImpostor();
 648
 1344649            impostorRenderer.gameObject.SetActive(impostorVisibility);
 1344650        }
 651
 0652        public void SetImpostorForward(Vector3 newForward) { impostorRenderer.transform.forward = newForward; }
 653
 0654        public void SetImpostorColor(Color newColor) { AvatarRendererHelpers.SetImpostorTintColor(impostorRenderer.mater
 655
 656        public void SetThrottling(int framesBetweenUpdates)
 657        {
 0658            gpuSkinningFramesBetweenUpdates = framesBetweenUpdates;
 0659            gpuSkinningThrottler?.SetThrottling(gpuSkinningFramesBetweenUpdates);
 0660        }
 661
 662        public void SetAvatarFade(float avatarFade)
 663        {
 0664            if (bodyShapeController == null || !bodyShapeController.isReady)
 0665                return;
 666
 0667            Material[] mats = mainMeshRenderer.sharedMaterials;
 0668            for (int j = 0; j < mats.Length; j++)
 669            {
 0670                mats[j].SetFloat(ShaderUtils.DitherFade, avatarFade);
 671            }
 672
 0673            OnAvatarAlphaValueUpdate?.Invoke(avatarFade);
 0674        }
 675
 676        public void SetImpostorFade(float impostorFade)
 677        {
 678            //TODO implement dither in Unlit shader
 0679            Color current = impostorRenderer.material.GetColor(BASE_COLOR_PROPERTY);
 0680            current.a = impostorFade;
 0681            impostorRenderer.material.SetColor(BASE_COLOR_PROPERTY, current);
 682
 0683            OnImpostorAlphaValueUpdate?.Invoke(impostorFade);
 0684        }
 685
 686        private void HideAll()
 687        {
 688            // TODO: Cache this somewhere (maybe when the LoadAvatar finishes) instead of fetching this on every call
 0689            Renderer[] renderers = gameObject.GetComponentsInChildren<Renderer>();
 690
 0691            for (int i = 0; i < renderers.Length; i++)
 692            {
 0693                renderers[i].enabled = false;
 694            }
 0695        }
 696
 697        public void SetFacialFeaturesVisible(bool visible)
 698        {
 0699            if (bodyShapeController == null || !bodyShapeController.isReady)
 0700                return;
 701
 0702            if (isLoading)
 0703                return;
 704
 0705            bodyShapeController.SetFacialFeaturesVisible(visible, true);
 0706        }
 707
 708        public void SetSSAOEnabled(bool ssaoEnabled)
 709        {
 0710            if ( isLoading )
 0711                return;
 712
 0713            Material[] mats = mainMeshRenderer.sharedMaterials;
 714
 0715            for (int j = 0; j < mats.Length; j++)
 716            {
 0717                if (ssaoEnabled)
 0718                    mats[j].DisableKeyword("_SSAO_OFF");
 719                else
 0720                    mats[j].EnableKeyword("_SSAO_OFF");
 721            }
 0722        }
 723
 724        private bool MergeAvatar(IEnumerable<SkinnedMeshRenderer> allRenderers)
 725        {
 0726            var renderersToCombine = allRenderers.Where((r) => !r.transform.parent.gameObject.name.Contains("Mask")).ToL
 727
 0728            var featureFlags = DataStore.i.featureFlags.flags.Get();
 0729            avatarMeshCombiner.useCullOpaqueHeuristic = featureFlags.IsFeatureEnabled("cull-opaque-heuristic");
 730
 0731            bool success = avatarMeshCombiner.Combine(
 732                bodyShapeController.upperBodyRenderer,
 733                renderersToCombine.ToArray(),
 734                defaultMaterial);
 735
 0736            if ( success )
 737            {
 0738                avatarMeshCombiner.container.transform.SetParent( transform, true );
 0739                avatarMeshCombiner.container.transform.localPosition = Vector3.zero;
 740            }
 741
 0742            return success;
 743        }
 744
 4000745        private void CleanMergedAvatar() { avatarMeshCombiner.Dispose(); }
 746
 747        private void ResetImpostor()
 748        {
 2097749            if (impostorRenderer == null)
 752750                return;
 751
 1345752            if (bodySnapshotTexturePromise != null)
 0753                AssetPromiseKeeper_Texture.i.Forget(bodySnapshotTexturePromise);
 754
 1345755            AvatarRendererHelpers.ResetImpostor(impostorMeshFilter.mesh, impostorRenderer.material);
 756
 1345757            initializedImpostor = false;
 1345758        }
 759
 760        private void LateUpdate()
 761        {
 8630762            if (gpuSkinning != null && mainMeshRenderer.enabled)
 0763                gpuSkinningThrottler.TryUpdate();
 8630764        }
 765
 766        protected virtual void OnDestroy()
 767        {
 1326768            isDestroyed = true;
 1326769            CleanupAvatar();
 1326770        }
 771
 0772        public Transform[] GetBones() => bodyShapeController?.bones;
 773    }
 774}