Files
MSCO/GameCharacter.cs
T
MrMahoandClaude Opus 5.5 cb116da2b8 Player models: MSC body mesh with own skeleton and normal clothes
All MSC humans share one body mesh (20 bones, same order, different bone
names) and differ only by shirt/pants/face materials. Instead of cloning NPCs
(their thighs hang off 'skeleton', the head off a HeadPivot outside the
skeleton - legs and torso stretched like a dress), MSCO now takes just the
most used body mesh, builds its own flat skeleton from the bind pose and sets
every bone directly from the MSCO figure's joints. Clothes: char_shirt*,
char_pants*, char_face* (no women/ghost/curtain/flower), chosen per player id.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 13:15:22 +02:00

243 lines
12 KiB
C#

using System.Collections.Generic;
using UnityEngine;
namespace MSCO
{
/// <summary>
/// Mitspieler als MSC-Mensch: Alle Leute im Spiel benutzen denselben Körper ("bodymesh", 20 Knochen) und
/// unterscheiden sich nur durch drei Materialien - Hemd (char_shirtXX), Hose (char_pantsXX) und Gesicht
/// (char_faceXX). MSCO nimmt nur dieses Mesh, baut ein eigenes Skelett aus seiner Bind-Pose und gibt jedem
/// Spieler ein normales Hemd/Hose/Gesicht (keine Kleider, Vorhänge oder Geister). Die Knochen werden jedes Frame
/// direkt aus den Gelenkpunkten der MSCO-Figur gesetzt (laufen, springen, ducken, sitzen, Items halten).
///
/// Knochen-Reihenfolge ist bei allen MSC-Menschen gleich (nur die Namen unterscheiden sich):
/// 0 Becken, 1-2 Wirbelsäule, 3 Kopf, 4/9 Schlüsselbein, 5/10 Schulter, 6/11 Ellbogen, 7/12 Hand, 8/13 Finger,
/// 14/17 Hüfte, 15/18 Knie, 16/19 Knöchel (links/rechts). Bind-Pose: Z oben, X vorne, -Y links.
/// </summary>
public class GameCharacter
{
/// <summary>Gelenkpunkte der MSCO-Figur im lokalen Raum des Spielers (jedes Frame von RemotePlayer.Pose).</summary>
public class Rig
{
public Vector3 Hip, NeckBase, Head;
public Vector3 Up = Vector3.up;
public float Pitch;
public readonly Vector3[] HipJoint = new Vector3[2], Knee = new Vector3[2], Ankle = new Vector3[2];
public readonly Vector3[] Shoulder = new Vector3[2], Elbow = new Vector3[2], Hand = new Vector3[2];
}
const int BoneCount = 20;
const int Pelvis = 0, Head = 3;
static readonly int[] Torso = { 0, 1, 2, 4, 9 };
static readonly int[] Shoulder = { 5, 10 }, Elbow = { 6, 11 }, Wrist = { 7, 12 }, Finger = { 8, 13 };
static readonly int[] HipBone = { 14, 17 }, Knee = { 15, 18 }, Ankle = { 16, 19 };
// ---------------------------------------------------------------- Mesh und Kleidung aus dem Spiel
static Mesh bodyMesh;
static List<Material> shirts, pants, faces;
static float nextSearch;
static bool FindAssets()
{
if (bodyMesh != null && shirts.Count > 0 && pants.Count > 0 && faces.Count > 0) return true;
if (Time.realtimeSinceStartup < nextSearch) return false;
nextSearch = Time.realtimeSinceStartup + 10f;
var users = new Dictionary<Mesh, int>();
var shirtByName = new Dictionary<string, Material>();
var pantsByName = new Dictionary<string, Material>();
var faceByName = new Dictionary<string, Material>();
foreach (SkinnedMeshRenderer smr in Resources.FindObjectsOfTypeAll<SkinnedMeshRenderer>())
{
if (smr == null || smr.name != "bodymesh") continue;
Mesh m = smr.sharedMesh;
if (m == null || m.bindposes == null || m.bindposes.Length != BoneCount || m.subMeshCount != 3) continue;
int n;
users.TryGetValue(m, out n);
users[m] = n + 1;
Material[] mats = smr.sharedMaterials;
if (mats == null || mats.Length != 3) continue;
Add(shirtByName, mats[0], "char_shirt");
Add(pantsByName, mats[1], "char_pants");
if (mats[2] != null && !mats[2].name.Contains("woman")) Add(faceByName, mats[2], "char_face");
}
// Das Mesh, das die meisten Leute benutzen = der normale Mann
Mesh best = null;
int bestUsers = 0;
foreach (KeyValuePair<Mesh, int> kv in users)
if (kv.Value > bestUsers && LooksHuman(kv.Key)) { best = kv.Key; bestUsers = kv.Value; }
bodyMesh = best;
shirts = Sorted(shirtByName);
pants = Sorted(pantsByName);
faces = Sorted(faceByName);
bool ok = bodyMesh != null && shirts.Count > 0 && pants.Count > 0 && faces.Count > 0;
if (ok) MscoLog.Info("[MSCO] Player models: MSC body with " + shirts.Count + " shirts, " + pants.Count + " trousers, " + faces.Count + " faces");
return ok;
}
static void Add(Dictionary<string, Material> into, Material m, string prefix)
{
if (m != null && m.name.StartsWith(prefix) && !into.ContainsKey(m.name)) into[m.name] = m;
}
static List<Material> Sorted(Dictionary<string, Material> d)
{
var names = new List<string>(d.Keys);
names.Sort(string.CompareOrdinal); // bei allen Spielern gleiche Reihenfolge -> gleiches Aussehen
return names.ConvertAll(n => d[n]);
}
/// <summary>Prüft die erwartete Knochen-Reihenfolge an der Bind-Pose (Kopf oben, Knöchel unten, Beine lang).</summary>
static bool LooksHuman(Mesh m)
{
Vector3[] p = BindPositions(m);
if (p[Head].z < p[Pelvis].z + 0.4f) return false;
for (int i = 0; i < 2; i++)
{
if (p[Ankle[i]].z > p[Knee[i]].z || p[Knee[i]].z > p[HipBone[i]].z) return false;
if (p[Wrist[i]].z > p[Shoulder[i]].z) return false;
}
return p[Shoulder[0]].y < 0f && p[Shoulder[1]].y > 0f; // links = -Y
}
static Vector3[] BindPositions(Mesh m)
{
var p = new Vector3[BoneCount];
for (int i = 0; i < BoneCount; i++) p[i] = m.bindposes[i].inverse.GetColumn(3);
return p;
}
// ---------------------------------------------------------------- Instanz
GameObject model;
Transform owner;
readonly Transform[] bones = new Transform[BoneCount];
readonly Vector3[] restPos = new Vector3[BoneCount]; // relativ zum Spieler
readonly Quaternion[] restRot = new Quaternion[BoneCount];
Vector3 restHipCenter;
public string Description { get; private set; }
/// <summary>Figur bauen (variant wählt Hemd, Hose und Gesicht). null, wenn das Spiel-Mesh nicht gefunden wird.</summary>
public static GameCharacter Create(Transform owner, int variant)
{
if (!FindAssets()) return null;
var c = new GameCharacter();
try
{
c.Build(owner, Mathf.Abs(variant));
return c;
}
catch (System.Exception e)
{
MscoLog.Warning("[MSCO] Player model could not be built: " + e.Message);
c.Destroy();
return null;
}
}
void Build(Transform owner, int variant)
{
this.owner = owner;
Vector3[] bind = BindPositions(bodyMesh);
float leg = (bind[Knee[0]] - bind[HipBone[0]]).magnitude + (bind[Ankle[0]] - bind[Knee[0]]).magnitude;
float scale = 0.84f / Mathf.Max(0.1f, leg); // Bein der MSCO-Figur
model = new GameObject("MSCO_Model");
model.transform.SetParent(owner, false);
// Bind-Pose: X vorne, Z oben -> Spieler: Z vorne, Y oben
model.transform.localRotation = Quaternion.Inverse(Quaternion.LookRotation(Vector3.right, Vector3.forward));
model.transform.localScale = Vector3.one * scale;
Matrix4x4[] bindposes = bodyMesh.bindposes;
for (int i = 0; i < BoneCount; i++)
{
var b = new GameObject("bone" + i).transform;
b.SetParent(model.transform, false);
Matrix4x4 m = bindposes[i].inverse;
b.localPosition = m.GetColumn(3);
b.localRotation = Quaternion.LookRotation(m.GetColumn(2), m.GetColumn(1));
bones[i] = b;
restPos[i] = owner.InverseTransformPoint(b.position);
restRot[i] = Quaternion.Inverse(owner.rotation) * b.rotation;
}
restHipCenter = (restPos[HipBone[0]] + restPos[HipBone[1]]) * 0.5f;
var smr = model.AddComponent<SkinnedMeshRenderer>();
smr.sharedMesh = bodyMesh;
smr.bones = bones;
smr.rootBone = bones[Pelvis];
smr.updateWhenOffscreen = true; // die Knochen bewegen sich weit weg von der gespeicherten Hülle
Material shirt = shirts[(variant * 7 + 3) % shirts.Count];
Material trousers = pants[(variant * 3 + 1) % pants.Count];
Material face = faces[(variant * 5 + 2) % faces.Count];
smr.sharedMaterials = new[] { shirt, trousers, face };
Description = "MSC body (" + shirt.name + ", " + trousers.name + ", " + face.name + ")";
}
// ---------------------------------------------------------------- jedes Frame
public void Apply(Rig rig)
{
if (model == null) return;
Quaternion root = owner.rotation;
Quaternion lean = Quaternion.FromToRotation(Vector3.up, rig.Up);
Quaternion torso = root * lean;
foreach (int i in Torso) Place(i, TorsoPoint(rig, lean, restPos[i]), torso * restRot[i]);
Place(Head, TorsoPoint(rig, lean, restPos[Head]), root * Quaternion.Euler(rig.Pitch, 0f, 0f) * restRot[Head]);
for (int s = 0; s < 2; s++)
{
// Bein: Hüfte am Becken, dann Richtung Knie bzw. Knöchel der MSCO-Figur
Vector3 hip = TorsoPoint(rig, lean, restPos[HipBone[s]]);
Quaternion thigh = AimDelta(HipBone[s], Knee[s], rig.Knee[s] - rig.HipJoint[s]);
Place(HipBone[s], hip, root * thigh * restRot[HipBone[s]]);
Vector3 knee = hip + root * (thigh * (restPos[Knee[s]] - restPos[HipBone[s]]));
Quaternion shin = AimDelta(Knee[s], Ankle[s], rig.Ankle[s] - rig.Knee[s]);
Place(Knee[s], knee, root * shin * restRot[Knee[s]]);
Vector3 ankle = knee + root * (shin * (restPos[Ankle[s]] - restPos[Knee[s]]));
Place(Ankle[s], ankle, root * restRot[Ankle[s]]); // Fuß bleibt flach
// Arm: Schulter am Oberkörper, dann Richtung Ellbogen bzw. Hand
Vector3 shoulder = TorsoPoint(rig, lean, restPos[Shoulder[s]]);
Quaternion upper = AimDelta(Shoulder[s], Elbow[s], rig.Elbow[s] - rig.Shoulder[s]);
Place(Shoulder[s], shoulder, root * upper * restRot[Shoulder[s]]);
Vector3 elbow = shoulder + root * (upper * (restPos[Elbow[s]] - restPos[Shoulder[s]]));
Quaternion fore = AimDelta(Elbow[s], Wrist[s], rig.Hand[s] - rig.Elbow[s]);
Place(Elbow[s], elbow, root * fore * restRot[Elbow[s]]);
Vector3 wrist = elbow + root * (fore * (restPos[Wrist[s]] - restPos[Elbow[s]]));
Place(Wrist[s], wrist, root * fore * restRot[Wrist[s]]);
Vector3 finger = wrist + root * (fore * (restPos[Finger[s]] - restPos[Wrist[s]]));
Place(Finger[s], finger, root * fore * restRot[Finger[s]]);
}
}
/// <summary>Punkt des Oberkörpers: an der Hüfte der MSCO-Figur, mit ihr geneigt.</summary>
Vector3 TorsoPoint(Rig rig, Quaternion lean, Vector3 rest)
{
return owner.TransformPoint(rig.Hip + lean * (rest - restHipCenter));
}
/// <summary>Drehung, die das Knochenstück from->to aus der Ruhelage in die gewünschte Richtung bringt.</summary>
Quaternion AimDelta(int from, int to, Vector3 desired)
{
Vector3 rest = restPos[to] - restPos[from];
if (desired.sqrMagnitude < 1e-6f || rest.sqrMagnitude < 1e-6f) return Quaternion.identity;
return Quaternion.FromToRotation(rest, desired);
}
void Place(int i, Vector3 pos, Quaternion rot)
{
bones[i].position = pos;
bones[i].rotation = rot;
}
public void Destroy()
{
if (model != null) Object.Destroy(model);
model = null;
}
}
}