C#:
// AutoAligner.cs
using UnityEngine;
using System.Collections.Generic;
using System.Linq;
namespace Stopper {
public class AutoAligner : MVRScript {
JSONStorableStringChooser personChooser;
UIDynamicPopup personPopup;
JSONStorableStringChooser boneChooser;
UIDynamicPopup bonePopup;
Atom selectedPerson;
Transform targetBone;
UIDynamicTextField bonePathLabel;
Dictionary<string, string> labeledBonePaths = new Dictionary<string, string>();
JSONStorableFloat oscSpeedJSON;
JSONStorableFloat oscAmplitudeJSON;
JSONStorableFloat moveXJSON;
JSONStorableFloat moveYJSON;
JSONStorableFloat moveZJSON;
JSONStorableFloat rotXJSON;
JSONStorableFloat rotYJSON;
JSONStorableFloat rotZJSON;
JSONStorableBool alignPositionJSON;
JSONStorableBool alignRotationJSON;
JSONStorableBool oscMoveXJSON;
JSONStorableBool oscMoveYJSON;
JSONStorableBool oscMoveZJSON;
JSONStorableBool oscRotXJSON;
JSONStorableBool oscRotYJSON;
JSONStorableBool oscRotZJSON;
float oscTime;
public override void Init() {
List<string> personChoices = SuperController.singleton.GetAtoms()
.Where(a => a.type == "Person")
.Select(a => a.uid)
.ToList();
personChooser = new JSONStorableStringChooser("person", personChoices, null, "Target Person", (uid) => {
selectedPerson = SuperController.singleton.GetAtomByUid(uid);
UpdateBoneList();
});
RegisterStringChooser(personChooser);
personPopup = CreateFilterablePopup(personChooser);
boneChooser = new JSONStorableStringChooser("bone", new List<string>(), null, "Target Bone", (path) => {
if (labeledBonePaths.ContainsKey(path)) {
targetBone = FindBoneByPath(selectedPerson?.transform, labeledBonePaths[path]);
} else {
targetBone = FindBoneByPath(selectedPerson?.transform, path);
}
if (bonePathLabel != null) bonePathLabel.text = labeledBonePaths.ContainsKey(path) ? labeledBonePaths[path] : path;
});
RegisterStringChooser(boneChooser);
bonePopup = CreateFilterablePopup(boneChooser);
bonePathLabel = CreateTextField(new JSONStorableString("bonePathLabel", ""), false);
bonePathLabel.height = 30f;
alignPositionJSON = new JSONStorableBool("Align Position", true);
RegisterBool(alignPositionJSON);
CreateToggle(alignPositionJSON);
alignRotationJSON = new JSONStorableBool("Align Rotation", true);
RegisterBool(alignRotationJSON);
CreateToggle(alignRotationJSON);
oscMoveXJSON = new JSONStorableBool("Oscillate Position X", false);
RegisterBool(oscMoveXJSON);
CreateToggle(oscMoveXJSON);
oscMoveYJSON = new JSONStorableBool("Oscillate Position Y", false);
RegisterBool(oscMoveYJSON);
CreateToggle(oscMoveYJSON);
oscMoveZJSON = new JSONStorableBool("Oscillate Position Z", false);
RegisterBool(oscMoveZJSON);
CreateToggle(oscMoveZJSON);
oscRotXJSON = new JSONStorableBool("Oscillate Rotation X", false);
RegisterBool(oscRotXJSON);
CreateToggle(oscRotXJSON);
oscRotYJSON = new JSONStorableBool("Oscillate Rotation Y", false);
RegisterBool(oscRotYJSON);
CreateToggle(oscRotYJSON);
oscRotZJSON = new JSONStorableBool("Oscillate Rotation Z", false);
RegisterBool(oscRotZJSON);
CreateToggle(oscRotZJSON);
oscSpeedJSON = new JSONStorableFloat("Oscillation Speed", 1f, 0.1f, 30f);
RegisterFloat(oscSpeedJSON);
CreateSlider(oscSpeedJSON);
oscAmplitudeJSON = new JSONStorableFloat("Oscillation Amplitude", 0.1f, 0f, 1f);
RegisterFloat(oscAmplitudeJSON);
CreateSlider(oscAmplitudeJSON);
moveXJSON = new JSONStorableFloat("Position Offset X", 0f, -1f, 1f);
RegisterFloat(moveXJSON);
CreateSlider(moveXJSON);
moveYJSON = new JSONStorableFloat("Position Offset Y", 0f, -1f, 1f);
RegisterFloat(moveYJSON);
CreateSlider(moveYJSON);
moveZJSON = new JSONStorableFloat("Position Offset Z", 0f, -1f, 1f);
RegisterFloat(moveZJSON);
CreateSlider(moveZJSON);
rotXJSON = new JSONStorableFloat("Rotation Offset X", 0f, -180f, 180f);
RegisterFloat(rotXJSON);
CreateSlider(rotXJSON);
rotYJSON = new JSONStorableFloat("Rotation Offset Y", 0f, -180f, 180f);
RegisterFloat(rotYJSON);
CreateSlider(rotYJSON);
rotZJSON = new JSONStorableFloat("Rotation Offset Z", 0f, -180f, 180f);
RegisterFloat(rotZJSON);
CreateSlider(rotZJSON);
}
void UpdateBoneList() {
if (selectedPerson == null) return;
labeledBonePaths.Clear();
var allTransforms = selectedPerson.GetComponentsInChildren<Transform>(true);
foreach (var t in allTransforms) {
string path = GetRelativePath(t, selectedPerson.transform);
if (path.EndsWith("mouthPhysicsMeshPredictionPoint")) {
labeledBonePaths["Mouth"] = path;
} else if (path.EndsWith("LabiaTrigger")) {
labeledBonePaths["Genitals"] = path;
} else if (path.EndsWith("_JointAl/Debug")) {
labeledBonePaths["Anus"] = path;
}
}
boneChooser.choices = labeledBonePaths.Keys.ToList();
if (boneChooser.choices.Count > 0) {
boneChooser.val = boneChooser.choices[0];
if (bonePathLabel != null) bonePathLabel.text = labeledBonePaths[boneChooser.choices[0]];
}
}
Transform FindBoneByPath(Transform root, string path) {
return root != null ? root.Find(path) : null;
}
string GetRelativePath(Transform t, Transform root) {
List<string> names = new List<string>();
while (t != null && t != root) {
names.Insert(0, t.name);
t = t.parent;
}
return string.Join("/", names.ToArray());
}
void LateUpdate() {
if (selectedPerson == null || targetBone == null || boneChooser.val == null || !labeledBonePaths.ContainsKey(boneChooser.val)) return;
var checkTransform = FindBoneByPath(selectedPerson.transform, labeledBonePaths[boneChooser.val]);
if (checkTransform == null) return;
oscTime += Time.deltaTime * oscSpeedJSON.val;
float oscOffset = Mathf.Sin(oscTime) * oscAmplitudeJSON.val;
Vector3 offset = Vector3.zero;
if (oscMoveXJSON.val) offset += targetBone.right * oscOffset;
if (oscMoveYJSON.val) offset += targetBone.up * oscOffset;
if (oscMoveZJSON.val) offset += targetBone.forward * oscOffset;
offset += targetBone.right * moveXJSON.val;
offset += targetBone.up * moveYJSON.val;
offset += targetBone.forward * moveZJSON.val;
Vector3 newPosition = targetBone.position + offset;
Quaternion additionalRotation = Quaternion.identity;
if (oscRotXJSON.val) additionalRotation *= Quaternion.Euler(oscOffset, 0, 0);
if (oscRotYJSON.val) additionalRotation *= Quaternion.Euler(0, oscOffset, 0);
if (oscRotZJSON.val) additionalRotation *= Quaternion.Euler(0, 0, oscOffset);
Quaternion newRotation = targetBone.rotation * additionalRotation * Quaternion.Euler(
rotXJSON.val,
rotYJSON.val,
rotZJSON.val);
if (alignPositionJSON.val) containingAtom.mainController.transform.position = newPosition;
if (alignRotationJSON.val) containingAtom.mainController.transform.rotation = newRotation;
}
}
}
The code was written by ChatGPT.
Below is the list of referenced plugins
AlignHelper

