Skip to content

Instantly share code, notes, and snippets.

@naelstrof
Last active January 3, 2023 21:03
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save naelstrof/3a1c812cf573df73a82fd495ed08c371 to your computer and use it in GitHub Desktop.
Save naelstrof/3a1c812cf573df73a82fd495ed08c371 to your computer and use it in GitHub Desktop.
Tests your avatar in-editor from the VRChat SDK->Test Selected Avatars menu item.
// VRChatHelper.cs, a tool to help test your model without launching VRChat.
// Made by naelstrof (naelstrof@gmail.com)
// Some eye tracking advice to help prevent this tool from yelling at you: https://docs.google.com/document/d/1BvX_OdEilbJ7wEcvd5MRA1g29NGCAp3G3nHGp73t_CQ/edit?usp=sharing
#if UNITY_EDITOR
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
[InitializeOnLoad]
public class VRChatHelper : MonoBehaviour {
static int testing;
[MenuItem("VRChat SDK/Test Selected Avatars")]
private static void TestAvatar() {
foreach (GameObject obj in Selection.gameObjects) {
VRChatHelper v = obj.AddComponent<VRChatHelper> ();
}
EditorApplication.isPlaying = true;
}
static VRChatHelper() {
EditorApplication.playmodeStateChanged += PlaymodeStateChanged;
}
// This is called for each object, including the global static one. So we just keep track of how many have initiated/stopped to determine if we've stopped testing.
private static void PlaymodeStateChanged() {
if (EditorApplication.isPlayingOrWillChangePlaymode) {
testing++;
}
if (EditorApplication.isPaused == false && EditorApplication.isPlayingOrWillChangePlaymode == false && testing > 0) {
testing--;
}
if (testing == 0) {
foreach (VRChatHelper v in Component.FindObjectsOfType<VRChatHelper>()) {
DestroyImmediate (v);
}
}
}
Animator a;
Vector3 lookPos;
VRCSDK2.VRC_AvatarDescriptor d;
SkinnedMeshRenderer m;
SkinnedMeshRenderer eyeMesh;
Transform le, re;
int blinkl;
int blinkr;
int lowerlidl;
int lowerlidr;
int curindex;
float waitTime = 1f;
float waitTimer;
HumanDescription h;
HumanLimit leLimit;
HumanLimit reLimit;
Vector3 leStartRot;
Vector3 reStartRot;
Vector3 jawStartRot;
bool sane;
bool warn;
bool eyesSane;
public bool TestWalking = true;
public bool TestEyes = true;
public bool TestLipSync = true;
void Start () {
eyesSane = true;
// Check that we're applied to the right thing.
a = GetComponentInChildren<Animator>();
d = GetComponent<VRCSDK2.VRC_AvatarDescriptor> ();
if (d == null) {
Destroy (this);
throw new UnityException ("A non-avatar was selected! Ignoring...");
}
if (a == null) {
Destroy (this);
throw new UnityException ("It seems your avatar is missing an animator, you should add one! (It's a component, and required).");
} else {
if (a.runtimeAnimatorController == null) {
// Try to quickly apply the simple animator...
string[] guids = AssetDatabase.FindAssets ("SimpleAvatarController", null);
if (guids.Length <= 0) {
throw new UnityException ("Your avatar needs an animation controller attached to the animator component! Try setting it to SimpleAvatarController.");
}
RuntimeAnimatorController rAC = AssetDatabase.LoadAssetAtPath<RuntimeAnimatorController> (AssetDatabase.GUIDToAssetPath (guids [0]));
if (rAC == null) {
throw new UnityException ("Your avatar needs an animation controller attached to the animator component! Try setting it to SimpleAvatarController.");
}
a.runtimeAnimatorController = rAC;
}
string fbxAsset = gameObject.name;
// Check bone limits.
if (!GetHumanDescription (fbxAsset, ref h)) {
throw new UnityException ("Failed to import the human description of the specified asset. Is it a humanoid, or did you change the name of the prefab in scene? (The avatar's name in the hierarchy must share its name with the model name in the asset browser.)");
}
foreach (HumanBone bone in h.human) {
if (bone.limit.max == Vector3.zero && bone.limit.min == Vector3.zero && bone.limit.axisLength != 0) {
Debug.LogError ("A muscle attached to " + bone.humanName + " has 0 flexibility. This might not be intended. (To fix, adjust your muscle settings.)");
}
if (bone.humanName == "LeftEye") {
leLimit = bone.limit;
if (bone.boneName == "LeftEye") {
Debug.LogError ("You shouldn't have LeftEye be the real eye, it should be an empty bone! Create a different bone for eye tracking. (See https://docs.google.com/document/d/1BvX_OdEilbJ7wEcvd5MRA1g29NGCAp3G3nHGp73t_CQ/edit?usp=sharing for details.)");
warn = true;
eyesSane = false;
}
}
if (bone.humanName == "RightEye") {
reLimit = bone.limit;
if (bone.boneName == "RightEye") {
Debug.LogError ("You shouldn't have RightEye be the real eye, it should be an empty bone! Create a different bone for eye tracking. (See https://docs.google.com/document/d/1BvX_OdEilbJ7wEcvd5MRA1g29NGCAp3G3nHGp73t_CQ/edit?usp=sharing for details.)");
warn = true;
eyesSane = false;
}
}
}
}
// Check that our visemes are fully definined, if we're an avatar with viseme blend shapes.
if (d.lipSync == VRCSDK2.VRC_AvatarDescriptor.LipSyncStyle.VisemeBlendShape) {
m = d.VisemeSkinnedMesh;
foreach (string s in d.VisemeBlendShapes) {
if (m.sharedMesh.GetBlendShapeIndex (s) == -1) {
throw new UnityException ("Missing viseme definition, make sure they're all set in the Avatar Descriptor.");
}
}
} else if (d.lipSync == VRCSDK2.VRC_AvatarDescriptor.LipSyncStyle.JawFlapBlendShape) {
m = d.VisemeSkinnedMesh;
if (d.MouthOpenBlendShapeName == "" || d.MouthOpenBlendShapeName == null) {
throw new UnityException ("Missing jawflap viseme definition, make sure it's set in the Avatar Descriptor.");
}
} else if (d.lipSync == VRCSDK2.VRC_AvatarDescriptor.LipSyncStyle.JawFlapBone) {
if (d.lipSyncJawBone == null) {
throw new UnityException ("Missing jawflap bone make sure it's set in the Avatar Descriptor.");
}
jawStartRot = d.lipSyncJawBone.localEulerAngles;
}
// Check that the mesh "Body" exists. Required for eyes to track.
Transform body = gameObject.transform.Find ("Body");
if (body == null) {
Debug.LogError("Mesh containing blinking visemes must be named \"Body\", or eye tracking won't work!");
warn = true;
eyesSane = false;
} else {
// Sanity check that our Body is actually a mesh
eyeMesh = body.gameObject.GetComponent<SkinnedMeshRenderer> ();
if (eyeMesh == null) {
Debug.LogError("Mesh containing blinking visemes named \"Body\" isn't actually a mesh!");
eyesSane = false;
warn = true;
}
// Check that our blink visemes exist and are in the correct index.
blinkl = eyeMesh.sharedMesh.GetBlendShapeIndex ("vrc.blink_left");
blinkr = eyeMesh.sharedMesh.GetBlendShapeIndex ("vrc.blink_right");
lowerlidl = eyeMesh.sharedMesh.GetBlendShapeIndex ("vrc.lowerlid_left");
lowerlidr = eyeMesh.sharedMesh.GetBlendShapeIndex ("vrc.lowerlid_right");
if (blinkl != 0 || blinkr != 1 || lowerlidl != 2 || lowerlidr != 3) {
Debug.LogError("Missing blink shapes, need to be named correctly and in the correct index for eye tracking to work!");
eyesSane = false;
warn = true;
}
}
// Make sure our armature has the correct structure for eyes to work.
Transform leftEyeCheck = gameObject.transform.Find ("Armature/Hips/Spine/Chest/Neck/Head/LeftEye");
Transform rightEyeCheck = gameObject.transform.Find ("Armature/Hips/Spine/Chest/Neck/Head/RightEye");
if (leftEyeCheck == null || rightEyeCheck == null) {
Debug.LogError("Armature structure must be exactly `Armature/Hips/Spine/Chest/Neck/Head/{LeftEye,RightEye}` in order for eye tracking to function!");
eyesSane = false;
warn = true;
}
le = a.GetBoneTransform (HumanBodyBones.LeftEye);
re = a.GetBoneTransform (HumanBodyBones.RightEye);
if (le == null || re == null) {
Debug.LogError ("Eyes were not set in the rig configuration! Eyes won't work unless they're specified.");
eyesSane = false;
} else {
leStartRot = le.localEulerAngles;
reStartRot = re.localEulerAngles;
}
sane = true;
}
void OnDrawGizmosSelected() {
Camera cam = Camera.current;
Vector3 cameraPosition = transform.position + new Vector3(0f,1.8f,0f);
Vector3 forward = Vector3.zero;
if (cam == null) {
cam = Camera.main;
}
if (cam != null) {
cameraPosition = cam.transform.position;
forward = cam.transform.forward*5f;
}
if (!sane) {
Gizmos.color = Color.red;
Handles.Label (cameraPosition + forward + new Vector3(0,2,0), "An error was detected! Check the console log for details...");
return;
}
Gizmos.color = Color.yellow;
if (warn) {
Handles.Label (cameraPosition + forward + new Vector3(0,2,0), "A warning was detected! Check the console log for details...");
}
Gizmos.DrawSphere(lookPos, 0.1f);
if (le != null && re != null) {
Gizmos.DrawLine (le.position, lookPos);
Gizmos.DrawLine (re.position, lookPos);
}
Gizmos.color = Color.yellow;
}
// Update is called once per frame
void OnAnimatorIK(int layerIndex) {
if (!sane) {
return;
}
if (!TestEyes) {
a.SetLookAtWeight (0f);
return;
}
if (eyesSane) {
a.SetLookAtWeight (1.0f, 0.0f, 0.3f, 1.0f);
} else {
a.SetLookAtWeight (1.0f, 0.3f, 1.0f, 0.0f);
}
a.SetLookAtPosition (lookPos);
}
void Update () {
if (!sane) {
return;
}
if (a != null && TestWalking) {
a.SetFloat ("MovementX", Mathf.Sin (Time.time / 1.8f));
a.SetFloat ("MovementZ", Mathf.Cos (Time.time / 2.2f));
} else {
a.SetFloat ("MovementX", 0);
a.SetFloat ("MovementZ", 0);
}
// visually test blinking and lowering lids.
if (eyesSane && TestEyes && eyeMesh != null && blinkl == 0 && blinkr == 1 && lowerlidl == 2 && lowerlidr == 3) {
eyeMesh.SetBlendShapeWeight (blinkl, Mathf.Clamp ((Mathf.Sin (Time.time) - 0.99f) * 10000f, 0, 100f));
eyeMesh.SetBlendShapeWeight (blinkr, Mathf.Clamp ((Mathf.Sin (Time.time) - 0.99f) * 10000f, 0, 100f));
eyeMesh.SetBlendShapeWeight (lowerlidl, (Mathf.Sin (Time.time/2f)*100f));
eyeMesh.SetBlendShapeWeight (lowerlidr, (Mathf.Sin (Time.time/2f)*100f));
}
// If we have a mesh that has visemes. Rotate through each one to test them visually.
if (m != null && TestLipSync && d.lipSync == VRCSDK2.VRC_AvatarDescriptor.LipSyncStyle.VisemeBlendShape) {
if (waitTimer <= 0) {
waitTimer = waitTime;
m.SetBlendShapeWeight (m.sharedMesh.GetBlendShapeIndex (d.VisemeBlendShapes [curindex++]), 0f);
curindex = curindex % d.VisemeBlendShapes.Length;
} else {
m.SetBlendShapeWeight (m.sharedMesh.GetBlendShapeIndex (d.VisemeBlendShapes [curindex]), ((waitTime - waitTimer) / waitTime) * 100f);
waitTimer -= Time.deltaTime;
}
// Or just flex the one over and over
} else if (m != null && TestLipSync && d.lipSync == VRCSDK2.VRC_AvatarDescriptor.LipSyncStyle.JawFlapBlendShape) {
m.SetBlendShapeWeight (m.sharedMesh.GetBlendShapeIndex (d.MouthOpenBlendShapeName), Mathf.Sin (Time.time) * 100f);
} // Or we test the jaw flap bone, which happens in LateUpdate...
// Set the look target position as something random around the character.
Transform head = a.GetBoneTransform (HumanBodyBones.Head);
lookPos = head.position + Vector3.forward * Mathf.Sin (Time.time * 1f)*5f + Vector3.up * Mathf.Sin (Time.time * 0.25f)*5f + Vector3.right * Mathf.Cos (Time.time * 1f)*5f;
}
void LateUpdate() {
if (!sane) {
return;
}
if (TestLipSync && d.lipSync == VRCSDK2.VRC_AvatarDescriptor.LipSyncStyle.JawFlapBone) {
d.lipSyncJawBone.localEulerAngles = jawStartRot + new Vector3 (0f, 0f, Mathf.Sin (Time.time) * 30f);
}
if (!eyesSane || !TestEyes) {
return;
}
if (le != null) {
/*Vector3 rot = le.localEulerAngles - leStartRot;
while (rot.x > 180f) {
rot.x -= 360f;
}
while (rot.x <= -180f) {
rot.x += 360f;
}
while (rot.y > 180f) {
rot.y -= 360f;
}
while (rot.y <= -180f) {
rot.y += 360f;
}
while (rot.z > 180f) {
rot.z -= 360f;
}
while (rot.z <= -180f) {
rot.z += 360f;
}
// FIXME: Not sure why these have to be flipped around, might be specific to the orientation of the eye bone or something. Testing needed...
rot.x = Mathf.Clamp (rot.x, -leLimit.max.y, -leLimit.min.y);
rot.y = Mathf.Clamp (rot.y, -leLimit.max.x, -leLimit.min.x);
rot.z = Mathf.Clamp (rot.z, -leLimit.max.z, -leLimit.min.z);
le.localEulerAngles = rot + leStartRot;*/
}
if (re != null) {
/*Vector3 rot = re.localEulerAngles - reStartRot;
while (rot.x > 180f) {
rot.x -= 360f;
}
while (rot.x <= -180f) {
rot.x += 360f;
}
while (rot.y > 180f) {
rot.y -= 360f;
}
while (rot.y <= -180f) {
rot.y += 360f;
}
while (rot.z > 180f) {
rot.z -= 360f;
}
while (rot.z <= -180f) {
rot.z += 360f;
}
// FIXME: Not sure why these have to be flipped around, might be specific to the orientation of the eye bone or something. Testing needed...
rot.x = Mathf.Clamp (rot.x, -reLimit.max.y, -reLimit.min.y);
rot.y = Mathf.Clamp (rot.y, -reLimit.max.x, -reLimit.min.x);
rot.z = Mathf.Clamp (rot.z, -reLimit.max.z, -reLimit.min.z);
re.localEulerAngles = rot + reStartRot;*/
}
}
public bool GetHumanDescription(string target, ref HumanDescription des) {
if (target != null) {
string[] assets = AssetDatabase.FindAssets (target, null);
if (assets.Length <= 0) {
return false;
}
// Figure out which one has the rig
foreach (string guid in assets) {
AssetImporter importer = AssetImporter.GetAtPath (AssetDatabase.GUIDToAssetPath (guid));
if (importer != null) {
ModelImporter modelImporter = importer as ModelImporter;
if (modelImporter != null) {
des = modelImporter.humanDescription;
return true;
}
}
}
}
return false;
}
}
#endif
@SirXen0
Copy link

SirXen0 commented Feb 4, 2021

Doesn't work in the current version of the avatar SDK :(

Assets\Editor\VRChatHelper.cs(41,13): error CS0234: The type or namespace name 'VRC_AvatarDescriptor' does not exist in the namespace 'VRCSDK2' (are you missing an assembly reference?)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment