using System.Collections; using System.Collections.Generic; using Unity.Profiling; using UnityEngine; using UnityEngine.Profiling; using VRC.SDKBase; using VRC.SDKBase.Validation; namespace VRCSDK2.Validation { public static class AvatarValidation { private const int ENFORCE_AUDIO_SOURCE_GAMEOBJECTS_PER_FRAME = 50; private const int ENFORCE_STATIONS_GAMEOBJECTS_PER_FRAME = 50; private const int MAX_STATIONS_PER_AVATAR = 6; private const float MAX_STATION_ACTIVATE_DISTANCE = 0f; private const float MAX_STATION_LOCATION_DISTANCE = 2f; private const float MAX_STATION_COLLIDER_DIMENSION = 2f; private static ProfilerMarker _clampRenderQueuesProfilerMarker = new ProfilerMarker("AvatarValidation.ClampRenderQueues"); private static readonly List _clampRenderQueuesMaterialsTempList = new List(); public static readonly string[] ComponentTypeWhiteListCommon = new string[] { "UnityEngine.Transform", "UnityEngine.Animator", "VRC.Core.PipelineManager", #if !VRC_CLIENT "VRC.Core.PipelineSaver", #endif "RootMotion.FinalIK.IKExecutionOrder", "RootMotion.FinalIK.VRIK", "RootMotion.FinalIK.FullBodyBipedIK", "RootMotion.FinalIK.LimbIK", "RootMotion.FinalIK.AimIK", "RootMotion.FinalIK.BipedIK", "RootMotion.FinalIK.GrounderIK", "RootMotion.FinalIK.GrounderFBBIK", "RootMotion.FinalIK.GrounderVRIK", "RootMotion.FinalIK.GrounderQuadruped", "RootMotion.FinalIK.TwistRelaxer", "RootMotion.FinalIK.ShoulderRotator", "RootMotion.FinalIK.FBBIKArmBending", "RootMotion.FinalIK.FBBIKHeadEffector", "RootMotion.FinalIK.FABRIK", "RootMotion.FinalIK.FABRIKChain", "RootMotion.FinalIK.FABRIKRoot", "RootMotion.FinalIK.CCDIK", "RootMotion.FinalIK.RotationLimit", "RootMotion.FinalIK.RotationLimitHinge", "RootMotion.FinalIK.RotationLimitPolygonal", "RootMotion.FinalIK.RotationLimitSpline", "UnityEngine.SkinnedMeshRenderer", "LimbIK", // our limbik based on Unity ik "LoadingAvatarTextureAnimation", "UnityEngine.MeshFilter", "UnityEngine.MeshRenderer", "UnityEngine.Animation", "UnityEngine.ParticleSystem", "UnityEngine.ParticleSystemRenderer", #if UNITY_STANDALONE "DynamicBone", "DynamicBoneCollider", #endif "UnityEngine.TrailRenderer", #if UNITY_STANDALONE "UnityEngine.Cloth", "UnityEngine.Light", "UnityEngine.BoxCollider", "UnityEngine.SphereCollider", "UnityEngine.CapsuleCollider", "UnityEngine.Rigidbody", "UnityEngine.Joint", "UnityEngine.Animations.AimConstraint", "UnityEngine.Animations.LookAtConstraint", "UnityEngine.Animations.ParentConstraint", "UnityEngine.Animations.PositionConstraint", "UnityEngine.Animations.RotationConstraint", "UnityEngine.Animations.ScaleConstraint", "UnityEngine.Camera", #endif "UnityEngine.FlareLayer", "UnityEngine.GUILayer", #if UNITY_STANDALONE "UnityEngine.AudioSource", "ONSPAudioSource", #endif "UnityEngine.LineRenderer", "RealisticEyeMovements.EyeAndHeadAnimator", "RealisticEyeMovements.LookTargetController", }; public static readonly string[] ComponentTypeWhiteListSdk2 = new string[] { "VRCSDK2.VRC_AvatarDescriptor", "VRCSDK2.VRC_AvatarVariations", #if UNITY_STANDALONE "VRCSDK2.VRC_SpatialAudioSource", #endif "VRCSDK2.VRC_IKFollower", "VRCSDK2.VRC_Station", }; public static readonly string[] ComponentTypeWhiteListSdk3 = new string[] { "VRC.SDK3.VRCTestMarker", "VRC.SDK3.Avatars.Components.VRCAvatarDescriptor", #if UNITY_STANDALONE "VRC.SDK3.Avatars.Components.VRCSpatialAudioSource", #endif "VRC.SDK3.Avatars.Components.VRCStation", }; private static string[] CombinedComponentTypeWhiteListSdk2 = null; private static string[] CombinedComponentTypeWhiteListSdk3 = null; public static readonly string[] ShaderWhiteList = new string[] { "VRChat/Mobile/Standard Lite", "VRChat/Mobile/Diffuse", "VRChat/Mobile/Bumped Diffuse", "VRChat/Mobile/Bumped Mapped Specular", "VRChat/Mobile/Toon Lit", "VRChat/Mobile/MatCap Lit", "VRChat/Mobile/Particles/Additive", "VRChat/Mobile/Particles/Multiply", }; public static bool ps_limiter_enabled = false; public static int ps_max_particles = 50000; public static int ps_max_systems = 200; public static int ps_max_emission = 5000; public static int ps_max_total_emission = 40000; public static int ps_mesh_particle_divider = 50; public static int ps_mesh_particle_poly_limit = 50000; public static int ps_collision_penalty_high = 120; public static int ps_collision_penalty_med = 60; public static int ps_collision_penalty_low = 10; public static int ps_trails_penalty = 10; public static int ps_max_particle_force = 0; // can not be disabled private static int _enforceAudioSourcesFrameNumber = 0; private static int _enforceAudioSourcesProcessedThisFrame = 0; private static int _enforceAvatarStationsFrameNumber = 0; private static int _enforceAvatarStationsProcessedThisFrame = 0; private static HashSet GetWhitelistForSDK(GameObject avatar) { VRC.SDKBase.VRC_AvatarDescriptor descriptor = avatar.GetComponent(); #if VRC_SDK_VRCSDK2 if (descriptor is VRCSDK2.VRC_AvatarDescriptor) { if (CombinedComponentTypeWhiteListSdk2 == null) { List concatenation = new List(ComponentTypeWhiteListCommon); concatenation.AddRange(ComponentTypeWhiteListSdk2); CombinedComponentTypeWhiteListSdk2 = concatenation.ToArray(); } return ValidationUtils.WhitelistedTypes("avatar-sdk2", CombinedComponentTypeWhiteListSdk2); } #endif #if VRC_SDK_VRCSDK3 if (descriptor is VRC.SDK3.Avatars.Components.VRCAvatarDescriptor) { if (CombinedComponentTypeWhiteListSdk3 == null) { List concatenation = new List(ComponentTypeWhiteListCommon); concatenation.AddRange(ComponentTypeWhiteListSdk3); CombinedComponentTypeWhiteListSdk3 = concatenation.ToArray(); } return ValidationUtils.WhitelistedTypes("avatar-sdk3", CombinedComponentTypeWhiteListSdk3); } #endif throw new System.Exception("Malformed avatar"); } public static void RemoveIllegalComponents(GameObject target, bool retry = true) { ValidationUtils.RemoveIllegalComponents(target, GetWhitelistForSDK(target), retry); } public static IEnumerable FindIllegalComponents(GameObject target) { return ValidationUtils.FindIllegalComponents(target, GetWhitelistForSDK(target)); } public static List EnforceAudioSourceLimits(GameObject currentAvatar) { List found = new List(); IEnumerator enforcer = EnforceAudioSourceLimitsEnumerator(currentAvatar, (a) => found.Add(a)); while (enforcer.MoveNext()) { _enforceAudioSourcesProcessedThisFrame = 0; } return found; } static void ProcessSpatialAudioSources(AudioSource audioSource) { #if VRC_SDK_VRCSDK2 VRC_SpatialAudioSource vrcSpatialAudioSource2 = audioSource.gameObject.GetComponent(); if (vrcSpatialAudioSource2 == null) { // user has not yet added VRC_SpatialAudioSource (or ONSP) // so set up some defaults vrcSpatialAudioSource2 = audioSource.gameObject.AddComponent(); vrcSpatialAudioSource2.Gain = AudioManagerSettings.AvatarAudioMaxGain; vrcSpatialAudioSource2.Far = AudioManagerSettings.AvatarAudioMaxRange; vrcSpatialAudioSource2.Near = 0f; vrcSpatialAudioSource2.VolumetricRadius = 0f; vrcSpatialAudioSource2.EnableSpatialization = true; vrcSpatialAudioSource2.enabled = true; audioSource.spatialize = true; audioSource.priority = Mathf.Clamp(audioSource.priority, 200, 255); audioSource.bypassEffects = false; audioSource.bypassListenerEffects = false; audioSource.spatialBlend = 1f; audioSource.spread = 0; // user is allowed to change, but for now put a safe default audioSource.maxDistance = AudioManagerSettings.AvatarAudioMaxRange; audioSource.minDistance = audioSource.maxDistance / 500f; audioSource.rolloffMode = AudioRolloffMode.Logarithmic; } #elif VRC_SDK_VRCSDK3 VRC.SDK3.Avatars.Components.VRCSpatialAudioSource vrcSpatialAudioSource2 = audioSource.gameObject.GetComponent(); if (vrcSpatialAudioSource2 == null) { // user has not yet added VRC_SpatialAudioSource (or ONSP) // so set up some defaults vrcSpatialAudioSource2 = audioSource.gameObject.AddComponent(); vrcSpatialAudioSource2.Gain = AudioManagerSettings.AvatarAudioMaxGain; vrcSpatialAudioSource2.Far = AudioManagerSettings.AvatarAudioMaxRange; vrcSpatialAudioSource2.Near = 0f; vrcSpatialAudioSource2.VolumetricRadius = 0f; vrcSpatialAudioSource2.EnableSpatialization = true; vrcSpatialAudioSource2.enabled = true; audioSource.spatialize = true; audioSource.priority = Mathf.Clamp(audioSource.priority, 200, 255); audioSource.bypassEffects = false; audioSource.bypassListenerEffects = false; audioSource.spatialBlend = 1f; audioSource.spread = 0; // user is allowed to change, but for now put a safe default audioSource.maxDistance = AudioManagerSettings.AvatarAudioMaxRange; audioSource.minDistance = audioSource.maxDistance / 500f; audioSource.rolloffMode = AudioRolloffMode.Logarithmic; } #endif } private static IEnumerator EnforceAudioSourceLimitsEnumerator(GameObject currentAvatar, System.Action onFound) { if (currentAvatar == null) { yield break; } Queue children = new Queue(); if (currentAvatar != null) { children.Enqueue(currentAvatar.gameObject); } while (children.Count > 0) { if (Time.frameCount > _enforceAudioSourcesFrameNumber) { _enforceAudioSourcesFrameNumber = Time.frameCount; _enforceAudioSourcesProcessedThisFrame = 0; } if (_enforceAudioSourcesProcessedThisFrame > ENFORCE_AUDIO_SOURCE_GAMEOBJECTS_PER_FRAME) { yield return null; } Profiler.BeginSample("EnforceAudioSourceLimitsEnumerator"); _enforceAudioSourcesProcessedThisFrame++; GameObject child = children.Dequeue(); if (child == null) { Profiler.EndSample(); continue; } int childCount = child.transform.childCount; for (int idx = 0; idx < childCount; ++idx) { children.Enqueue(child.transform.GetChild(idx).gameObject); } #if VRC_CLIENT if (child.GetComponent() != null) { Profiler.EndSample(); continue; } #endif AudioSource[] sources = child.transform.GetComponents(); if (sources != null && sources.Length > 0) { AudioSource audioSource = sources[0]; if (audioSource == null) { Profiler.EndSample(); continue; } #if VRC_CLIENT audioSource.outputAudioMixerGroup = VRCAudioManager.GetAvatarGroup(); audioSource.priority = Mathf.Clamp(audioSource.priority, 200, 255); #else ProcessSpatialAudioSources( audioSource ); #endif //!VRC_CLIENT onFound(audioSource); if (sources.Length > 1) { Debug.LogError("Disabling extra AudioSources on GameObject(" + child.name + "). Only one is allowed per GameObject."); for (int i = 1; i < sources.Length; i++) { if (sources[i] == null) { Profiler.EndSample(); continue; } #if VRC_CLIENT sources[i].enabled = false; sources[i].clip = null; #else ValidationUtils.RemoveComponent(sources[i]); #endif //!VRC_CLIENT } } } Profiler.EndSample(); } } public static void EnforceRealtimeParticleSystemLimits(Dictionary particleSystems, bool includeDisabled = false, bool stopSystems = true) { float totalEmission = 0; ParticleSystem ps = null; int max = 0; int em_penalty = 1; ParticleSystem.EmissionModule em; float emission = 0; ParticleSystem.Burst[] bursts; foreach (KeyValuePair kp in particleSystems) { if (kp.Key == null) continue; if (!kp.Key.isPlaying && !includeDisabled) continue; ps = kp.Key; max = kp.Value; em_penalty = 1; if (ps.collision.enabled) { // particle force is always restricted (not dependent on ps_limiter_enabled) var restrictedCollision = ps.collision; restrictedCollision.colliderForce = ps_max_particle_force; if (ps_limiter_enabled) { switch (ps.collision.quality) { case ParticleSystemCollisionQuality.High: max = max / ps_collision_penalty_high; em_penalty += 3; break; case ParticleSystemCollisionQuality.Medium: max = max / ps_collision_penalty_med; em_penalty += 2; break; case ParticleSystemCollisionQuality.Low: max = max / ps_collision_penalty_low; em_penalty += 2; break; } } } if (ps_limiter_enabled && ps.trails.enabled) { max = max / ps_trails_penalty; em_penalty += 3; } if (ps_limiter_enabled && ps.emission.enabled) { em = ps.emission; emission = 0; emission += GetCurveMax(em.rateOverTime); emission += GetCurveMax(em.rateOverDistance); bursts = new ParticleSystem.Burst[em.burstCount]; em.GetBursts(bursts); for (int i = 0; i < bursts.Length; i++) { float adjMax = bursts[i].repeatInterval > 1 ? bursts[i].maxCount : bursts[i].maxCount * bursts[i].repeatInterval; if (adjMax > ps_max_emission) bursts[i].maxCount = (short)Mathf.Clamp(adjMax, 0, ps_max_emission); } em.SetBursts(bursts); emission *= em_penalty; totalEmission += emission; if ((emission > ps_max_emission || totalEmission > ps_max_total_emission) && stopSystems) { kp.Key.Stop(true, ParticleSystemStopBehavior.StopEmittingAndClear); // Debug.LogWarning("Particle system named " + kp.Key.gameObject.name + " breached particle emission limits, it has been stopped"); } } if (ps_limiter_enabled && ps.main.maxParticles > Mathf.Clamp(max, 1, kp.Value)) { ParticleSystem.MainModule psm = ps.main; psm.maxParticles = Mathf.Clamp(psm.maxParticles, 1, max); if (stopSystems) kp.Key.Stop(true, ParticleSystemStopBehavior.StopEmittingAndClear); Debug.LogWarning("Particle system named " + kp.Key.gameObject.name + " breached particle limits, it has been limited"); } } } public static List EnforceAvatarStationLimits(GameObject currentAvatar) { List found = new List(); IEnumerator enforcer = EnforceAvatarStationLimitsEnumerator(currentAvatar, (a) => found.Add(a)); while (enforcer.MoveNext()) { _enforceAvatarStationsProcessedThisFrame = 0; } return found; } public static IEnumerator EnforceAvatarStationLimitsEnumerator(GameObject currentAvatar, System.Action onFound) { Queue children = new Queue(); children.Enqueue(currentAvatar.gameObject); int stationCount = 0; uint objectsProcessedThisFrame = 0; while (children.Count > 0) { if (Time.frameCount > _enforceAvatarStationsFrameNumber) { _enforceAvatarStationsFrameNumber = Time.frameCount; _enforceAvatarStationsProcessedThisFrame = 0; } if (_enforceAvatarStationsProcessedThisFrame > ENFORCE_STATIONS_GAMEOBJECTS_PER_FRAME) { yield return null; } Profiler.BeginSample("EnforceAvatarStationLimitsEnumerator"); _enforceAvatarStationsProcessedThisFrame++; GameObject child = children.Dequeue(); if (child == null) { Profiler.EndSample(); continue; } int childCount = child.transform.childCount; for (int idx = 0; idx < childCount; ++idx) { children.Enqueue(child.transform.GetChild(idx).gameObject); } VRC.SDKBase.VRCStation[] stations = child.transform.GetComponents(); if (stations != null && stations.Length > 0) { foreach (VRC.SDKBase.VRCStation station in stations) { if (station == null) { Profiler.EndSample(); continue; } #if VRC_CLIENT VRC_StationInternal stationInternal = station.transform.GetComponent(); #endif if (stationCount < MAX_STATIONS_PER_AVATAR) { bool markedForDestruction = false; // keep this station, but limit it if (station.disableStationExit) { Debug.LogError("[" + currentAvatar.name + "]==> Stations on avatars cannot disable station exit. Re-enabled."); station.disableStationExit = false; } if (station.stationEnterPlayerLocation != null) { if (Vector3.Distance(station.stationEnterPlayerLocation.position, station.transform.position) > MAX_STATION_LOCATION_DISTANCE) { #if VRC_CLIENT markedForDestruction = true; Debug.LogError("[" + currentAvatar.name + "]==> Station enter location is too far from station (max dist=" + MAX_STATION_LOCATION_DISTANCE + "). Station disabled."); #else Debug.LogError("Station enter location is too far from station (max dist="+MAX_STATION_LOCATION_DISTANCE+"). Station will be disabled at runtime."); #endif } if (Vector3.Distance(station.stationExitPlayerLocation.position, station.transform.position) > MAX_STATION_LOCATION_DISTANCE) { #if VRC_CLIENT markedForDestruction = true; Debug.LogError("[" + currentAvatar.name + "]==> Station exit location is too far from station (max dist=" + MAX_STATION_LOCATION_DISTANCE + "). Station disabled."); #else Debug.LogError("Station exit location is too far from station (max dist="+MAX_STATION_LOCATION_DISTANCE+"). Station will be disabled at runtime."); #endif } if (markedForDestruction) { #if VRC_CLIENT ValidationUtils.RemoveComponent(station); if (stationInternal != null) { ValidationUtils.RemoveComponent(stationInternal); } #endif } else { if (onFound != null) { onFound(station); } } } } else { #if VRC_CLIENT Debug.LogError("[" + currentAvatar.name + "]==> Removing station over limit of " + MAX_STATIONS_PER_AVATAR); ValidationUtils.RemoveComponent(station); if (stationInternal != null) { ValidationUtils.RemoveComponent(stationInternal); } #else Debug.LogError("Too many stations on avatar("+ currentAvatar.name +"). Maximum allowed="+MAX_STATIONS_PER_AVATAR+". Extra stations will be removed at runtime."); #endif } stationCount++; } } Profiler.EndSample(); if (objectsProcessedThisFrame < ENFORCE_STATIONS_GAMEOBJECTS_PER_FRAME) { continue; } objectsProcessedThisFrame = 0; yield return null; } } public static void RemoveCameras(GameObject currentAvatar, bool localPlayer, bool friend) { if (!localPlayer && currentAvatar != null) { foreach (Camera camera in currentAvatar.GetComponentsInChildren(true)) { if (camera == null || camera.gameObject == null) continue; Debug.LogWarning("Removing camera from " + camera.gameObject.name); if (friend && camera.targetTexture != null) { camera.enabled = false; } else { camera.enabled = false; if (camera.targetTexture != null) camera.targetTexture = new RenderTexture(16, 16, 24); ValidationUtils.RemoveComponent(camera); } } } } public static void StripAnimations(GameObject currentAvatar) { foreach (Animator anim in currentAvatar.GetComponentsInChildren(true)) { if (anim == null) continue; StripRuntimeAnimatorController(anim.runtimeAnimatorController); } foreach (VRC.SDKBase.VRCStation station in currentAvatar.GetComponentsInChildren(true)) { if (station == null) continue; StripRuntimeAnimatorController(station.animatorController); } #if VRC_SDK_VRCSDK3 // also strip any controllers inside the av3 descriptor var desc3 = currentAvatar.GetComponent(); if (desc3 != null) { foreach (var layer in desc3.baseAnimationLayers) StripRuntimeAnimatorController(layer.animatorController); foreach (var layer in desc3.specialAnimationLayers) StripRuntimeAnimatorController(layer.animatorController); } #endif } private static void StripRuntimeAnimatorController(RuntimeAnimatorController rc) { if (rc == null || rc.animationClips == null) return; foreach (AnimationClip clip in rc.animationClips) { if (clip == null) continue; if (clip.events != null && clip.events.Length > 0) Debug.LogWarning("Removing animation events found on " + clip.name + " on animcontroller " + rc.name); clip.events = null; } } public static void RemoveExtraAnimationComponents(GameObject currentAvatar) { if (currentAvatar == null) return; // remove Animator comps { Animator mainAnimator = currentAvatar.GetComponent(); bool removeMainAnimator = false; if (mainAnimator != null) { if (!mainAnimator.isHuman || mainAnimator.avatar == null || !mainAnimator.avatar.isValid) { removeMainAnimator = true; } } foreach (Animator anim in currentAvatar.GetComponentsInChildren(true)) { if (anim == null || anim.gameObject == null) continue; // exclude the main avatar animator if (anim == mainAnimator) { if (!removeMainAnimator) { continue; } } Debug.LogWarning("Removing Animator comp from " + anim.gameObject.name); anim.enabled = false; ValidationUtils.RemoveComponent(anim); } } ValidationUtils.RemoveComponentsOfType(currentAvatar); } private static Color32 GetTrustLevelColor(VRC.Core.APIUser user) { #if VRC_CLIENT Color32 color = new Color32(255, 255, 255, 255); if (user == null) { return color; } color = VRCPlayer.GetDisplayColorForSocialRank(user); return color; #else // we are in sdk, this is not meaningful anyway return (Color32)Color.grey; #endif } private static Material CreateFallbackMaterial(Material originalMaterial, VRC.Core.APIUser user) { #if VRC_CLIENT Material fallbackMaterial; Color trustCol = user != null ? (Color)GetTrustLevelColor(user) : Color.white; string displayName = user != null ? user.displayName : "localUser"; if (originalMaterial == null || originalMaterial.shader == null) { fallbackMaterial = VRC.Core.AssetManagement.CreateMatCap(trustCol * 0.8f + new Color(0.2f, 0.2f, 0.2f)); fallbackMaterial.name = string.Format("MC_{0}_{1}", fallbackMaterial.shader.name, displayName); } else { var safeShader = VRC.Core.AssetManagement.GetSafeShader(originalMaterial.shader.name); if (safeShader == null) { fallbackMaterial = VRC.Core.AssetManagement.CreateSafeFallbackMaterial(originalMaterial, trustCol * 0.8f + new Color(0.2f, 0.2f, 0.2f)); fallbackMaterial.name = string.Format("FB_{0}_{1}_{2}", fallbackMaterial.shader.name, displayName, originalMaterial.name); } else { //Debug.Log("*** using safe internal fallback for shader:"+ safeShader.name + ""); fallbackMaterial = new Material(safeShader); if (safeShader.name == "Standard" || safeShader.name == "Standard (Specular setup)") { VRC.Core.AssetManagement.SetupBlendMode(fallbackMaterial); } fallbackMaterial.CopyPropertiesFromMaterial(originalMaterial); fallbackMaterial.name = string.Format("INT_{0}_{1}_{2}", fallbackMaterial.shader.name, displayName, originalMaterial.name); } } return fallbackMaterial; #else // we are in sdk, this is not meaningful anyway return new Material(Shader.Find("Standard")); #endif } public static void BuildAvatarRenderersList(GameObject currentAvatar, List avatarRenderers) { currentAvatar.GetComponentsInChildren(true, avatarRenderers); } private static readonly List _replaceShadersWorkingList = new List(); public static void ReplaceShaders(VRC.Core.APIUser user, List avatarRenderers, FallbackMaterialCache fallbackMaterialCache, bool debug = true) { foreach (Renderer avatarRenderer in avatarRenderers) { if (avatarRenderer == null) { continue; } avatarRenderer.GetSharedMaterials(_replaceShadersWorkingList); bool materialNeedsReplacement = false; foreach(Material material in _replaceShadersWorkingList) { // A fallback material exists and is being used. if(fallbackMaterialCache.TryGetFallbackMaterial(material, out Material fallbackMaterial) && material == fallbackMaterial) { continue; } materialNeedsReplacement = true; break; } if(!materialNeedsReplacement) { return; } Material[] avatarRendererSharedMaterials = avatarRenderer.sharedMaterials; for (int i = 0; i < avatarRendererSharedMaterials.Length; ++i) { Material currentMaterial = avatarRendererSharedMaterials[i]; if (currentMaterial == null) { continue; } if(fallbackMaterialCache.TryGetFallbackMaterial(currentMaterial, out Material fallbackMaterial)) { if (debug) { Debug.Log($"*** Using existing fallback: '{fallbackMaterial.shader.name}' "); } } else { // The current material is not in our safe list so create a fallback. fallbackMaterial = CreateFallbackMaterial(currentMaterial, user); // Map the current material to the fallback and the fallback to itself. fallbackMaterialCache.AddFallbackMaterial(currentMaterial, fallbackMaterial); fallbackMaterialCache.AddFallbackMaterial(fallbackMaterial, fallbackMaterial); if (debug) { Debug.Log($"*** Creating new fallback: '{fallbackMaterial.shader.name}' "); } } avatarRendererSharedMaterials[i] = fallbackMaterial; } avatarRenderer.sharedMaterials = avatarRendererSharedMaterials; } } public static void ReplaceShadersRealtime(VRC.Core.APIUser user, List avatarRenderers, FallbackMaterialCache fallbackMaterialCache, bool debug = false) { ReplaceShaders(user, avatarRenderers, fallbackMaterialCache, debug); } public static void SetupParticleLimits() { ps_limiter_enabled = VRC.Core.ConfigManager.RemoteConfig.GetBool("ps_limiter_enabled", ps_limiter_enabled); ps_max_particles = VRC.Core.ConfigManager.RemoteConfig.GetInt("ps_max_particles", ps_max_particles); ps_max_systems = VRC.Core.ConfigManager.RemoteConfig.GetInt("ps_max_systems", ps_max_systems); ps_max_emission = VRC.Core.ConfigManager.RemoteConfig.GetInt("ps_max_emission", ps_max_emission); ps_max_total_emission = VRC.Core.ConfigManager.RemoteConfig.GetInt("ps_max_total_emission", ps_max_total_emission); ps_mesh_particle_divider = VRC.Core.ConfigManager.RemoteConfig.GetInt("ps_mesh_particle_divider", ps_mesh_particle_divider); ps_mesh_particle_poly_limit = VRC.Core.ConfigManager.RemoteConfig.GetInt("ps_mesh_particle_poly_limit", ps_mesh_particle_poly_limit); ps_collision_penalty_high = VRC.Core.ConfigManager.RemoteConfig.GetInt("ps_collision_penalty_high", ps_collision_penalty_high); ps_collision_penalty_med = VRC.Core.ConfigManager.RemoteConfig.GetInt("ps_collision_penalty_med", ps_collision_penalty_med); ps_collision_penalty_low = VRC.Core.ConfigManager.RemoteConfig.GetInt("ps_collision_penalty_low", ps_collision_penalty_low); ps_trails_penalty = VRC.Core.ConfigManager.RemoteConfig.GetInt("ps_trails_penalty", ps_trails_penalty); if (Application.isMobilePlatform) ps_limiter_enabled = true; else { ps_limiter_enabled = VRC.Core.ConfigManager.LocalConfig.GetList("betas").Contains("particle_system_limiter") || ps_limiter_enabled; ps_max_particles = VRC.Core.ConfigManager.LocalConfig.GetInt("ps_max_particles", ps_max_particles); ps_max_systems = VRC.Core.ConfigManager.LocalConfig.GetInt("ps_max_systems", ps_max_systems); ps_max_emission = VRC.Core.ConfigManager.LocalConfig.GetInt("ps_max_emission", ps_max_emission); ps_max_total_emission = VRC.Core.ConfigManager.LocalConfig.GetInt("ps_max_total_emission", ps_max_total_emission); ps_mesh_particle_divider = VRC.Core.ConfigManager.LocalConfig.GetInt("ps_mesh_particle_divider", ps_mesh_particle_divider); ps_mesh_particle_poly_limit = VRC.Core.ConfigManager.LocalConfig.GetInt("ps_mesh_particle_poly_limit", ps_mesh_particle_poly_limit); ps_collision_penalty_high = VRC.Core.ConfigManager.LocalConfig.GetInt("ps_collision_penalty_high", ps_collision_penalty_high); ps_collision_penalty_med = VRC.Core.ConfigManager.LocalConfig.GetInt("ps_collision_penalty_med", ps_collision_penalty_med); ps_collision_penalty_low = VRC.Core.ConfigManager.LocalConfig.GetInt("ps_collision_penalty_low", ps_collision_penalty_low); ps_trails_penalty = VRC.Core.ConfigManager.LocalConfig.GetInt("ps_trails_penalty", ps_trails_penalty); } } public static Dictionary EnforceParticleSystemLimits(GameObject currentAvatar) { Dictionary particleSystems = new Dictionary(); foreach (ParticleSystem ps in currentAvatar.transform.GetComponentsInChildren(true)) { int realtime_max = ps_max_particles; // always limit collision force var collision = ps.collision; if (collision.colliderForce > ps_max_particle_force) { collision.colliderForce = ps_max_particle_force; Debug.LogError("Collision force is restricted on avatars, particle system named " + ps.gameObject.name + " collision force restricted to " + ps_max_particle_force); } if (ps_limiter_enabled) { if (particleSystems.Count > ps_max_systems) { Debug.LogError("Too many particle systems, #" + particleSystems.Count + " named " + ps.gameObject.name + " deleted"); ValidationUtils.RemoveComponent(ps); continue; } else { var main = ps.main; var emission = ps.emission; ParticleSystemRenderer renderer = ps.GetComponent(); if (renderer != null) { if (renderer.renderMode == ParticleSystemRenderMode.Mesh) { Mesh[] meshes = new Mesh[0]; int highestPoly = 0; renderer.GetMeshes(meshes); if (meshes.Length == 0 && renderer.mesh != null) { meshes = new Mesh[] { renderer.mesh }; } // Debug.Log(meshes.Length + " meshes possible emmited meshes from " + ps.gameObject.name); foreach (Mesh m in meshes) { if (m.isReadable) { if (m.triangles.Length / 3 > highestPoly) { highestPoly = m.triangles.Length / 3; } } else { if (1000 > highestPoly) { highestPoly = int.MaxValue; } } } if (highestPoly > 0) { highestPoly = Mathf.Clamp(highestPoly / ps_mesh_particle_divider, 1, highestPoly); realtime_max = Mathf.FloorToInt((float)realtime_max / highestPoly); if (highestPoly > ps_mesh_particle_poly_limit) { Debug.LogError("Particle system named " + ps.gameObject.name + " breached polygon limits, it has been deleted"); ValidationUtils.RemoveComponent(ps); continue; } } } } ParticleSystem.MinMaxCurve rate = emission.rateOverTime; if (rate.mode == ParticleSystemCurveMode.Constant) { rate.constant = Mathf.Clamp(rate.constant, 0, ps_max_emission); } else if (rate.mode == ParticleSystemCurveMode.TwoConstants) { rate.constantMax = Mathf.Clamp(rate.constantMax, 0, ps_max_emission); } else { rate.curveMultiplier = Mathf.Clamp(rate.curveMultiplier, 0, ps_max_emission); } emission.rateOverTime = rate; rate = emission.rateOverDistance; if (rate.mode == ParticleSystemCurveMode.Constant) { rate.constant = Mathf.Clamp(rate.constant, 0, ps_max_emission); } else if (rate.mode == ParticleSystemCurveMode.TwoConstants) { rate.constantMax = Mathf.Clamp(rate.constantMax, 0, ps_max_emission); } else { rate.curveMultiplier = Mathf.Clamp(rate.curveMultiplier, 0, ps_max_emission); } emission.rateOverDistance = rate; //Disable collision with PlayerLocal layer collision.collidesWith &= ~(1 << 10); } } particleSystems.Add(ps, realtime_max); } EnforceRealtimeParticleSystemLimits(particleSystems, true, false); return particleSystems; } public static bool ClearLegacyAnimations(GameObject currentAvatar) { bool hasLegacyAnims = false; foreach (var ani in currentAvatar.GetComponentsInChildren(true)) { if (ani.clip != null) if (ani.clip.legacy) { Debug.LogWarningFormat("Legacy animation found named '{0}' on '{1}', removing", ani.clip.name, ani.gameObject.name); ani.clip = null; hasLegacyAnims = true; } foreach (AnimationState anistate in ani) if (anistate.clip.legacy) { Debug.LogWarningFormat("Legacy animation found named '{0}' on '{1}', removing", anistate.clip.name, ani.gameObject.name); ani.RemoveClip(anistate.clip); hasLegacyAnims = true; } } return hasLegacyAnims; } private static float GetCurveMax(ParticleSystem.MinMaxCurve minMaxCurve) { switch (minMaxCurve.mode) { case ParticleSystemCurveMode.Constant: return minMaxCurve.constant; case ParticleSystemCurveMode.TwoConstants: return minMaxCurve.constantMax; default: return minMaxCurve.curveMultiplier; } } public static bool AreAnyParticleSystemsPlaying(Dictionary particleSystems) { foreach (KeyValuePair kp in particleSystems) { if (kp.Key != null && kp.Key.isPlaying) return true; } return false; } public static void StopAllParticleSystems(Dictionary particleSystems) { foreach (KeyValuePair kp in particleSystems) { if (kp.Key != null && kp.Key.isPlaying) { kp.Key.Stop(true, ParticleSystemStopBehavior.StopEmittingAndClear); } } } public static IEnumerable FindIllegalShaders(GameObject target) { return ShaderValidation.FindIllegalShaders(target, ShaderWhiteList); } /// /// NOTE: intended to be called from 'VRCAvatarManager.SafetyCheckAndComponentScan' /// but temporarily disabled (until we enable texture streaming) /// public static void ReportTexturesWithoutMipMapStreaming(VRC.Core.ApiAvatar avatar, GameObject target) { var badTextures = new List(); foreach (Renderer r in target.GetComponentsInChildren()) { foreach (Material m in r.sharedMaterials) { foreach (int i in m.GetTexturePropertyNameIDs()) { Texture2D t = m.GetTexture(i) as Texture2D; if (!t) continue; if ((t.mipmapCount > 0) && !t.streamingMipmaps) badTextures.Add(t); } } } if (badTextures.Count > 0) { string warning = "[" + avatar.name + "]==> One or more avatar textures have non-streaming mipmaps: "; foreach (Texture2D t in badTextures) { warning += "'" + t.name + "', "; } warning = warning.Remove(warning.LastIndexOf(",")); Debug.LogWarning(warning + "."); } } public static void ClampRenderQueues(List avatarRenderers, int minimumRenderQueue, int maximumRenderQueue) { using(_clampRenderQueuesProfilerMarker.Auto()) { foreach(Renderer avatarRenderer in avatarRenderers) { if(avatarRenderer == null) { continue; } avatarRenderer.GetSharedMaterials(_clampRenderQueuesMaterialsTempList); foreach(Material avatarSharedMaterial in _clampRenderQueuesMaterialsTempList) { if(avatarSharedMaterial == null) { continue; } int renderQueue = avatarSharedMaterial.renderQueue; if(renderQueue < minimumRenderQueue) { avatarSharedMaterial.renderQueue = minimumRenderQueue; } else if(renderQueue > maximumRenderQueue) { avatarSharedMaterial.renderQueue = maximumRenderQueue; } } } } } } }