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>
This commit is contained in:
+5
-4
@@ -10,11 +10,12 @@
|
|||||||
- Braucht keinen neuen Server: die Infos kommen vom Master (msco.mbm.wtf) und funktionieren mit 2.8-Servern
|
- Braucht keinen neuen Server: die Infos kommen vom Master (msco.mbm.wtf) und funktionieren mit 2.8-Servern
|
||||||
|
|
||||||
**Mitspieler als echte MSC-Menschen**
|
**Mitspieler als echte MSC-Menschen**
|
||||||
- Andere Spieler sehen aus wie die Leute im Spiel: MSCO nimmt eine Kopie eines MSC-Menschen (Körper, Gesicht,
|
- Andere Spieler sehen aus wie die Leute im Spiel: derselbe Körper wie alle MSC-Menschen, mit einem normalen Hemd,
|
||||||
Kleidung) und bewegt dessen Skelett mit der bisherigen Animation - laufen, springen, ducken, sitzen, Items halten
|
einer Hose und einem Gesicht aus dem Spiel (keine Kleider/Vorhänge/Geister). Bewegt wird ein eigenes Skelett mit der
|
||||||
- Jeder Spieler bekommt (nach Spieler-Nummer) einen anderen Menschen; bei allen Mitspielern derselbe
|
bisherigen Animation - laufen, springen, ducken, sitzen, Items halten
|
||||||
|
- Jeder Spieler bekommt (nach Spieler-Nummer) eine andere Kombination; alle Mitspieler sehen dieselbe
|
||||||
- Konsole (F7): `model classic` zeigt wieder die MSCO-Figur, `model game` die MSC-Menschen; `rig` zeigt, wer welches
|
- Konsole (F7): `model classic` zeigt wieder die MSCO-Figur, `model game` die MSC-Menschen; `rig` zeigt, wer welches
|
||||||
Modell hat. Findet MSCO keinen passenden Menschen, bleibt automatisch die MSCO-Figur
|
Modell hat. Findet MSCO den Körper nicht, bleibt automatisch die MSCO-Figur
|
||||||
|
|
||||||
**Beifahrer**
|
**Beifahrer**
|
||||||
- **Automatisch einsteigen**: in die offene Beifahrertür an den Sitz treten - man setzt sich von selbst hin, keine Taste
|
- **Automatisch einsteigen**: in die offene Beifahrertür an den Sitz treten - man setzt sich von selbst hin, keine Taste
|
||||||
|
|||||||
+155
-224
@@ -1,15 +1,18 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
|
||||||
namespace MSCO
|
namespace MSCO
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Echte Spielfigur für Mitspieler: eine Kopie eines MSC-Menschen (SkinnedMeshRenderer "bodymesh" mit Skelett
|
/// Mitspieler als MSC-Mensch: Alle Leute im Spiel benutzen denselben Körper ("bodymesh", 20 Knochen) und
|
||||||
/// pelvis/knee_left/ankle_left/shoulder_left/arm_left/hand_left/head ...). Die Knochen werden jedes Frame aus der
|
/// unterscheiden sich nur durch drei Materialien - Hemd (char_shirtXX), Hose (char_pantsXX) und Gesicht
|
||||||
/// Pose der MSCO-Figur gesetzt (Hüfte, Knie, Knöchel, Schulter, Ellbogen, Hand, Kopf) - laufen, springen, ducken,
|
/// (char_faceXX). MSCO nimmt nur dieses Mesh, baut ein eigenes Skelett aus seiner Bind-Pose und gibt jedem
|
||||||
/// sitzen und Items halten funktionieren dadurch genau wie vorher. Die Bewegung wird über Richtungen übertragen
|
/// Spieler ein normales Hemd/Hose/Gesicht (keine Kleider, Vorhänge oder Geister). Die Knochen werden jedes Frame
|
||||||
/// (Ruhelage aus der Bind-Pose des Modells), dadurch ist egal, wie die Knochen im Modell ausgerichtet sind.
|
/// 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>
|
/// </summary>
|
||||||
public class GameCharacter
|
public class GameCharacter
|
||||||
{
|
{
|
||||||
@@ -23,289 +26,217 @@ namespace MSCO
|
|||||||
public readonly Vector3[] Shoulder = new Vector3[2], Elbow = new Vector3[2], Hand = new Vector3[2];
|
public readonly Vector3[] Shoulder = new Vector3[2], Elbow = new Vector3[2], Hand = new Vector3[2];
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------- Vorlagen im Spiel finden
|
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 };
|
||||||
|
|
||||||
static readonly string[] RequiredBones =
|
// ---------------------------------------------------------------- Mesh und Kleidung aus dem Spiel
|
||||||
{ "pelvis", "head", "knee_left", "knee_right", "ankle_left", "ankle_right", "hand_left", "hand_right" };
|
|
||||||
|
|
||||||
static List<SkinnedMeshRenderer> templates;
|
static Mesh bodyMesh;
|
||||||
static float nextTemplateSearch;
|
static List<Material> shirts, pants, faces;
|
||||||
|
static float nextSearch;
|
||||||
|
|
||||||
/// <summary>Passende Menschen im Spiel (sortiert nach Pfad: bei allen Spielern gleiche Reihenfolge).</summary>
|
static bool FindAssets()
|
||||||
static List<SkinnedMeshRenderer> Templates()
|
|
||||||
{
|
{
|
||||||
if (templates != null && templates.Count > 0 && templates.TrueForAll(t => t != null)) return templates;
|
if (bodyMesh != null && shirts.Count > 0 && pants.Count > 0 && faces.Count > 0) return true;
|
||||||
if (Time.realtimeSinceStartup < nextTemplateSearch) return templates ?? new List<SkinnedMeshRenderer>();
|
if (Time.realtimeSinceStartup < nextSearch) return false;
|
||||||
nextTemplateSearch = Time.realtimeSinceStartup + 10f;
|
nextSearch = Time.realtimeSinceStartup + 10f;
|
||||||
|
|
||||||
var found = new List<KeyValuePair<string, SkinnedMeshRenderer>>();
|
var users = new Dictionary<Mesh, int>();
|
||||||
var seenMeshes = new HashSet<Mesh>();
|
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>())
|
foreach (SkinnedMeshRenderer smr in Resources.FindObjectsOfTypeAll<SkinnedMeshRenderer>())
|
||||||
{
|
{
|
||||||
if (smr == null || smr.sharedMesh == null || !smr.name.StartsWith("bodymesh") || smr.name.Contains("kid")) continue;
|
if (smr == null || smr.name != "bodymesh") continue;
|
||||||
if (smr.hideFlags != HideFlags.None || smr.bones == null || smr.bones.Length < 10) continue;
|
Mesh m = smr.sharedMesh;
|
||||||
if (smr.sharedMesh.bindposes == null || smr.sharedMesh.bindposes.Length != smr.bones.Length) continue;
|
if (m == null || m.bindposes == null || m.bindposes.Length != BoneCount || m.subMeshCount != 3) continue;
|
||||||
if (!HasBones(smr)) continue;
|
int n;
|
||||||
Transform root = CloneRoot(smr);
|
users.TryGetValue(m, out n);
|
||||||
if (root == null || !SafeToClone(root)) continue;
|
users[m] = n + 1;
|
||||||
if (!seenMeshes.Add(smr.sharedMesh)) continue; // gleiches Aussehen nur einmal
|
Material[] mats = smr.sharedMaterials;
|
||||||
found.Add(new KeyValuePair<string, SkinnedMeshRenderer>(PathOf(smr.transform), smr));
|
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");
|
||||||
}
|
}
|
||||||
found.Sort((a, b) => string.CompareOrdinal(a.Key, b.Key));
|
// Das Mesh, das die meisten Leute benutzen = der normale Mann
|
||||||
templates = found.ConvertAll(kv => kv.Value);
|
Mesh best = null;
|
||||||
if (templates.Count > 0) MscoLog.Info("[MSCO] Player models: " + templates.Count + " MSC characters found");
|
int bestUsers = 0;
|
||||||
return templates;
|
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 bool HasBones(SkinnedMeshRenderer smr)
|
static void Add(Dictionary<string, Material> into, Material m, string prefix)
|
||||||
{
|
{
|
||||||
var names = new HashSet<string>();
|
if (m != null && m.name.StartsWith(prefix) && !into.ContainsKey(m.name)) into[m.name] = m;
|
||||||
foreach (Transform b in smr.bones) if (b != null) names.Add(b.name);
|
|
||||||
foreach (string r in RequiredBones) if (!names.Contains(r)) return false;
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Kleinster gemeinsamer Vorfahre von Mesh und Skelett - das wird kopiert.</summary>
|
static List<Material> Sorted(Dictionary<string, Material> d)
|
||||||
static Transform CloneRoot(SkinnedMeshRenderer smr)
|
|
||||||
{
|
{
|
||||||
Transform bone = smr.rootBone != null ? smr.rootBone : smr.bones[0];
|
var names = new List<string>(d.Keys);
|
||||||
if (bone == null) return null;
|
names.Sort(string.CompareOrdinal); // bei allen Spielern gleiche Reihenfolge -> gleiches Aussehen
|
||||||
for (Transform a = smr.transform; a != null; a = a.parent)
|
return names.ConvertAll(n => d[n]);
|
||||||
if (bone == a || bone.IsChildOf(a)) return a;
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>Prüft die erwartete Knochen-Reihenfolge an der Bind-Pose (Kopf oben, Knöchel unten, Beine lang).</summary>
|
||||||
/// Kopieren darf keine Spiel-Logik starten: entweder hängen in dem Teilbaum keine Skripte (PlayMaker ...),
|
static bool LooksHuman(Mesh m)
|
||||||
/// oder er ist ohnehin gerade inaktiv (dann wird er für die Kopie kurz ausgeschaltet, ohne Folgen).
|
|
||||||
/// </summary>
|
|
||||||
static bool SafeToClone(Transform root)
|
|
||||||
{
|
{
|
||||||
if (!root.gameObject.activeInHierarchy) return true;
|
Vector3[] p = BindPositions(m);
|
||||||
return root.GetComponentsInChildren<MonoBehaviour>(true).Length == 0;
|
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 string PathOf(Transform t)
|
static Vector3[] BindPositions(Mesh m)
|
||||||
{
|
{
|
||||||
string path = t.name;
|
var p = new Vector3[BoneCount];
|
||||||
for (Transform p = t.parent; p != null; p = p.parent) path = p.name + "/" + path;
|
for (int i = 0; i < BoneCount; i++) p[i] = m.bindposes[i].inverse.GetColumn(3);
|
||||||
return path;
|
return p;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------- Instanz
|
// ---------------------------------------------------------------- Instanz
|
||||||
|
|
||||||
class Bone
|
GameObject model;
|
||||||
{
|
|
||||||
public Transform T;
|
|
||||||
public Quaternion RestRot; // relativ zum Spieler
|
|
||||||
public Vector3 RestPos; // relativ zum Spieler
|
|
||||||
public Vector3 RestDir; // zum Kind-Gelenk (Arme/Beine)
|
|
||||||
}
|
|
||||||
|
|
||||||
GameObject clone;
|
|
||||||
Transform owner;
|
Transform owner;
|
||||||
Bone pelvis, head;
|
readonly Transform[] bones = new Transform[BoneCount];
|
||||||
readonly List<Bone> spine = new List<Bone>();
|
readonly Vector3[] restPos = new Vector3[BoneCount]; // relativ zum Spieler
|
||||||
readonly Bone[] upperLeg = new Bone[2], lowerLeg = new Bone[2], foot = new Bone[2];
|
readonly Quaternion[] restRot = new Quaternion[BoneCount];
|
||||||
readonly Bone[] upperArm = new Bone[2], lowerArm = new Bone[2];
|
Vector3 restHipCenter;
|
||||||
Vector3 pelvisOffset;
|
|
||||||
public string Description { get; private set; }
|
public string Description { get; private set; }
|
||||||
|
|
||||||
/// <summary>Figur aus einer Spielvorlage bauen (variant wählt den Menschen). null, wenn keine passt.</summary>
|
/// <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)
|
public static GameCharacter Create(Transform owner, int variant)
|
||||||
{
|
{
|
||||||
List<SkinnedMeshRenderer> list = Templates();
|
if (!FindAssets()) return null;
|
||||||
if (list.Count == 0) return null;
|
|
||||||
var c = new GameCharacter();
|
var c = new GameCharacter();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (c.Build(owner, list[Mathf.Abs(variant) % list.Count])) return c;
|
c.Build(owner, Mathf.Abs(variant));
|
||||||
|
return c;
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (System.Exception e)
|
||||||
{
|
{
|
||||||
MscoLog.Warning("[MSCO] Player model could not be built: " + e.Message);
|
MscoLog.Warning("[MSCO] Player model could not be built: " + e.Message);
|
||||||
}
|
|
||||||
c.Destroy();
|
c.Destroy();
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bool Build(Transform owner, SkinnedMeshRenderer template)
|
void Build(Transform owner, int variant)
|
||||||
{
|
{
|
||||||
this.owner = owner;
|
this.owner = owner;
|
||||||
Transform tplRoot = CloneRoot(template);
|
Vector3[] bind = BindPositions(bodyMesh);
|
||||||
GameObject tpl = tplRoot.gameObject;
|
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
|
||||||
|
|
||||||
// Kopieren, ohne dass Skripte der Vorlage anlaufen (siehe SafeToClone)
|
model = new GameObject("MSCO_Model");
|
||||||
bool wasActive = tpl.activeSelf;
|
model.transform.SetParent(owner, false);
|
||||||
bool toggle = wasActive && !tpl.activeInHierarchy;
|
// Bind-Pose: X vorne, Z oben -> Spieler: Z vorne, Y oben
|
||||||
if (toggle) tpl.SetActive(false);
|
model.transform.localRotation = Quaternion.Inverse(Quaternion.LookRotation(Vector3.right, Vector3.forward));
|
||||||
try { clone = (GameObject)UnityEngine.Object.Instantiate(tpl); }
|
model.transform.localScale = Vector3.one * scale;
|
||||||
finally { if (toggle) tpl.SetActive(true); }
|
|
||||||
clone.name = "MSCO_Model";
|
|
||||||
Strip(clone);
|
|
||||||
|
|
||||||
SkinnedMeshRenderer smr = null;
|
Matrix4x4[] bindposes = bodyMesh.bindposes;
|
||||||
foreach (SkinnedMeshRenderer s in clone.GetComponentsInChildren<SkinnedMeshRenderer>(true))
|
for (int i = 0; i < BoneCount; i++)
|
||||||
if (s.sharedMesh == template.sharedMesh) { smr = s; break; }
|
|
||||||
if (smr == null) return false;
|
|
||||||
Description = "MSC character " + PathOf(template.transform);
|
|
||||||
|
|
||||||
// Knochen nach Namen
|
|
||||||
var byName = new Dictionary<string, Transform>();
|
|
||||||
foreach (Transform b in smr.bones) if (b != null && !byName.ContainsKey(b.name)) byName[b.name] = b;
|
|
||||||
|
|
||||||
// In die Bind-Pose (Ruhelage des Modells) setzen: Eltern vor Kindern
|
|
||||||
Matrix4x4[] bind = smr.sharedMesh.bindposes;
|
|
||||||
var order = new List<int>();
|
|
||||||
for (int i = 0; i < smr.bones.Length; i++) if (smr.bones[i] != null) order.Add(i);
|
|
||||||
order.Sort((a, b) => Depth(smr.bones[a]).CompareTo(Depth(smr.bones[b])));
|
|
||||||
foreach (int i in order)
|
|
||||||
{
|
{
|
||||||
Matrix4x4 m = smr.transform.localToWorldMatrix * bind[i].inverse;
|
var b = new GameObject("bone" + i).transform;
|
||||||
smr.bones[i].position = m.GetColumn(3);
|
b.SetParent(model.transform, false);
|
||||||
smr.bones[i].rotation = Quaternion.LookRotation(m.GetColumn(2), m.GetColumn(1));
|
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;
|
||||||
|
|
||||||
// Gliedmaßen geometrisch bestimmen: in jeder Kette (Becken -> ... -> Knöchel -> Zehen bzw. Rumpf -> ... -> Hand
|
var smr = model.AddComponent<SkinnedMeshRenderer>();
|
||||||
// -> Finger) sind die zwei längsten aufeinanderfolgenden Stücke Ober-/Unterschenkel bzw. Ober-/Unterarm.
|
smr.sharedMesh = bodyMesh;
|
||||||
// So ist egal, ob ein Knochen nach seinem Anfang oder seinem Ende benannt ist.
|
smr.bones = bones;
|
||||||
clone.transform.SetParent(owner, true);
|
smr.rootBone = bones[Pelvis];
|
||||||
Transform headT = byName["head"], pelvisT = byName["pelvis"];
|
smr.updateWhenOffscreen = true; // die Knochen bewegen sich weit weg von der gespeicherten Hülle
|
||||||
var trunk = new HashSet<Transform> { pelvisT };
|
Material shirt = shirts[(variant * 7 + 3) % shirts.Count];
|
||||||
for (Transform t = headT; t != null && t != pelvisT; t = t.parent) trunk.Add(t);
|
Material trousers = pants[(variant * 3 + 1) % pants.Count];
|
||||||
Transform[][] legs = { Limb(byName["ankle_left"], trunk), Limb(byName["ankle_right"], trunk) };
|
Material face = faces[(variant * 5 + 2) % faces.Count];
|
||||||
Transform[][] arms = { Limb(byName["hand_left"], trunk), Limb(byName["hand_right"], trunk) };
|
smr.sharedMaterials = new[] { shirt, trousers, face };
|
||||||
foreach (Transform[] l in legs) if (l == null) return false;
|
Description = "MSC body (" + shirt.name + ", " + trousers.name + ", " + face.name + ")";
|
||||||
foreach (Transform[] l in arms) if (l == null) return false;
|
|
||||||
|
|
||||||
Vector3 up = (headT.position - pelvisT.position).normalized;
|
|
||||||
Vector3 right = (arms[1][0].position - arms[0][0].position).normalized;
|
|
||||||
Vector3 forward = Vector3.Cross(right, up).normalized;
|
|
||||||
if (forward.sqrMagnitude < 0.5f) return false;
|
|
||||||
Quaternion facing = Quaternion.LookRotation(forward, up);
|
|
||||||
float legLength = (legs[0][1].position - legs[0][0].position).magnitude + (legs[0][2].position - legs[0][1].position).magnitude;
|
|
||||||
if (legLength < 0.01f) return false;
|
|
||||||
float scale = 0.84f / legLength; // Bein der MSCO-Figur (Oberschenkel + Schienbein + etwas Knöchel)
|
|
||||||
|
|
||||||
// Erst drehen und skalieren, dann Ruhelage relativ zum Spieler merken
|
|
||||||
clone.transform.localScale = clone.transform.localScale * scale;
|
|
||||||
clone.transform.rotation = owner.rotation * Quaternion.Inverse(facing) * clone.transform.rotation;
|
|
||||||
clone.transform.position += owner.position - pelvisT.position;
|
|
||||||
|
|
||||||
pelvis = MakeBone(pelvisT, null);
|
|
||||||
head = MakeBone(headT, null);
|
|
||||||
for (Transform s = headT.parent; s != null && s != pelvisT; s = s.parent) spine.Add(MakeBone(s, null));
|
|
||||||
for (int i = 0; i < 2; i++)
|
|
||||||
{
|
|
||||||
upperLeg[i] = MakeBone(legs[i][0], legs[i][1]);
|
|
||||||
lowerLeg[i] = MakeBone(legs[i][1], legs[i][2]);
|
|
||||||
foot[i] = MakeBone(legs[i][2], null);
|
|
||||||
upperArm[i] = MakeBone(arms[i][0], arms[i][1]);
|
|
||||||
lowerArm[i] = MakeBone(arms[i][1], arms[i][2]);
|
|
||||||
}
|
|
||||||
Vector3 hipCenter = (upperLeg[0].RestPos + upperLeg[1].RestPos) * 0.5f;
|
|
||||||
pelvisOffset = pelvis.RestPos - hipCenter;
|
|
||||||
|
|
||||||
smr.updateWhenOffscreen = true; // Knochen bewegen sich weit weg von der gespeicherten Hülle
|
|
||||||
foreach (Renderer r in clone.GetComponentsInChildren<Renderer>(true)) r.enabled = true;
|
|
||||||
clone.SetActive(true);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gliedmaße als [Anfang oberes Stück, Gelenk (Knie/Ellbogen), Ende (Knöchel/Handgelenk)]: Kette vom Rumpf bis zum
|
|
||||||
/// Ende (über end hinaus entlang des längsten Kindes), dann die zwei längsten aufeinanderfolgenden Stücke.
|
|
||||||
/// </summary>
|
|
||||||
static Transform[] Limb(Transform end, HashSet<Transform> trunk)
|
|
||||||
{
|
|
||||||
var chain = new List<Transform>();
|
|
||||||
for (Transform t = end; t != null && !trunk.Contains(t); t = t.parent) chain.Insert(0, t);
|
|
||||||
for (Transform t = end; t.childCount > 0; )
|
|
||||||
{
|
|
||||||
Transform next = null;
|
|
||||||
float best = -1f;
|
|
||||||
foreach (Transform c in t)
|
|
||||||
{
|
|
||||||
float d = (c.position - t.position).sqrMagnitude;
|
|
||||||
if (d > best) { best = d; next = c; }
|
|
||||||
}
|
|
||||||
chain.Add(next);
|
|
||||||
t = next;
|
|
||||||
}
|
|
||||||
if (chain.Count < 3) return null;
|
|
||||||
int bestK = -1;
|
|
||||||
float bestLen = 0f;
|
|
||||||
for (int k = 0; k + 2 < chain.Count; k++)
|
|
||||||
{
|
|
||||||
float len = Mathf.Min((chain[k + 1].position - chain[k].position).magnitude, (chain[k + 2].position - chain[k + 1].position).magnitude);
|
|
||||||
if (len > bestLen) { bestLen = len; bestK = k; }
|
|
||||||
}
|
|
||||||
return bestK < 0 ? null : new[] { chain[bestK], chain[bestK + 1], chain[bestK + 2] };
|
|
||||||
}
|
|
||||||
|
|
||||||
static int Depth(Transform t)
|
|
||||||
{
|
|
||||||
int d = 0;
|
|
||||||
for (; t != null; t = t.parent) d++;
|
|
||||||
return d;
|
|
||||||
}
|
|
||||||
|
|
||||||
Bone MakeBone(Transform t, Transform child)
|
|
||||||
{
|
|
||||||
var b = new Bone
|
|
||||||
{
|
|
||||||
T = t,
|
|
||||||
RestRot = Quaternion.Inverse(owner.rotation) * t.rotation,
|
|
||||||
RestPos = owner.InverseTransformPoint(t.position)
|
|
||||||
};
|
|
||||||
if (child != null) b.RestDir = owner.InverseTransformPoint(child.position) - b.RestPos;
|
|
||||||
return b;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Alles außer Aussehen entfernen (Skripte, Physik, Kollision, Ton, Animation).</summary>
|
|
||||||
static void Strip(GameObject go)
|
|
||||||
{
|
|
||||||
foreach (Joint j in go.GetComponentsInChildren<Joint>(true)) UnityEngine.Object.DestroyImmediate(j);
|
|
||||||
foreach (Component c in go.GetComponentsInChildren<Component>(true))
|
|
||||||
{
|
|
||||||
if (c == null || c is Transform || c is Renderer || c is MeshFilter) continue;
|
|
||||||
UnityEngine.Object.DestroyImmediate(c);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------- jedes Frame
|
// ---------------------------------------------------------------- jedes Frame
|
||||||
|
|
||||||
public void Apply(Rig rig)
|
public void Apply(Rig rig)
|
||||||
{
|
{
|
||||||
if (clone == null) return;
|
if (model == null) return;
|
||||||
Quaternion root = owner.rotation;
|
Quaternion root = owner.rotation;
|
||||||
Quaternion lean = Quaternion.FromToRotation(Vector3.up, rig.Up);
|
Quaternion lean = Quaternion.FromToRotation(Vector3.up, rig.Up);
|
||||||
|
Quaternion torso = root * lean;
|
||||||
|
|
||||||
pelvis.T.position = owner.TransformPoint(rig.Hip + lean * pelvisOffset);
|
foreach (int i in Torso) Place(i, TorsoPoint(rig, lean, restPos[i]), torso * restRot[i]);
|
||||||
pelvis.T.rotation = root * lean * pelvis.RestRot;
|
Place(Head, TorsoPoint(rig, lean, restPos[Head]), root * Quaternion.Euler(rig.Pitch, 0f, 0f) * restRot[Head]);
|
||||||
for (int i = spine.Count - 1; i >= 0; i--) spine[i].T.rotation = root * lean * spine[i].RestRot; // von unten nach oben
|
|
||||||
head.T.rotation = root * Quaternion.Euler(rig.Pitch, 0f, 0f) * head.RestRot;
|
|
||||||
|
|
||||||
for (int i = 0; i < 2; i++)
|
for (int s = 0; s < 2; s++)
|
||||||
{
|
{
|
||||||
Aim(upperLeg[i], rig.Knee[i] - rig.HipJoint[i], root);
|
// Bein: Hüfte am Becken, dann Richtung Knie bzw. Knöchel der MSCO-Figur
|
||||||
Aim(lowerLeg[i], rig.Ankle[i] - rig.Knee[i], root);
|
Vector3 hip = TorsoPoint(rig, lean, restPos[HipBone[s]]);
|
||||||
foot[i].T.rotation = root * foot[i].RestRot; // Füße bleiben flach
|
Quaternion thigh = AimDelta(HipBone[s], Knee[s], rig.Knee[s] - rig.HipJoint[s]);
|
||||||
Aim(upperArm[i], rig.Elbow[i] - rig.Shoulder[i], root);
|
Place(HipBone[s], hip, root * thigh * restRot[HipBone[s]]);
|
||||||
Aim(lowerArm[i], rig.Hand[i] - rig.Elbow[i], root);
|
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]]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void Aim(Bone b, Vector3 desired, Quaternion root)
|
/// <summary>Punkt des Oberkörpers: an der Hüfte der MSCO-Figur, mit ihr geneigt.</summary>
|
||||||
|
Vector3 TorsoPoint(Rig rig, Quaternion lean, Vector3 rest)
|
||||||
{
|
{
|
||||||
if (desired.sqrMagnitude < 1e-6f || b.RestDir.sqrMagnitude < 1e-6f) return;
|
return owner.TransformPoint(rig.Hip + lean * (rest - restHipCenter));
|
||||||
b.T.rotation = root * Quaternion.FromToRotation(b.RestDir, desired) * b.RestRot;
|
}
|
||||||
|
|
||||||
|
/// <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()
|
public void Destroy()
|
||||||
{
|
{
|
||||||
if (clone != null) UnityEngine.Object.Destroy(clone);
|
if (model != null) Object.Destroy(model);
|
||||||
clone = null;
|
model = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user