AlignHelper - Plugins + Scripts -
In scene creation, it is often necessary to align or offset the position of some atoms relative to others. This plugin helps to align atoms in a scene with other atoms or a point on the screen. How To Use: Load this plugin on the atoms that...

CUA Clothing

CUA Clothing - Plugins + Scripts -
This plugin allows CUAs to be loaded when clothing is put on. The CUA can be attached to bones, skin, or the loaded clothing item, similar to https://hub.virtamate.com/resources/attach-to-vertex.45482/ Usage If you haven't worked with clothing...

I referenced the alignment function from AlignHelper.
I don’t have much knowledge of code, so I’m not sure how similar it is, but after dozens of attempts, ChatGPT managed to understand how it works and succeeded.
Next, I needed the position of the bone set as the receiver—this was based on the functionality of CUA Clothing.
I listed all the bones of the character and used the Collider Editor by the creator 'Acid Bubbles' to determine the most suitable bone as the target.
That part alone took around 8 hours.
ChatGPT kept giving error codes or repeated the same non-working solutions.
I didn’t realize how stressful it would be to instruct it to understand the logic behind fetching the receiver list. As for the distance and speed control, I just asked ChatGPT and it created them.
Since it's a simple logic, I guess it was easy for it to implement
This one was newly created using two plugins released under a CC BY license.
I would like to express my gratitude for the original creators' hard work and dedication, and I clearly state that I hold no copyright. Since the original authors released their work under a CC BY license, I am releasing this under a CC BY-SA license in the spirit of continuing their values.
This plugin doesn’t always function perfectly.
If a collider interaction occurs while the asset’s physics option is turned off, the character may get pushed away. In practice, enabling the physics option is almost essential.
Because of this, if the character makes a large motion or changes position, the object can slip out of the mouth, for example. Putting it back in requires the inconvenience of turning the collider and physics options off and on again.