From 3c6ea616b91a7959a7d6d9f585717dc996286f46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ha=C3=AF=7E?= Date: Sat, 21 Dec 2024 23:50:56 +0100 Subject: [PATCH 1/2] Make SkinnedMeshes of the local player head cast shadows: - The SkinnedMeshes of the local player that reference any bone under the Head bone should now cast shadows of the head onto the world and other players. - SkinnedMeshes placed directly under the Head bone without any bone reference are not supported in this version. - MeshRenderers placed directly under the Head bone are not supported in this version. - This implementation: - Evaluates at runtime. - Creates a copy of all the transforms under the Head hierarchy. - Creates copies of SkinnedMeshes that reference any bone under the Head. - Bones under Head will reference the aforementionned copy. - Bones not under Head will reference a zero-scale bone under the neck. - Those SkinnedMeshes are set to Shadow Only. - This implementation currently uses "Update When Offscreen" OFF and an off-world "Bounds" value to prevent the shadow from displaying when rendering the local player avatar in third person. - This implementation was chosen because it's currently unclear how to manage the camera layers, and should be discussed further. - Just before any camera is rendered, once per frame, we copy the transforms, SkinnedMesh component enabled-ness, and blendshapes, so that the shadow reflects the visual state of the original SkinnedMesh. --- .../Avatar/BasisAvatarFactory.cs | 14 +- .../Basis Framework/Avatar/BasisLayer.cs | 9 + .../Basis Framework/Avatar/BasisLayer.cs.meta | 3 + .../Devices/Desktop/BasisLocalInputActions.cs | 3 +- .../Drivers/BasisLocalCameraDriver.cs | 6 +- .../Players/BasisHeadShadowDriver.cs | 282 ++++++++++++++++++ .../Players/BasisHeadShadowDriver.cs.meta | 3 + .../Players/BasisLocalPlayer.cs | 5 +- 8 files changed, 315 insertions(+), 10 deletions(-) create mode 100644 Basis/Packages/Basis Framework/Avatar/BasisLayer.cs create mode 100644 Basis/Packages/Basis Framework/Avatar/BasisLayer.cs.meta create mode 100644 Basis/Packages/Basis Framework/Players/BasisHeadShadowDriver.cs create mode 100644 Basis/Packages/Basis Framework/Players/BasisHeadShadowDriver.cs.meta diff --git a/Basis/Packages/Basis Framework/Avatar/BasisAvatarFactory.cs b/Basis/Packages/Basis Framework/Avatar/BasisAvatarFactory.cs index 2ce189083..6a25cf804 100644 --- a/Basis/Packages/Basis Framework/Avatar/BasisAvatarFactory.cs +++ b/Basis/Packages/Basis Framework/Avatar/BasisAvatarFactory.cs @@ -7,13 +7,14 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using Basis.Scripts.BasisSdk.Helpers; using UnityEngine; using UnityEngine.AddressableAssets; namespace Basis.Scripts.Avatar { public static class BasisAvatarFactory { - public static BasisLoadableBundle LoadingAvatar = new BasisLoadableBundle() + public static BasisLoadableBundle LoadingAvatar = new BasisLoadableBundle() { BasisBundleInformation = new BasisBundleInformation() { @@ -180,7 +181,7 @@ private static void InitializePlayerAvatar(BasisPlayer Player, GameObject Output localPlayer.InitalizeIKCalibration(localPlayer.AvatarDriver); for (int Index = 0; Index < Avatar.Renders.Length; Index++) { - Avatar.Renders[Index].gameObject.layer = 6; + Avatar.Renders[Index].gameObject.layer = BasisLayer.LocalPlayerAvatar; } Avatar.OnAvatarReady?.Invoke(true); } @@ -191,7 +192,7 @@ private static void InitializePlayerAvatar(BasisPlayer Player, GameObject Output remotePlayer.InitalizeIKCalibration(remotePlayer.RemoteAvatarDriver); for (int Index = 0; Index < Avatar.Renders.Length; Index++) { - Avatar.Renders[Index].gameObject.layer = 7; + Avatar.Renders[Index].gameObject.layer = BasisLayer.RemotePlayerAvatar; } Avatar.OnAvatarReady?.Invoke(false); } @@ -251,7 +252,7 @@ public static void LoadLoadingAvatar(BasisPlayer Player, string LoadingAvatarToU Player.InitalizeIKCalibration(BasisLocalPlayer.AvatarDriver); for (int Index = 0; Index < RenderCount; Index++) { - Avatar.Renders[Index].gameObject.layer = 6; + Avatar.Renders[Index].gameObject.layer = BasisLayer.LocalPlayerAvatar; } } else @@ -262,7 +263,7 @@ public static void LoadLoadingAvatar(BasisPlayer Player, string LoadingAvatarToU Player.InitalizeIKCalibration(BasisRemotePlayer.RemoteAvatarDriver); for (int Index = 0; Index < RenderCount; Index++) { - Avatar.Renders[Index].gameObject.layer = 7; + Avatar.Renders[Index].gameObject.layer = BasisLayer.RemotePlayerAvatar; } } } @@ -305,6 +306,9 @@ public static void CreateLocal(BasisLocalPlayer Player) Debug.LogError("Missing LocalPlayer or Avatar"); return; } + if (!Player.HeadShadowDriver) Player.HeadShadowDriver = BasisHelpers.GetOrAddComponent(Player.gameObject); + + Player.HeadShadowDriver.Initialize(Player.Avatar); Player.AvatarDriver.InitialLocalCalibration(Player); } diff --git a/Basis/Packages/Basis Framework/Avatar/BasisLayer.cs b/Basis/Packages/Basis Framework/Avatar/BasisLayer.cs new file mode 100644 index 000000000..8f01d6b1f --- /dev/null +++ b/Basis/Packages/Basis Framework/Avatar/BasisLayer.cs @@ -0,0 +1,9 @@ +namespace Basis.Scripts.Avatar +{ + public class BasisLayer + { + // TODO: These eventually need to be configurable, with values to be chosen by the framework consumer. + public const int LocalPlayerAvatar = 6; + public const int RemotePlayerAvatar = 7; + } +} diff --git a/Basis/Packages/Basis Framework/Avatar/BasisLayer.cs.meta b/Basis/Packages/Basis Framework/Avatar/BasisLayer.cs.meta new file mode 100644 index 000000000..30506c717 --- /dev/null +++ b/Basis/Packages/Basis Framework/Avatar/BasisLayer.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: b2602b63ea984c5f8b58e86f496b10e3 +timeCreated: 1734820085 \ No newline at end of file diff --git a/Basis/Packages/Basis Framework/Device Management/Devices/Desktop/BasisLocalInputActions.cs b/Basis/Packages/Basis Framework/Device Management/Devices/Desktop/BasisLocalInputActions.cs index ee77944d7..b7adecd93 100644 --- a/Basis/Packages/Basis Framework/Device Management/Devices/Desktop/BasisLocalInputActions.cs +++ b/Basis/Packages/Basis Framework/Device Management/Devices/Desktop/BasisLocalInputActions.cs @@ -67,6 +67,7 @@ private void OnBeforeRender() { BasisLocalPlayer.Instance.LocalBoneDriver.SimulateAndApply(Time.timeAsDouble,Time.deltaTime); AfterAvatarChanges?.Invoke(); + BasisLocalPlayer.Instance.HeadShadowDriver.PrepareThisFrame(); } public void Update() @@ -304,4 +305,4 @@ public void RunCancelled() } } } -} \ No newline at end of file +} diff --git a/Basis/Packages/Basis Framework/Drivers/BasisLocalCameraDriver.cs b/Basis/Packages/Basis Framework/Drivers/BasisLocalCameraDriver.cs index fb021affe..8d6b0e203 100644 --- a/Basis/Packages/Basis Framework/Drivers/BasisLocalCameraDriver.cs +++ b/Basis/Packages/Basis Framework/Drivers/BasisLocalCameraDriver.cs @@ -46,7 +46,7 @@ public class BasisLocalCameraDriver : MonoBehaviour public Vector3 largerScale; public static Vector3 LeftEye; public static Vector3 RightEye; - + public Color UnMutedMutedIconColorActive = Color.white; public Color UnMutedMutedIconColorInactive = Color.grey; @@ -297,6 +297,7 @@ public void BeginCameraRendering(ScriptableRenderContext context, Camera Camera) { if (Camera.GetInstanceID() == CameraInstanceID) { + LocalPlayer.HeadShadowDriver.BeforeRenderFirstPerson(); ScaleheadToZero(); if (CameraData.allowXRRendering) { @@ -310,6 +311,7 @@ public void BeginCameraRendering(ScriptableRenderContext context, Camera Camera) } else { + LocalPlayer.HeadShadowDriver.BeforeRenderThirdPerson(); ScaleHeadToNormal(); } } @@ -341,4 +343,4 @@ Vector3 CalculatePosition(Vector2 size, Vector3 percentage) return offset + center; } } -} \ No newline at end of file +} diff --git a/Basis/Packages/Basis Framework/Players/BasisHeadShadowDriver.cs b/Basis/Packages/Basis Framework/Players/BasisHeadShadowDriver.cs new file mode 100644 index 000000000..c64ed580d --- /dev/null +++ b/Basis/Packages/Basis Framework/Players/BasisHeadShadowDriver.cs @@ -0,0 +1,282 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Basis.Scripts.Avatar; +using UnityEngine; +using UnityEngine.Rendering; + +namespace Basis.Scripts.BasisSdk.Players +{ + public class BasisHeadShadowDriver : MonoBehaviour + { + private Transform _headNullable; + + private MeshRenderer[] _meshRenderersUnderHeadBoneOrZero; + private SkinnedMeshRenderer[] _skinnedMeshesOnAvatarThatDependOnHead; + private List _transformsUnderHeadBoneOrZero; + + private int[] _skinnedMeshBlendShapeCount; + + private Transform _nonHeadAndNonNeckDisappearer; + private SkinnedMeshRenderer[] _copiesOfSkinnedMeshes; + private Transform[] _copiesOfTransformsUnderHeadBone; + + private bool _isShadowNecessary; + + private static readonly Bounds DoNotRenderBounds = new(Vector3.one * 999_999_999, Vector3.zero); + + public void Initialize(BasisAvatar avatar) + { + _headNullable = avatar.Animator is { } animator && animator + && animator.GetBoneTransform(HumanBodyBones.Head) is { } head && head + ? head : null; + if (_headNullable == null) + { + Debug.Log("There was Head bone to generate the shadow clone (This is not a problem)"); + } + + _meshRenderersUnderHeadBoneOrZero = _headNullable ? _headNullable.GetComponentsInChildren(true) : Array.Empty(); + _transformsUnderHeadBoneOrZero = _headNullable ? _headNullable.GetComponentsInChildren(true).ToList() : new List(); + + // TODO: Need to handle special case when there is a SMR under the Head hierarchy, which may have no bones in it. + _skinnedMeshesOnAvatarThatDependOnHead = avatar.GetComponentsInChildren(true) + // Ignore SMRs that don't have a mesh. + .Where(meshRenderer => meshRenderer.sharedMesh) + // Intersect is lazily evaluated, so Any will stop when the first element in common is found. + .Where(HasAnyBoneThatRequiresBonesUnderHeadHierarchy) + .ToArray(); + + _isShadowNecessary = _headNullable && (_skinnedMeshesOnAvatarThatDependOnHead.Length > 0 || _meshRenderersUnderHeadBoneOrZero.Length > 0); + if (_isShadowNecessary) + { + // Head can't be null past this point. + var neck = _headNullable.parent; + + _nonHeadAndNonNeckDisappearer = new GameObject("NonHeadAndNonNeckDisappearer") + { + transform = { position = neck.position, rotation = neck.rotation, localScale = Vector3.zero } + }.transform; + _nonHeadAndNonNeckDisappearer.SetParent(neck, true); + + // Create copies + CloneHead(); + + _copiesOfSkinnedMeshes = new SkinnedMeshRenderer[_skinnedMeshesOnAvatarThatDependOnHead.Length]; + _skinnedMeshBlendShapeCount = new int[_skinnedMeshesOnAvatarThatDependOnHead.Length]; + for (var index = 0; index < _skinnedMeshesOnAvatarThatDependOnHead.Length; index++) + { + var originalSmr = _skinnedMeshesOnAvatarThatDependOnHead[index]; + + var copy = new GameObject(NameOfShadowCopy(originalSmr.name)) + { + transform = { parent = originalSmr.transform, localPosition = Vector3.zero, localRotation = Quaternion.identity, localScale = Vector3.one } + }; + copy.SetActive(false); + copy.layer = BasisLayer.LocalPlayerAvatar; + + var smrCopy = copy.AddComponent(); + _copiesOfSkinnedMeshes[index] = smrCopy; + + smrCopy.sharedMesh = originalSmr.sharedMesh; + smrCopy.bones = ProduceNewBoneArrayReferencingShadowCopyHeadBones(originalSmr.bones); + + smrCopy.localBounds = originalSmr.localBounds; + smrCopy.quality = originalSmr.quality; + smrCopy.updateWhenOffscreen = originalSmr.updateWhenOffscreen; + smrCopy.rootBone = originalSmr.rootBone; + + smrCopy.sharedMaterials = originalSmr.sharedMaterials; + + smrCopy.shadowCastingMode = ShadowCastingMode.ShadowsOnly; + smrCopy.receiveShadows = false; + + smrCopy.lightProbeUsage = LightProbeUsage.Off; + smrCopy.probeAnchor = originalSmr.probeAnchor; + + smrCopy.skinnedMotionVectors = originalSmr.skinnedMotionVectors; + smrCopy.allowOcclusionWhenDynamic = originalSmr.allowOcclusionWhenDynamic; + smrCopy.renderingLayerMask = originalSmr.renderingLayerMask; + + _skinnedMeshBlendShapeCount[index] = originalSmr.sharedMesh.blendShapeCount; + + copy.SetActive(true); + } + + // TODO: It would be smarter to let MeshRenderers always display, but: + // - Set it to render Shadow Only when rendering in first-person. + // - Set it to render Mesh and Shadow when rendering in third-person. + } + else + { + _copiesOfTransformsUnderHeadBone = Array.Empty(); + _copiesOfSkinnedMeshes = Array.Empty(); + } + } + + public void PrepareThisFrame() + { + if (!_isShadowNecessary) return; + + for (var index = 0; index < _skinnedMeshesOnAvatarThatDependOnHead.Length; index++) + { + var smr = _skinnedMeshesOnAvatarThatDependOnHead[index]; + if (smr) // Handle the remote possibility that the original SkinnedMeshRenderer may have been deleted by an outside system. + { + var copy = _copiesOfSkinnedMeshes[index]; + if (copy) // Handle the possibility that our copy of the SkinnedMeshRenderer may have been deleted by an outside system. + { + if (smr.enabled != copy.enabled) copy.enabled = smr.enabled; + + // We want the copy to be Inactive when the original or any of its parents is Inactive, + // hence the discrepancy between activeInHierarchy and self. + var isActiveInHierarchy = smr.gameObject.activeInHierarchy; + if (isActiveInHierarchy != copy.gameObject.activeSelf) + { + copy.gameObject.SetActive(isActiveInHierarchy); + } + + var blendShapeCount = _skinnedMeshBlendShapeCount[index]; + for (var blendShapeIndex = 0; blendShapeIndex < blendShapeCount; blendShapeIndex++) + { + copy.SetBlendShapeWeight(blendShapeIndex, smr.GetBlendShapeWeight(blendShapeIndex)); + } + } + } + else + { + if (_copiesOfSkinnedMeshes[index]) + { + Destroy(_copiesOfSkinnedMeshes[index].gameObject); + } + _copiesOfSkinnedMeshes[index] = null; + } + } + + for (var index = 0; index < _transformsUnderHeadBoneOrZero.Count; index++) + { + var from = _transformsUnderHeadBoneOrZero[index]; + var to = _copiesOfTransformsUnderHeadBone[index]; + + if (from && to) + { + to.localPosition = from.localPosition; + to.localRotation = from.localRotation; + to.localScale = from.localScale; + } + } + } + + public void BeforeRenderFirstPerson() + { + if (!_isShadowNecessary) return; + + // Made shadow visible. + // There may be better ways to do this. + for (var index = 0; index < _copiesOfSkinnedMeshes.Length; index++) + { + var original = _skinnedMeshesOnAvatarThatDependOnHead[index]; + var copy = _copiesOfSkinnedMeshes[index]; + + // Handle the possibility of runtime removal by an external system. + if (original && copy) + { + copy.updateWhenOffscreen = original.updateWhenOffscreen; + copy.bounds = original.bounds; + } + } + } + + public void BeforeRenderThirdPerson() + { + if (!_isShadowNecessary) return; + + // Made shadow invisible, so that it doesn't cast shadow on the thing it's trying to copy. + // There may be better ways to do this. + foreach (var copy in _copiesOfSkinnedMeshes) + { + // Handle the possibility of runtime removal by an external system. + if (copy) + { + copy.updateWhenOffscreen = false; + copy.bounds = DoNotRenderBounds; + } + } + } + + private Transform[] ProduceNewBoneArrayReferencingShadowCopyHeadBones(Transform[] originalSmrBones) + { + var newBoneArray = new Transform[originalSmrBones.Length]; + for (var index = 0; index < originalSmrBones.Length; index++) + { + var originalBone = originalSmrBones[index]; + var indexOfBoneOrMinus = _transformsUnderHeadBoneOrZero.IndexOf(originalBone); + if (indexOfBoneOrMinus != -1) + { + newBoneArray[index] = _copiesOfTransformsUnderHeadBone[indexOfBoneOrMinus]; + } + else + { + newBoneArray[index] = _nonHeadAndNonNeckDisappearer; + } + } + + return newBoneArray; + } + + private void CloneHead() + { + if (_headNullable == null) + { + _copiesOfTransformsUnderHeadBone = Array.Empty(); + return; + } + + var copies = new Transform[_transformsUnderHeadBoneOrZero.Count]; + for (var index = 0; index < _transformsUnderHeadBoneOrZero.Count; index++) + { + var current = _transformsUnderHeadBoneOrZero[index]; + var copy = new GameObject(NameOfShadowCopy(current.name)) + { + transform = { position = current.position, rotation = current.rotation, localScale = current.localScale } + }; + copies[index] = copy.transform; + } + + for (var index = 0; index < _transformsUnderHeadBoneOrZero.Count; index++) + { + var current = _transformsUnderHeadBoneOrZero[index]; + var copy = copies[index]; + + var indexOfParentOrMinus = _transformsUnderHeadBoneOrZero.IndexOf(current.parent); + if (indexOfParentOrMinus < 0) + { + // The parent of the Head bone can't be found, so it must be the neck bone. + copy.SetParent(_headNullable.parent, true); + } + else + { + copy.SetParent(copies[indexOfParentOrMinus], true); + } + } + + _copiesOfTransformsUnderHeadBone = copies; + } + + private static string NameOfShadowCopy(string name) + { + return $"_{name}_ShadowCopy"; + } + + private bool HasAnyBoneThatRequiresBonesUnderHeadHierarchy(SkinnedMeshRenderer meshRenderer) + { + // This does not exclude skinned mesh that have something under the Head bone without actually requiring it + // (it would be better if it did, but that's probably a task better done at avatar build time). + return AsSafeSet(meshRenderer.bones).Intersect(_transformsUnderHeadBoneOrZero).Any(); + } + + private static HashSet AsSafeSet(Transform[] transformsWithNullsAndDuplicates) + { + return new HashSet(transformsWithNullsAndDuplicates.Where(t => t != null)); + } + } +} diff --git a/Basis/Packages/Basis Framework/Players/BasisHeadShadowDriver.cs.meta b/Basis/Packages/Basis Framework/Players/BasisHeadShadowDriver.cs.meta new file mode 100644 index 000000000..4a0128daf --- /dev/null +++ b/Basis/Packages/Basis Framework/Players/BasisHeadShadowDriver.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 891f1a68105e46528d49269d5efeac11 +timeCreated: 1734813283 \ No newline at end of file diff --git a/Basis/Packages/Basis Framework/Players/BasisLocalPlayer.cs b/Basis/Packages/Basis Framework/Players/BasisLocalPlayer.cs index 5f9a244d5..ffbcbe61b 100644 --- a/Basis/Packages/Basis Framework/Players/BasisLocalPlayer.cs +++ b/Basis/Packages/Basis Framework/Players/BasisLocalPlayer.cs @@ -34,13 +34,14 @@ public class BasisLocalPlayer : BasisPlayer /// /// the bool when true is the final size /// the bool when false is not the final size - /// use the bool to + /// use the bool to /// public Action OnPlayersHeightChanged; public BasisLocalBoneDriver LocalBoneDriver; public BasisLocalAvatarDriver AvatarDriver; // public BasisFootPlacementDriver FootPlacementDriver; public BasisAudioAndVisemeDriver VisemeDriver; + public BasisHeadShadowDriver HeadShadowDriver; [SerializeField] public LayerMask GroundMask; public static string LoadFileNameAndExtension = "LastUsedAvatar.BAS"; @@ -210,4 +211,4 @@ private void OnPausedEvent(bool IsPaused) } } } -} \ No newline at end of file +} From fa7cfcf948829b218ccc27fa7261bc61e8c4e522 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ha=C3=AF=7E?= Date: Sun, 22 Dec 2024 22:08:18 +0100 Subject: [PATCH 2/2] Handle case where a mirror is ON: - Make object state after rendering camera more consistent: - When first-person camera finishes rendering: - rescale head back to one. - make first-person head shadow invisible. - This fixes an issue where having a mirror turned on would cause the head to be scaled to zero, which was messing the head shadow processing on the next frame. - Defensively protect access to the HeadShadowDriver field, which is null for a short time window on application load, but non-null of the time. --- .../Devices/Desktop/BasisLocalInputActions.cs | 6 ++- .../Drivers/BasisLocalCameraDriver.cs | 40 ++++++++++++++++++- .../Players/BasisHeadShadowDriver.cs | 33 ++++++++++++--- 3 files changed, 71 insertions(+), 8 deletions(-) diff --git a/Basis/Packages/Basis Framework/Device Management/Devices/Desktop/BasisLocalInputActions.cs b/Basis/Packages/Basis Framework/Device Management/Devices/Desktop/BasisLocalInputActions.cs index b7adecd93..46c3bee6e 100644 --- a/Basis/Packages/Basis Framework/Device Management/Devices/Desktop/BasisLocalInputActions.cs +++ b/Basis/Packages/Basis Framework/Device Management/Devices/Desktop/BasisLocalInputActions.cs @@ -67,7 +67,11 @@ private void OnBeforeRender() { BasisLocalPlayer.Instance.LocalBoneDriver.SimulateAndApply(Time.timeAsDouble,Time.deltaTime); AfterAvatarChanges?.Invoke(); - BasisLocalPlayer.Instance.HeadShadowDriver.PrepareThisFrame(); + + // Defensive check, this only seems to be null when the first avatar is not loaded yet. + // May be fixable by updating the prefab so that it's always defined by default. + if (BasisLocalPlayer.Instance.HeadShadowDriver) + BasisLocalPlayer.Instance.HeadShadowDriver.PrepareThisFrame(); } public void Update() diff --git a/Basis/Packages/Basis Framework/Drivers/BasisLocalCameraDriver.cs b/Basis/Packages/Basis Framework/Drivers/BasisLocalCameraDriver.cs index 8d6b0e203..876ec68ba 100644 --- a/Basis/Packages/Basis Framework/Drivers/BasisLocalCameraDriver.cs +++ b/Basis/Packages/Basis Framework/Drivers/BasisLocalCameraDriver.cs @@ -69,6 +69,7 @@ public void OnEnable() MicrophoneRecorder.MainThreadOnHasAudio += MicrophoneTransmitting; MicrophoneRecorder.MainThreadOnHasSilence += MicrophoneNotTransmitting; RenderPipelineManager.beginCameraRendering += BeginCameraRendering; + RenderPipelineManager.endCameraRendering += EndCameraRendering; BasisDeviceManagement.Instance.OnBootModeChanged += OnModeSwitch; BasisLocalPlayer.Instance.OnPlayersHeightChanged += OnHeightChanged; InstanceExists?.Invoke(); @@ -285,6 +286,7 @@ public void OnDisable() if (HasEvents) { RenderPipelineManager.beginCameraRendering -= BeginCameraRendering; + RenderPipelineManager.endCameraRendering -= BeginCameraRendering; BasisDeviceManagement.Instance.OnBootModeChanged -= OnModeSwitch; MicrophoneRecorder.MainThreadOnHasAudio -= MicrophoneTransmitting; MicrophoneRecorder.MainThreadOnHasSilence -= MicrophoneNotTransmitting; @@ -297,7 +299,11 @@ public void BeginCameraRendering(ScriptableRenderContext context, Camera Camera) { if (Camera.GetInstanceID() == CameraInstanceID) { - LocalPlayer.HeadShadowDriver.BeforeRenderFirstPerson(); + // Defensive check, this only seems to be null when the first avatar is not loaded yet. + // May be fixable by updating the prefab so that it's always defined by default. + if (LocalPlayer.HeadShadowDriver) + LocalPlayer.HeadShadowDriver.BeforeRenderFirstPerson(); + ScaleheadToZero(); if (CameraData.allowXRRendering) { @@ -311,11 +317,41 @@ public void BeginCameraRendering(ScriptableRenderContext context, Camera Camera) } else { - LocalPlayer.HeadShadowDriver.BeforeRenderThirdPerson(); + // Defensive check, this only seems to be null when the first avatar is not loaded yet. + // May be fixable by updating the prefab so that it's always defined by default. + if (LocalPlayer.HeadShadowDriver) + LocalPlayer.HeadShadowDriver.BeforeRenderThirdPerson(); + ScaleHeadToNormal(); } } } + + private void EndCameraRendering(ScriptableRenderContext context, Camera Camera) + { + if (LocalPlayer.HasAvatarDriver && LocalPlayer.AvatarDriver.References.Hashead) + { + if (Camera.GetInstanceID() == CameraInstanceID) + { + // Rescale head to avoid making the end state of the avatar + // dependent on the last rendered camera. + ScaleHeadToNormal(); + + // Defensive check, this only seems to be null when the first avatar is not loaded yet. + // May be fixable by updating the prefab so that it's always defined by default. + if (LocalPlayer.HeadShadowDriver) + LocalPlayer.HeadShadowDriver.AfterRenderFirstPerson(); + } + else + { + // Defensive check, this only seems to be null when the first avatar is not loaded yet. + // May be fixable by updating the prefab so that it's always defined by default. + if (LocalPlayer.HeadShadowDriver) + LocalPlayer.HeadShadowDriver.AfterRenderThirdPerson(); + } + } + } + public void ScaleHeadToNormal() { if (LocalPlayer.AvatarDriver.References.head.localScale != LocalPlayer.AvatarDriver.HeadScale) diff --git a/Basis/Packages/Basis Framework/Players/BasisHeadShadowDriver.cs b/Basis/Packages/Basis Framework/Players/BasisHeadShadowDriver.cs index c64ed580d..4c2540115 100644 --- a/Basis/Packages/Basis Framework/Players/BasisHeadShadowDriver.cs +++ b/Basis/Packages/Basis Framework/Players/BasisHeadShadowDriver.cs @@ -170,7 +170,33 @@ public void BeforeRenderFirstPerson() { if (!_isShadowNecessary) return; - // Made shadow visible. + MakeShadowVisible(); + } + + public void AfterRenderFirstPerson() + { + if (!_isShadowNecessary) return; + + // Make shadow invisible to avoid making the end state of the avatar + // dependent on the last rendered camera. + MakeShadowInvisible(); + } + + public void BeforeRenderThirdPerson() + { + if (!_isShadowNecessary) return; + + MakeShadowInvisible(); + } + + public void AfterRenderThirdPerson() + { + if (!_isShadowNecessary) return; + + } + + private void MakeShadowVisible() + { // There may be better ways to do this. for (var index = 0; index < _copiesOfSkinnedMeshes.Length; index++) { @@ -186,11 +212,8 @@ public void BeforeRenderFirstPerson() } } - public void BeforeRenderThirdPerson() + private void MakeShadowInvisible() { - if (!_isShadowNecessary) return; - - // Made shadow invisible, so that it doesn't cast shadow on the thing it's trying to copy. // There may be better ways to do this. foreach (var copy in _copiesOfSkinnedMeshes) {