LANTERN feel pass (DR-052 + gap list): SoD facing, underwater feel, Bathynaut kit, walk/run gait, suit lamp + new Synty anim packs
- SoD facing: PlayerFacing = body-yaw only (move-facing / cast-turn / idle-hold); every fire direction re-sourced to FacingMath.ResolveAim (pre-code review blocking catch); TickWindowMath shared windows with Movement-skip; reticle/FX coupled to the damage direction; cursor-dash kept. - Underwater feel: sharpness 15->6, turn 720->360, MoveSpeed 6->4.2; TuningKnob 26-28 (0 = no-override sentinels, dev-protocol bump on DebugTuningReport); stride footsteps + silt + cadence floor; bubbles; underwater ambience bed + distant groans; camera drag + dev scroll zoom. - Bathynaut kit in-engine: dome/tank/shoulder-lamp + bare head grafted (GraftSmr rigid rebase, RecalculateTangents); EmissiveGloamSkinned shader (Rukhanka deformation); shoulder lamp CASTS (warm steady spot on body yaw). - Gait: two-ring walk/run FreeformDirectional tree (walk @0.35, run @1.0) + blended-natural StrideScale; additive Posture(Bank) + Lead(chest-lead) layers; idle = AnimationIdles Base; banking driven from facing turn rate; flat terrain (Env_SeabedKit seabed squashed - CC is planar). - New packs: Synty AnimationIdles + AnimationSwordCombat (combat pass queued) + SyntyPropBoneTool; four authored clips (sway/trudge/banks/lean) + Anim_Player_Underwater.blend + suit-kit FBX. - Validation: 411/411 EditMode green; Play smokes (server==client facing, Aim-true projectile, bank/stride live-sampled, lamp beam verified); pre-code + post-impl adversarial reviews applied. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -126,6 +126,471 @@ namespace ProjectM.EditorTools
|
||||
AssetDatabase.Refresh();
|
||||
}
|
||||
|
||||
/// <summary>LANTERN suit-attachment follow-up (DR-051): graft the Bathynaut kitbash attachments (brass
|
||||
/// diving-bell dome + porthole, back tank pack, shoulder dive-lamp — exported rigid-skinned to
|
||||
/// Head/Spine_03/Clavicle_L in Suit_Bathynaut_Kitbash.blend) onto Player.prefab IN PLACE (GUID preserved,
|
||||
/// visual-only: ghost surface unchanged, same class of edit as the P5 body swap). The kit FBX's two SMRs
|
||||
/// are re-bound onto the player's existing flattened skeleton by bone NAME. Brass rides the shared
|
||||
/// M_Skinned_Palette (AnimatedLitShader + PaletteAtlas); glow rides M_EmissiveGloam_WarmSkinned
|
||||
/// (steady warm = true light). Idempotent / re-runnable.</summary>
|
||||
[MenuItem("ProjectM/Animation/Player - Attach Bathynaut Kit (LANTERN)")]
|
||||
public static void AttachBathynautKit()
|
||||
{
|
||||
const string kitFbx = "Assets/_Project/Art/Models/SM_Suit_BathynautKit.fbx";
|
||||
const string brassMat = "Assets/_Project/Art/Materials/M_Skinned_Palette.mat";
|
||||
const string glowMat = "Assets/_Project/Art/Materials/M_EmissiveGloam_WarmSkinned.mat";
|
||||
const string bodyMat = "Assets/_Project/Materials/M_SuitFrame_Bathynaut_Animated.mat";
|
||||
const string barePrefab = "Assets/Synty/PolygonSciFiSpace/Prefabs/Characters/SM_Chr_SpaceSoldier_Male_01.prefab";
|
||||
const string output = "Assets/_Project/Prefabs/Player.prefab";
|
||||
|
||||
var kit = AssetDatabase.LoadAssetAtPath<GameObject>(kitFbx);
|
||||
var mBrass = AssetDatabase.LoadAssetAtPath<Material>(brassMat);
|
||||
var mGlow = AssetDatabase.LoadAssetAtPath<Material>(glowMat);
|
||||
var mBody = AssetDatabase.LoadAssetAtPath<Material>(bodyMat);
|
||||
if (kit == null) { Debug.LogError($"[PlayerRigTools] Kit FBX missing: {kitFbx}"); return; }
|
||||
if (mBrass == null) { Debug.LogError($"[PlayerRigTools] Brass material missing: {brassMat}"); return; }
|
||||
if (mGlow == null) { Debug.LogError($"[PlayerRigTools] Glow material missing: {glowMat}"); return; }
|
||||
|
||||
var root = PrefabUtility.LoadPrefabContents(output);
|
||||
try
|
||||
{
|
||||
// Bone map: every transform under the player root by name (the flattened Synty skeleton).
|
||||
var bones = new System.Collections.Generic.Dictionary<string, Transform>();
|
||||
foreach (var t in root.GetComponentsInChildren<Transform>(true))
|
||||
if (!bones.ContainsKey(t.name)) bones.Add(t.name, t);
|
||||
|
||||
foreach (var src in kit.GetComponentsInChildren<SkinnedMeshRenderer>(true))
|
||||
GraftSmr(root, bones, src, src.name.Contains("Glow") ? mGlow : mBrass);
|
||||
|
||||
// 07-15 operator fork: brass dome + BARE head (the kitbash look) — drop the Synty sci-fi helmet
|
||||
// and graft the bare head SMR so the face reads through the amber porthole up close.
|
||||
var helmet = root.transform.Find("SM_Chr_Attach_SpaceSoldier_Male_Helmet_01");
|
||||
if (helmet != null) Object.DestroyImmediate(helmet.gameObject);
|
||||
// Purge any wrong-variant head graft (the Synty container holds Female/Male heads) before re-grafting.
|
||||
for (int i = root.transform.childCount - 1; i >= 0; i--)
|
||||
{
|
||||
var c = root.transform.GetChild(i);
|
||||
if (c.name.StartsWith("SM_Chr_SpaceSoldier_Head_") && c.name != "SM_Chr_SpaceSoldier_Head_Male_01")
|
||||
Object.DestroyImmediate(c.gameObject);
|
||||
}
|
||||
if (root.transform.Find("SM_Chr_SpaceSoldier_Head_Male_01") == null)
|
||||
{
|
||||
var bare = AssetDatabase.LoadAssetAtPath<GameObject>(barePrefab);
|
||||
SkinnedMeshRenderer headSrc = null;
|
||||
if (bare != null)
|
||||
foreach (var s in bare.GetComponentsInChildren<SkinnedMeshRenderer>(true))
|
||||
if (s.name == "SM_Chr_SpaceSoldier_Head_Male_01") { headSrc = s; break; }
|
||||
if (headSrc != null && mBody != null) GraftSmr(root, bones, headSrc, mBody, rebase: false); // meter-scale Synty source + blend-skinned neck: native bindposes are correct
|
||||
else Debug.LogWarning("[PlayerRigTools] Bare head SMR (or body material) not found — helmet removed without a head.");
|
||||
}
|
||||
|
||||
PrefabUtility.SaveAsPrefabAsset(root, output);
|
||||
}
|
||||
finally
|
||||
{
|
||||
PrefabUtility.UnloadPrefabContents(root);
|
||||
}
|
||||
AssetDatabase.SaveAssets();
|
||||
AssetDatabase.Refresh();
|
||||
Debug.Log("[PlayerRigTools] Bathynaut kit attached to Player.prefab (in place).");
|
||||
}
|
||||
|
||||
/// <summary>Re-bind a source SkinnedMeshRenderer onto the player's existing flattened skeleton by bone
|
||||
/// NAME and drop it in as a fresh child (idempotent: replaces a same-named child). Blender dedup
|
||||
/// suffixes ("Finger_01.001" — the Synty rig duplicates L/R finger names) fall back to the base name;
|
||||
/// safe because the kit meshes only WEIGHT Head/Spine_03/Clavicle_L.</summary>
|
||||
static void GraftSmr(GameObject root, System.Collections.Generic.Dictionary<string, Transform> bones,
|
||||
SkinnedMeshRenderer src, Material material, bool rebase = true)
|
||||
{
|
||||
// Resolve the bone rebind FIRST (07-16 review): destroying the old child before validation meant a
|
||||
// bone-name mismatch on a re-run silently stripped the piece from the prefab.
|
||||
var srcBones = src.bones;
|
||||
var dst = new Transform[srcBones.Length];
|
||||
for (int i = 0; i < srcBones.Length; i++)
|
||||
{
|
||||
string bn = srcBones[i] != null ? srcBones[i].name : null;
|
||||
Transform t = null;
|
||||
if (bn != null && !bones.TryGetValue(bn, out t))
|
||||
{
|
||||
// Blender dedup suffix ("Finger_01.001" — the Synty rig duplicates L/R finger names): strip
|
||||
// and take the first base-name match. Safe: the kit only WEIGHTS Head/Spine_03/Clavicle_L.
|
||||
int dot = bn.LastIndexOf('.');
|
||||
if (dot > 0) bones.TryGetValue(bn.Substring(0, dot), out t);
|
||||
}
|
||||
if (t == null)
|
||||
{
|
||||
Debug.LogError($"[PlayerRigTools] Player skeleton missing bone '{bn ?? "<null>"}' for {src.name}; skipped (existing graft left untouched).");
|
||||
return;
|
||||
}
|
||||
dst[i] = t;
|
||||
}
|
||||
|
||||
Mesh mesh;
|
||||
if (!rebase)
|
||||
{
|
||||
// Source skeleton already matches the player's conventions (meter-scale Synty prefab, e.g. the
|
||||
// bare head): keep the native mesh + bindposes; only the bones array is remapped.
|
||||
mesh = src.sharedMesh;
|
||||
}
|
||||
else
|
||||
{
|
||||
// REBASE (07-15/16): a Blender FBX roundtrip imports cm bones under a 0.01 armature (Synty
|
||||
// prefabs are meter bones) so raw bindpose reuse explodes ×100 on rebind. Bake the SOURCE rest
|
||||
// pose into world-space vertices and compute bindposes as the inverse of the RIGID
|
||||
// (scale-stripped) rest matrices — self-consistent feet-at-0 rest space regardless of exporter
|
||||
// scaling; the player's Root feet-offset applies at runtime exactly like the body meshes.
|
||||
var srcMesh = src.sharedMesh;
|
||||
var srcBind = srcMesh.bindposes;
|
||||
var restWorld = new Matrix4x4[srcBones.Length];
|
||||
var skinRest = new Matrix4x4[srcBones.Length];
|
||||
for (int i = 0; i < srcBones.Length; i++)
|
||||
{
|
||||
restWorld[i] = srcBones[i].localToWorldMatrix;
|
||||
skinRest[i] = restWorld[i] * srcBind[i];
|
||||
}
|
||||
var verts = srcMesh.vertices;
|
||||
var norms = srcMesh.normals;
|
||||
var weights = srcMesh.boneWeights;
|
||||
var newVerts = new Vector3[verts.Length];
|
||||
var newNorms = new Vector3[norms.Length];
|
||||
for (int v = 0; v < verts.Length; v++)
|
||||
{
|
||||
// The rigid rebase is only valid for 100%-single-bone skins (07-16 review guard): a
|
||||
// blend-skinned mesh (e.g. the bare head's neck weights) would distort — graft it rebase:false.
|
||||
if (weights[v].weight0 < 0.999f)
|
||||
{
|
||||
Debug.LogError($"[PlayerRigTools] {src.name} vertex {v} is blend-skinned (w0={weights[v].weight0:0.###}); rigid rebase would distort — skipped (graft with rebase:false instead).");
|
||||
return;
|
||||
}
|
||||
int b = weights[v].boneIndex0;
|
||||
newVerts[v] = skinRest[b].MultiplyPoint3x4(verts[v]);
|
||||
newNorms[v] = skinRest[b].rotation * norms[v];
|
||||
}
|
||||
var newBind = new Matrix4x4[srcBones.Length];
|
||||
for (int i = 0; i < srcBones.Length; i++)
|
||||
{
|
||||
var m = restWorld[i];
|
||||
newBind[i] = Matrix4x4.TRS((Vector3)m.GetColumn(3), m.rotation, Vector3.one).inverse;
|
||||
}
|
||||
|
||||
// Persist the rebased mesh GUID-stably (Clear+refill keeps an existing asset's GUID on re-runs).
|
||||
string meshPath = $"Assets/_Project/Art/Models/Rebased_{src.name}.asset";
|
||||
mesh = AssetDatabase.LoadAssetAtPath<Mesh>(meshPath);
|
||||
bool fresh = mesh == null;
|
||||
if (fresh) mesh = new Mesh();
|
||||
else mesh.Clear();
|
||||
mesh.name = "Rebased_" + src.name;
|
||||
mesh.vertices = newVerts;
|
||||
mesh.normals = newNorms;
|
||||
mesh.uv = srcMesh.uv;
|
||||
mesh.subMeshCount = srcMesh.subMeshCount;
|
||||
for (int s = 0; s < srcMesh.subMeshCount; s++) mesh.SetTriangles(srcMesh.GetTriangles(s), s);
|
||||
mesh.boneWeights = weights;
|
||||
mesh.bindposes = newBind;
|
||||
mesh.RecalculateBounds();
|
||||
mesh.RecalculateTangents(); // Rukhanka/BRG registration fails on a tangent-less mesh (BatchMeshID missing)
|
||||
if (fresh) AssetDatabase.CreateAsset(mesh, meshPath);
|
||||
else EditorUtility.SetDirty(mesh);
|
||||
}
|
||||
|
||||
var old = root.transform.Find(src.name);
|
||||
if (old != null) Object.DestroyImmediate(old.gameObject);
|
||||
|
||||
var go = new GameObject(src.name);
|
||||
go.transform.SetParent(root.transform, false);
|
||||
var smr = go.AddComponent<SkinnedMeshRenderer>();
|
||||
smr.sharedMesh = mesh;
|
||||
smr.bones = dst;
|
||||
smr.rootBone = src.rootBone != null && bones.TryGetValue(src.rootBone.name, out var rb) ? rb : dst[0];
|
||||
if (!rebase)
|
||||
{
|
||||
smr.localBounds = src.localBounds;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Bounds in rootBone space: rebased (feet-at-0 world) verts through the RIGID root rest inverse.
|
||||
var rootRest = src.rootBone != null ? src.rootBone.localToWorldMatrix : srcBones[0].localToWorldMatrix;
|
||||
var rootRestInv = Matrix4x4.TRS((Vector3)rootRest.GetColumn(3), rootRest.rotation, Vector3.one).inverse;
|
||||
var mv = mesh.vertices;
|
||||
var bmin = new Vector3(float.MaxValue, float.MaxValue, float.MaxValue);
|
||||
var bmax = new Vector3(float.MinValue, float.MinValue, float.MinValue);
|
||||
for (int v = 0; v < mv.Length; v++)
|
||||
{
|
||||
var p = rootRestInv.MultiplyPoint3x4(mv[v]);
|
||||
bmin = Vector3.Min(bmin, p); bmax = Vector3.Max(bmax, p);
|
||||
}
|
||||
var lb = new Bounds(); lb.SetMinMax(bmin, bmax);
|
||||
smr.localBounds = lb;
|
||||
}
|
||||
smr.sharedMaterials = new[] { material };
|
||||
Debug.Log($"[PlayerRigTools] Grafted {src.name} ({mesh.vertexCount}v, {dst.Length} bones{(rebase ? ", rebased" : "")}).");
|
||||
}
|
||||
|
||||
/// <summary>07-15 underwater feel: heavier gait — slow the Locomotion blend-state playback and lengthen
|
||||
/// the Idle⇄Locomotion transition blends (weightier starts/stops). The velocity-side drift (sharpness
|
||||
/// 15→6) already slows the Speed/MoveX/MoveZ ramps; this makes the cycle itself read heavy. Uses the
|
||||
/// AnimatorController API per the manage_animation gotcha. Idempotent / re-runnable.</summary>
|
||||
[MenuItem("ProjectM/Animation/Player - Retime Locomotion Gait (LANTERN)")]
|
||||
public static void RetimeLocomotionGait()
|
||||
{
|
||||
const float gaitSpeed = 0.85f;
|
||||
const float blend = 0.25f;
|
||||
var ac = AssetDatabase.LoadAssetAtPath<AnimatorController>(PlayerController);
|
||||
if (ac == null) { Debug.LogError($"[PlayerRigTools] Controller missing: {PlayerController}"); return; }
|
||||
|
||||
var sm = ac.layers[0].stateMachine;
|
||||
int retimed = 0, blends = 0;
|
||||
foreach (var cs in sm.states)
|
||||
{
|
||||
if (cs.state.name == "Locomotion" && !Mathf.Approximately(cs.state.speed, gaitSpeed))
|
||||
{
|
||||
cs.state.speed = gaitSpeed;
|
||||
retimed++;
|
||||
}
|
||||
foreach (var tr in cs.state.transitions)
|
||||
{
|
||||
if (tr.destinationState == null) continue;
|
||||
bool idleLoco = (cs.state.name == "Idle" && tr.destinationState.name == "Locomotion")
|
||||
|| (cs.state.name == "Locomotion" && tr.destinationState.name == "Idle");
|
||||
if (idleLoco && !Mathf.Approximately(tr.duration, blend)) { tr.duration = blend; blends++; }
|
||||
}
|
||||
}
|
||||
EditorUtility.SetDirty(ac);
|
||||
AssetDatabase.SaveAssets();
|
||||
Debug.Log($"[PlayerRigTools] Locomotion gait retimed (speed {gaitSpeed}: {retimed} state(s); idle⇄locomotion blends {blend}s: {blends} transition(s)).");
|
||||
}
|
||||
|
||||
/// <summary>07-16 gap-list: configure the four Blender underwater-feel clips (A_Idle_Sway, A_Walk_Trudge,
|
||||
/// A_Bank_L/R) — HUMANOID + CreateFromThisModel (CopyFromOther fails on Blender's Armature node), root
|
||||
/// motion baked into pose (lock+keepOriginal ×3 — the CC owns transforms), loops; the bank poses import
|
||||
/// as ADDITIVE (playback frames 8-12 held pose, additive reference = frame 1's base stance) for the
|
||||
/// Posture layer. Idempotent / re-runnable.</summary>
|
||||
[MenuItem("ProjectM/Animation/Player - 1 Import Underwater Clips (LANTERN)")]
|
||||
public static void ImportUnderwaterClips()
|
||||
{
|
||||
var specs = new (string file, float first, float last, bool additive)[]
|
||||
{
|
||||
("A_Idle_Sway", 1f, 121f, false),
|
||||
("A_Walk_Trudge", 1f, 33f, false), // v2: 32f long-stride cycle (foot-skate fix)
|
||||
("A_Bank_L", 8f, 12f, true),
|
||||
("A_Bank_R", 8f, 12f, true),
|
||||
};
|
||||
foreach (var s in specs)
|
||||
{
|
||||
string path = $"Assets/_Project/Animation/Authored/{s.file}.fbx";
|
||||
var imp = AssetImporter.GetAtPath(path) as ModelImporter;
|
||||
if (imp == null) { Debug.LogError($"[PlayerRigTools] Missing clip fbx: {path}"); continue; }
|
||||
imp.animationType = ModelImporterAnimationType.Human;
|
||||
imp.avatarSetup = ModelImporterAvatarSetup.CreateFromThisModel;
|
||||
imp.importAnimation = true;
|
||||
imp.SaveAndReimport(); // two-pass: takes only enumerate AFTER the rig import runs
|
||||
var clips = imp.defaultClipAnimations;
|
||||
if (clips.Length == 0) { Debug.LogError($"[PlayerRigTools] No takes in {path}"); continue; }
|
||||
var c = clips[0];
|
||||
c.name = s.file;
|
||||
c.firstFrame = s.first;
|
||||
c.lastFrame = s.last;
|
||||
c.loopTime = true;
|
||||
c.lockRootRotation = true; c.lockRootHeightY = true; c.lockRootPositionXZ = true;
|
||||
c.keepOriginalOrientation = true; c.keepOriginalPositionY = true; c.keepOriginalPositionXZ = true;
|
||||
if (s.additive)
|
||||
{
|
||||
c.hasAdditiveReferencePose = true;
|
||||
c.additiveReferencePoseFrame = 1f; // the base stance key — the layer plays bank-minus-stance
|
||||
}
|
||||
imp.clipAnimations = new[] { c };
|
||||
imp.SaveAndReimport();
|
||||
Debug.Log($"[PlayerRigTools] Imported {s.file} (frames {s.first}-{s.last}{(s.additive ? ", additive" : "")}).");
|
||||
}
|
||||
|
||||
// A_Lean_Fwd: ONE take -> TWO additive clips (the held lean + a zero-delta companion so the Lead
|
||||
// layer's 1D Speed tree has a neutral end at 0).
|
||||
{
|
||||
string path = "Assets/_Project/Animation/Authored/A_Lean_Fwd.fbx";
|
||||
var imp = AssetImporter.GetAtPath(path) as ModelImporter;
|
||||
if (imp != null)
|
||||
{
|
||||
imp.animationType = ModelImporterAnimationType.Human;
|
||||
imp.avatarSetup = ModelImporterAvatarSetup.CreateFromThisModel;
|
||||
imp.importAnimation = true;
|
||||
imp.SaveAndReimport();
|
||||
var takes = imp.defaultClipAnimations;
|
||||
if (takes.Length > 0)
|
||||
{
|
||||
ModelImporterClipAnimation Mk(string n, float f0, float f1)
|
||||
{
|
||||
var c = imp.defaultClipAnimations[0];
|
||||
c.name = n; c.firstFrame = f0; c.lastFrame = f1; c.loopTime = true;
|
||||
c.lockRootRotation = true; c.lockRootHeightY = true; c.lockRootPositionXZ = true;
|
||||
c.keepOriginalOrientation = true; c.keepOriginalPositionY = true; c.keepOriginalPositionXZ = true;
|
||||
c.hasAdditiveReferencePose = true; c.additiveReferencePoseFrame = 1f;
|
||||
return c;
|
||||
}
|
||||
imp.clipAnimations = new[] { Mk("A_Lean_Fwd", 8f, 12f), Mk("A_Lean_Zero", 1f, 2f) };
|
||||
imp.SaveAndReimport();
|
||||
Debug.Log("[PlayerRigTools] Imported A_Lean_Fwd + A_Lean_Zero (additive pair).");
|
||||
}
|
||||
else Debug.LogError("[PlayerRigTools] No takes in A_Lean_Fwd.fbx");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static AnimationClip LoadAuthoredClip(string file)
|
||||
{
|
||||
foreach (var o in AssetDatabase.LoadAllAssetsAtPath($"Assets/_Project/Animation/Authored/{file}.fbx"))
|
||||
if (o is AnimationClip clip && !clip.name.StartsWith("__preview")) return clip;
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>07-16 gap-list: wire the underwater clips into AC_PlayerTopDown — Idle state → A_Idle_Sway
|
||||
/// (buoyant sway replaces the static idle); the Locomotion 2D tree's FORWARD motion → A_Walk_Trudge
|
||||
/// (chest-lead heavy walk; strafe/back clips stay Synty — they mostly show mid-cast); plus an ADDITIVE
|
||||
/// "Posture" layer with a 1D Bank tree (A_Bank_L @ -1 … A_Bank_R @ +1, symmetric so Bank=0 cancels to
|
||||
/// neutral) driven by PlayerAnimationDriveSystem's turn-rate Bank param. Idempotent.</summary>
|
||||
[MenuItem("ProjectM/Animation/Player - 2 Wire Underwater Clips (LANTERN)")]
|
||||
public static void WireUnderwaterFeelClips()
|
||||
{
|
||||
var ac = AssetDatabase.LoadAssetAtPath<AnimatorController>(PlayerController);
|
||||
if (ac == null) { Debug.LogError($"[PlayerRigTools] Controller missing: {PlayerController}"); return; }
|
||||
var bankL = LoadAuthoredClip("A_Bank_L");
|
||||
var bankR = LoadAuthoredClip("A_Bank_R");
|
||||
if (bankL == null || bankR == null)
|
||||
{ Debug.LogError("[PlayerRigTools] Underwater clips not found — run Import Underwater Clips first."); return; }
|
||||
|
||||
// 07-16d (operator): idles come from the AnimationIdles pack (professionally-authored base loop).
|
||||
AnimationClip packIdle = null;
|
||||
foreach (var o in AssetDatabase.LoadAllAssetsAtPath("Assets/Synty/AnimationIdles/Animations/Polygon/Masculine/Base/Stances/A_POLY_IDL_Base_Masc.fbx"))
|
||||
if (o is AnimationClip pcl && !pcl.name.StartsWith("__preview")) packIdle = pcl;
|
||||
if (packIdle == null) Debug.LogError("[PlayerRigTools] AnimationIdles base idle not found — Idle keeps its current motion.");
|
||||
|
||||
const string locoDir = "Assets/Synty/AnimationBaseLocomotion/Animations/Polygon/Masculine/Locomotion/";
|
||||
AnimationClip Clip(string sub, string file)
|
||||
{
|
||||
foreach (var o in AssetDatabase.LoadAllAssetsAtPath(locoDir + sub + "/" + file + ".fbx"))
|
||||
if (o is AnimationClip cl && !cl.name.StartsWith("__preview")) return cl;
|
||||
Debug.LogError($"[PlayerRigTools] Locomotion clip missing: {sub}/{file}");
|
||||
return null;
|
||||
}
|
||||
|
||||
// 07-16e TWO-RING gait tree: MoveX/MoveZ magnitude = speed/MoveSpeed, so a WALK ring at the walk
|
||||
// clip's natural fraction (1.46/4.2 MoveSpeed ≈ 0.35) and a RUN ring at full deflection blend
|
||||
// walk→run as the underwater accel ramps velocity — "walk first, run when held". StrideScale
|
||||
// fine-corrects residual cadence against the blended natural (PlayerAnimationDriveSystem).
|
||||
const float walkRing = 0.35f; // = FeelConfig.TrudgeNaturalSpeed / Character_Default MoveSpeed (4.2)
|
||||
var dirs = new (Vector2 dir, string walk, string run)[]
|
||||
{
|
||||
(new Vector2(0f, 1f), "A_Walk_FwdStrafeF_Masc", "A_Run_FwdStrafeF_Masc"),
|
||||
(new Vector2(0.7f, 0.7f), "A_Walk_FwdStrafeFR_Masc", "A_Run_FwdStrafeFR_Masc"),
|
||||
(new Vector2(1f, 0f), "A_Walk_FwdStrafeR_Masc", "A_Run_FwdStrafeR_Masc"),
|
||||
(new Vector2(0.7f, -0.7f), "A_Walk_BckStrafeBR_Masc", "A_Run_FwdStrafeBR_Masc"),
|
||||
(new Vector2(0f, -1f), "A_Walk_BckStrafeB_Masc", "A_Run_BckStrafeB_Masc"),
|
||||
(new Vector2(-0.7f, -0.7f), "A_Walk_BckStrafeBL_Masc", "A_Run_BckStrafeBL_Masc"),
|
||||
(new Vector2(-1f, 0f), "A_Walk_FwdStrafeL_Masc", "A_Run_FwdStrafeL_Masc"),
|
||||
(new Vector2(-0.7f, 0.7f), "A_Walk_FwdStrafeFL_Masc", "A_Run_FwdStrafeFL_Masc"),
|
||||
};
|
||||
|
||||
var sm = ac.layers[0].stateMachine;
|
||||
foreach (var cs in sm.states)
|
||||
{
|
||||
if (cs.state.name == "Idle")
|
||||
{
|
||||
if (packIdle != null) cs.state.motion = packIdle;
|
||||
}
|
||||
else if (cs.state.name == "Locomotion" && cs.state.motion is BlendTree tree)
|
||||
{
|
||||
cs.state.speed = 1f;
|
||||
cs.state.speedParameterActive = true;
|
||||
cs.state.speedParameter = "StrideScale";
|
||||
tree.useAutomaticThresholds = false;
|
||||
tree.children = new UnityEditor.Animations.ChildMotion[0]; // full rebuild (idempotent)
|
||||
if (packIdle != null) tree.AddChild(packIdle, Vector2.zero);
|
||||
foreach (var d in dirs)
|
||||
{
|
||||
var w = Clip("Walk", d.walk);
|
||||
var r = Clip("Run", d.run);
|
||||
if (w != null) tree.AddChild(w, d.dir.normalized * walkRing);
|
||||
if (r != null) tree.AddChild(r, d.dir);
|
||||
}
|
||||
Debug.Log($"[PlayerRigTools] Locomotion rebuilt: idle center + walk ring @{walkRing} + run ring @1.0 ({tree.children.Length} children).");
|
||||
}
|
||||
}
|
||||
|
||||
if (!AnimRigUtil.HasParam(ac, "Bank"))
|
||||
ac.AddParameter("Bank", AnimatorControllerParameterType.Float);
|
||||
if (!AnimRigUtil.HasParam(ac, "StrideScale"))
|
||||
{
|
||||
ac.AddParameter("StrideScale", AnimatorControllerParameterType.Float);
|
||||
var ps = ac.parameters;
|
||||
for (int i = 0; i < ps.Length; i++) if (ps[i].name == "StrideScale") ps[i].defaultFloat = 1f;
|
||||
ac.parameters = ps;
|
||||
}
|
||||
|
||||
// Additive Posture layer (Bank: symmetric poses cancel at 0).
|
||||
int postureIdx = EnsureAdditiveLayer(ac, "Posture");
|
||||
var psm = ac.layers[postureIdx].stateMachine;
|
||||
AnimatorState bankState = null;
|
||||
foreach (var cs in psm.states) if (cs.state.name == "Bank") bankState = cs.state;
|
||||
if (bankState == null) bankState = psm.AddState("Bank");
|
||||
psm.defaultState = bankState;
|
||||
if (!(bankState.motion is BlendTree))
|
||||
{
|
||||
var bankTree = new BlendTree { name = "BankTree", blendType = BlendTreeType.Simple1D, blendParameter = "Bank", useAutomaticThresholds = false, hideFlags = HideFlags.HideInHierarchy };
|
||||
AssetDatabase.AddObjectToAsset(bankTree, ac);
|
||||
bankTree.AddChild(bankL, -1f);
|
||||
bankTree.AddChild(bankR, 1f);
|
||||
bankState.motion = bankTree;
|
||||
}
|
||||
bankState.writeDefaultValues = false;
|
||||
|
||||
// Additive Lead layer (underwater chest-lead scales with Speed).
|
||||
var leanFwd = LoadAuthoredClip("A_Lean_Fwd");
|
||||
AnimationClip leanZero = null;
|
||||
foreach (var o in AssetDatabase.LoadAllAssetsAtPath("Assets/_Project/Animation/Authored/A_Lean_Fwd.fbx"))
|
||||
if (o is AnimationClip cl && cl.name == "A_Lean_Zero") leanZero = cl;
|
||||
if (leanFwd != null && leanZero != null)
|
||||
{
|
||||
int leadIdx = EnsureAdditiveLayer(ac, "Lead");
|
||||
var lsm = ac.layers[leadIdx].stateMachine;
|
||||
AnimatorState leadState = null;
|
||||
foreach (var cs in lsm.states) if (cs.state.name == "Lean") leadState = cs.state;
|
||||
if (leadState == null) leadState = lsm.AddState("Lean");
|
||||
lsm.defaultState = leadState;
|
||||
if (!(leadState.motion is BlendTree))
|
||||
{
|
||||
var leadTree = new BlendTree { name = "LeadTree", blendType = BlendTreeType.Simple1D, blendParameter = "Speed", useAutomaticThresholds = false, hideFlags = HideFlags.HideInHierarchy };
|
||||
AssetDatabase.AddObjectToAsset(leadTree, ac);
|
||||
leadTree.AddChild(leanZero, 0.1f);
|
||||
leadTree.AddChild(leanFwd, 0.85f);
|
||||
leadState.motion = leadTree;
|
||||
}
|
||||
leadState.writeDefaultValues = false;
|
||||
}
|
||||
else Debug.LogError("[PlayerRigTools] Lean clips missing — run Import Underwater Clips first.");
|
||||
|
||||
EditorUtility.SetDirty(ac);
|
||||
AssetDatabase.SaveAssets();
|
||||
Debug.Log("[PlayerRigTools] Underwater clips wired v5 (pack idle, walk+run rings + StrideScale, Bank + Lead additive layers).");
|
||||
}
|
||||
|
||||
static int EnsureAdditiveLayer(AnimatorController ac, string name)
|
||||
{
|
||||
int idx = -1;
|
||||
for (int i = 0; i < ac.layers.Length; i++) if (ac.layers[i].name == name) idx = i;
|
||||
if (idx < 0)
|
||||
{
|
||||
ac.AddLayer(name);
|
||||
idx = ac.layers.Length - 1;
|
||||
}
|
||||
var layers = ac.layers; // array copy — modify + assign back
|
||||
layers[idx].blendingMode = UnityEditor.Animations.AnimatorLayerBlendingMode.Additive;
|
||||
layers[idx].defaultWeight = 1f;
|
||||
ac.layers = layers;
|
||||
return idx;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
static AnimationClip FindIdleClip(AnimatorController ac)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user