1052 lines
46 KiB
C#
1052 lines
46 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using Unity.Profiling;
|
|
using UnityEngine;
|
|
using UnityEngine.Profiling;
|
|
using Debug = UnityEngine.Debug;
|
|
|
|
// ReSharper disable RedundantNameQualifier
|
|
|
|
namespace VRC.SDKBase.Validation
|
|
{
|
|
public static class AvatarValidation
|
|
{
|
|
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;
|
|
|
|
public static readonly string[] ComponentTypeWhiteListCommon = new string[]
|
|
{
|
|
#if UNITY_STANDALONE
|
|
"DynamicBone",
|
|
"DynamicBoneCollider",
|
|
"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.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",
|
|
"UnityEngine.AudioSource",
|
|
"ONSPAudioSource",
|
|
#endif
|
|
#if !VRC_CLIENT
|
|
"VRC.Core.PipelineSaver",
|
|
#endif
|
|
"VRC.Core.PipelineManager",
|
|
"UnityEngine.Transform",
|
|
"UnityEngine.Animator",
|
|
"UnityEngine.SkinnedMeshRenderer",
|
|
"LimbIK", // our limbik based on Unity ik
|
|
"LoadingAvatarTextureAnimation",
|
|
"UnityEngine.MeshFilter",
|
|
"UnityEngine.MeshRenderer",
|
|
"UnityEngine.Animation",
|
|
"UnityEngine.ParticleSystem",
|
|
"UnityEngine.ParticleSystemRenderer",
|
|
"UnityEngine.TrailRenderer",
|
|
"UnityEngine.FlareLayer",
|
|
"UnityEngine.GUILayer",
|
|
"UnityEngine.LineRenderer",
|
|
"RealisticEyeMovements.EyeAndHeadAnimator",
|
|
"RealisticEyeMovements.LookTargetController",
|
|
};
|
|
|
|
public static readonly string[] ComponentTypeWhiteListSdk2 = new string[]
|
|
{
|
|
#if UNITY_STANDALONE
|
|
"VRCSDK2.VRC_SpatialAudioSource",
|
|
#endif
|
|
"VRCSDK2.VRC_AvatarDescriptor",
|
|
"VRCSDK2.VRC_AvatarVariations",
|
|
"VRCSDK2.VRC_IKFollower",
|
|
"VRCSDK2.VRC_Station",
|
|
};
|
|
|
|
public static readonly string[] ComponentTypeWhiteListSdk3 = new string[]
|
|
{
|
|
#if UNITY_STANDALONE
|
|
"VRC.SDK3.Avatars.Components.VRCSpatialAudioSource",
|
|
#endif
|
|
"VRC.SDK3.VRCTestMarker",
|
|
"VRC.SDK3.Avatars.Components.VRCAvatarDescriptor",
|
|
"VRC.SDK3.Avatars.Components.VRCStation",
|
|
"VRC.SDK3.Dynamics.PhysBone.Components.VRCPhysBone",
|
|
"VRC.SDK3.Dynamics.PhysBone.Components.VRCPhysBoneCollider",
|
|
"VRC.SDK3.Dynamics.Contact.Components.VRCContactSender",
|
|
"VRC.SDK3.Dynamics.Contact.Components.VRCContactReceiver",
|
|
"VRC.SDKBase.VRC_Slider",
|
|
};
|
|
|
|
#pragma warning disable 0414
|
|
|
|
private static string[] CombinedComponentTypeWhiteListSdk2 = null;
|
|
private static string[] CombinedComponentTypeWhiteListSdk3 = null;
|
|
|
|
#pragma warning restore 0414
|
|
|
|
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 HashSet<System.Type> GetWhitelistForSDK(GameObject avatar)
|
|
{
|
|
VRC.SDKBase.VRC_AvatarDescriptor descriptor = avatar.GetComponent<VRC.SDKBase.VRC_AvatarDescriptor>();
|
|
|
|
#if VRC_SDK_VRCSDK2
|
|
if(descriptor is VRCSDK2.VRC_AvatarDescriptor)
|
|
{
|
|
if(CombinedComponentTypeWhiteListSdk2 == null)
|
|
{
|
|
List<string> concatenation = new List<string>(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<string> concatenation = new List<string>(ComponentTypeWhiteListCommon);
|
|
concatenation.AddRange(ComponentTypeWhiteListSdk3);
|
|
CombinedComponentTypeWhiteListSdk3 = concatenation.ToArray();
|
|
}
|
|
|
|
return ValidationUtils.WhitelistedTypes("avatar-sdk3", CombinedComponentTypeWhiteListSdk3);
|
|
}
|
|
#endif
|
|
//throw new System.Exception("Malformed avatar");
|
|
// instead of exception, log error, and return empty whitelist
|
|
Debug.LogError("Malformed avatar");
|
|
return new HashSet<System.Type>();
|
|
}
|
|
|
|
public static void RemoveIllegalComponents(GameObject target, bool retry = true)
|
|
{
|
|
ValidationUtils.RemoveIllegalComponents(target, GetWhitelistForSDK(target), retry);
|
|
}
|
|
|
|
public static IEnumerable<Component> FindIllegalComponents(GameObject target)
|
|
{
|
|
return ValidationUtils.FindIllegalComponents(target, GetWhitelistForSDK(target));
|
|
}
|
|
|
|
private static ProfilerMarker _enforceAudioSourceLimitsProfilerMarker = new ProfilerMarker("AvatarValidation.EnforceAudioSourceLimits");
|
|
|
|
public static void EnforceAudioSourceLimits(GameObject currentAvatar)
|
|
{
|
|
using(_enforceAudioSourceLimitsProfilerMarker.Auto())
|
|
{
|
|
if(currentAvatar == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Queue<GameObject> children = new Queue<GameObject>();
|
|
if(currentAvatar != null)
|
|
{
|
|
children.Enqueue(currentAvatar.gameObject);
|
|
}
|
|
|
|
while(children.Count > 0)
|
|
{
|
|
GameObject child = children.Dequeue();
|
|
if(child == null)
|
|
{
|
|
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<USpeaker>() != null)
|
|
{
|
|
continue;
|
|
}
|
|
#endif
|
|
|
|
AudioSource[] sources = child.transform.GetComponents<AudioSource>();
|
|
if(sources == null || sources.Length <= 0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
AudioSource audioSource = sources[0];
|
|
if(audioSource == null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
|
|
#if VRC_CLIENT
|
|
audioSource.outputAudioMixerGroup = VRCAudioManager.GetAvatarGroup();
|
|
audioSource.priority = Mathf.Clamp(audioSource.priority, 200, 255);
|
|
#else
|
|
ProcessSpatialAudioSources(audioSource);
|
|
#endif //!VRC_CLIENT
|
|
|
|
if(sources.Length <= 1)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public static void EnforceClothLimits(GameObject avatarGameObject)
|
|
{
|
|
const int clothMaxSolverFrequency = 240;
|
|
foreach(Cloth cloth in avatarGameObject.GetComponentsInChildren<Cloth>(true))
|
|
{
|
|
if(cloth.clothSolverFrequency > clothMaxSolverFrequency)
|
|
{
|
|
cloth.clothSolverFrequency = clothMaxSolverFrequency;
|
|
}
|
|
}
|
|
}
|
|
|
|
#if VRC_CLIENT
|
|
public static void EnforceAimIKLimits(GameObject avatarGameObject)
|
|
{
|
|
const int aimIKMaxSolverFrequency = 64;
|
|
foreach(RootMotion.FinalIK.AimIK aimIK in avatarGameObject.GetComponentsInChildren<RootMotion.FinalIK.AimIK>(true))
|
|
{
|
|
if(aimIK.solver.maxIterations > aimIKMaxSolverFrequency)
|
|
{
|
|
aimIK.solver.maxIterations = aimIKMaxSolverFrequency;
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
|
|
#if !VRC_CLIENT
|
|
private static void ProcessSpatialAudioSources(AudioSource audioSource)
|
|
{
|
|
#if VRC_SDK_VRCSDK2
|
|
VRC_SpatialAudioSource vrcSpatialAudioSource2 = audioSource.gameObject.GetComponent<VRC_SpatialAudioSource>();
|
|
if (vrcSpatialAudioSource2 == null)
|
|
{
|
|
// user has not yet added VRC_SpatialAudioSource (or ONSP)
|
|
// so set up some defaults
|
|
vrcSpatialAudioSource2 = audioSource.gameObject.AddComponent<VRC_SpatialAudioSource>();
|
|
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<VRC.SDK3.Avatars.Components.VRCSpatialAudioSource>();
|
|
if (vrcSpatialAudioSource2 == null)
|
|
{
|
|
// user has not yet added VRC_SpatialAudioSource (or ONSP)
|
|
// so set up some defaults
|
|
vrcSpatialAudioSource2 = audioSource.gameObject.AddComponent<VRC.SDK3.Avatars.Components.VRCSpatialAudioSource>();
|
|
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
|
|
}
|
|
#endif
|
|
|
|
public static void EnforceRealtimeParticleSystemLimits(Dictionary<ParticleSystem, int> 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<ParticleSystem, int> 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");
|
|
}
|
|
}
|
|
}
|
|
|
|
private static ProfilerMarker _enforceAvatarStationLimitsProfilerMarker = new ProfilerMarker("AvatarValidation.EnforceAudioSourceLimits");
|
|
|
|
public static void EnforceAvatarStationLimits(GameObject currentAvatar)
|
|
{
|
|
using(_enforceAvatarStationLimitsProfilerMarker.Auto())
|
|
{
|
|
int stationCount = 0;
|
|
foreach(VRC.SDKBase.VRCStation station in currentAvatar.gameObject.GetComponentsInChildren<VRC.SDKBase.VRCStation>(true))
|
|
{
|
|
if(station == null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
#if VRC_CLIENT
|
|
VRC_StationInternal stationInternal = station.transform.GetComponent<VRC_StationInternal>();
|
|
#endif
|
|
if(stationCount < MAX_STATIONS_PER_AVATAR)
|
|
{
|
|
#if VRC_CLIENT
|
|
bool markedForDestruction = false;
|
|
#endif
|
|
// 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 VRC_CLIENT
|
|
if(markedForDestruction)
|
|
{
|
|
ValidationUtils.RemoveComponent(station);
|
|
if(stationInternal != null)
|
|
{
|
|
ValidationUtils.RemoveComponent(stationInternal);
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
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++;
|
|
}
|
|
}
|
|
}
|
|
|
|
public static void RemoveCameras(GameObject currentAvatar, bool localPlayer, bool friend)
|
|
{
|
|
if(!localPlayer && currentAvatar != null)
|
|
{
|
|
foreach(Camera camera in currentAvatar.GetComponentsInChildren<Camera>(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<Animator>(true))
|
|
{
|
|
if(anim == null)
|
|
continue;
|
|
|
|
StripRuntimeAnimatorController(anim.runtimeAnimatorController);
|
|
}
|
|
|
|
foreach(VRC.SDKBase.VRCStation station in currentAvatar.GetComponentsInChildren<VRC.SDKBase.VRCStation>(true))
|
|
{
|
|
if(station == null)
|
|
continue;
|
|
|
|
StripRuntimeAnimatorController(station.animatorController);
|
|
}
|
|
#if VRC_SDK_VRCSDK3
|
|
// also strip any controllers inside the av3 descriptor
|
|
var desc3 = currentAvatar.GetComponent<VRC.SDK3.Avatars.Components.VRCAvatarDescriptor>();
|
|
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<Animator>();
|
|
bool removeMainAnimator = false;
|
|
if(mainAnimator != null)
|
|
{
|
|
if(!mainAnimator.isHuman || mainAnimator.avatar == null || !mainAnimator.avatar.isValid)
|
|
{
|
|
removeMainAnimator = true;
|
|
}
|
|
}
|
|
|
|
foreach(Animator anim in currentAvatar.GetComponentsInChildren<Animator>(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<UnityEngine.Animation>(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("<color=cyan>*** using safe internal fallback for shader:"+ safeShader.name + "</color>");
|
|
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<Renderer> avatarRenderers)
|
|
{
|
|
currentAvatar.GetComponentsInChildren(true, avatarRenderers);
|
|
}
|
|
|
|
// TCL's method of allocation avoidance
|
|
private static readonly List<Material> _replaceShadersWorkingList = new List<Material>();
|
|
|
|
public static void ReplaceShaders(VRC.Core.APIUser user, List<Renderer> avatarRenderers, FallbackMaterialCache fallbackMaterialCache, bool debug = false)
|
|
{
|
|
foreach(Renderer avatarRenderer in avatarRenderers)
|
|
{
|
|
if(avatarRenderer == null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
avatarRenderer.GetSharedMaterials(_replaceShadersWorkingList);
|
|
bool anyReplaced = false;
|
|
for(int i = 0; i < _replaceShadersWorkingList.Count; ++i)
|
|
{
|
|
Material currentMaterial = _replaceShadersWorkingList[i];
|
|
if(currentMaterial == null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Check if the material has a cached fallback material if not then create a new one.
|
|
if(!fallbackMaterialCache.TryGetFallbackMaterial(currentMaterial, out Material fallbackMaterial))
|
|
{
|
|
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($"<color=cyan>*** Creating new fallback: '{fallbackMaterial.shader.name}' </color>");
|
|
}
|
|
|
|
if(fallbackMaterial == currentMaterial)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
_replaceShadersWorkingList[i] = fallbackMaterial;
|
|
anyReplaced = true;
|
|
continue;
|
|
}
|
|
|
|
// If the material is the fallback then we don't need to change it.
|
|
if(currentMaterial == fallbackMaterial)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if(debug)
|
|
{
|
|
Debug.Log($"<color=cyan>*** Using existing fallback: '{fallbackMaterial.shader.name}' </color>");
|
|
}
|
|
|
|
_replaceShadersWorkingList[i] = fallbackMaterial;
|
|
anyReplaced = true;
|
|
}
|
|
|
|
if(anyReplaced)
|
|
{
|
|
avatarRenderer.sharedMaterials = _replaceShadersWorkingList.ToArray();
|
|
}
|
|
}
|
|
}
|
|
|
|
public static void ReplaceShadersRealtime(VRC.Core.APIUser user, List<Renderer> 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<ParticleSystem, int> EnforceParticleSystemLimits(GameObject currentAvatar)
|
|
{
|
|
Dictionary<ParticleSystem, int> particleSystems = new Dictionary<ParticleSystem, int>();
|
|
|
|
foreach(ParticleSystem ps in currentAvatar.transform.GetComponentsInChildren<ParticleSystem>(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<ParticleSystemRenderer>();
|
|
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<UnityEngine.Animation>(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<ParticleSystem, int> particleSystems)
|
|
{
|
|
foreach(KeyValuePair<ParticleSystem, int> kp in particleSystems)
|
|
{
|
|
if(kp.Key != null && kp.Key.isPlaying)
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public static void StopAllParticleSystems(Dictionary<ParticleSystem, int> particleSystems)
|
|
{
|
|
foreach(KeyValuePair<ParticleSystem, int> kp in particleSystems)
|
|
{
|
|
if(kp.Key != null && kp.Key.isPlaying)
|
|
{
|
|
kp.Key.Stop(true, ParticleSystemStopBehavior.StopEmittingAndClear);
|
|
}
|
|
}
|
|
}
|
|
|
|
public static IEnumerable<Shader> FindIllegalShaders(GameObject target)
|
|
{
|
|
return ShaderValidation.FindIllegalShaders(target, ShaderWhiteList);
|
|
}
|
|
|
|
/// <summary>
|
|
/// NOTE: intended to be called from 'VRCAvatarManager.SafetyCheckAndComponentScan'
|
|
/// but temporarily disabled (until we enable texture streaming)
|
|
/// </summary>
|
|
public static void ReportTexturesWithoutMipMapStreaming(VRC.Core.ApiAvatar avatar, GameObject target)
|
|
{
|
|
var badTextures = new List<Texture2D>();
|
|
foreach(Renderer r in target.GetComponentsInChildren<Renderer>())
|
|
{
|
|
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(",", StringComparison.Ordinal));
|
|
Debug.LogWarning(warning + ".");
|
|
}
|
|
}
|
|
|
|
public static void ConvertDynamicBoneToAvatarDynamics(GameObject avatarGameObject)
|
|
{
|
|
#if VRC_CLIENT
|
|
AvatarDynamicsPlayerSetup.ConvertDynamicBonesToPhysBones(avatarGameObject);
|
|
#endif
|
|
}
|
|
}
|
|
}
|