479 lines
16 KiB
C#
479 lines
16 KiB
C#
|
|
using UnityEngine;
|
||
|
|
using System.Collections.Generic;
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Height scaling for GKC player characters.
|
||
|
|
/// NEW APPROACH: Instead of scaling COM/capsule/camera/IK down,
|
||
|
|
/// simply SCALES THE MODEL UP (or down) to match GKC's standard character height.
|
||
|
|
/// This way capsule, camera, IK, weapons, ground detection — everything stays at
|
||
|
|
/// GKC default values and just works. Only the model transform changes.
|
||
|
|
/// </summary>
|
||
|
|
public class characterHeightScaler : MonoBehaviour
|
||
|
|
{
|
||
|
|
[Header ("Auto-Detect Settings")]
|
||
|
|
[Space]
|
||
|
|
|
||
|
|
[Tooltip ("When enabled, the system automatically measures your FBX model height " +
|
||
|
|
"and scales it to match the GKC standard height. No manual slider needed.")]
|
||
|
|
public bool autoDetectHeight = true;
|
||
|
|
|
||
|
|
[Tooltip ("The standard/reference height in Unity units for the default GKC character. " +
|
||
|
|
"Typically 1.8 units (average humanoid).")]
|
||
|
|
public float gkcStandardHeight = 1.8f;
|
||
|
|
|
||
|
|
[Tooltip ("The detected height of the current FBX model in Unity units (before scaling).")]
|
||
|
|
public float detectedModelHeight;
|
||
|
|
|
||
|
|
[Tooltip ("The calculated model scale to match GKC standard height.\n" +
|
||
|
|
"= gkcStandardHeight / detectedModelHeight")]
|
||
|
|
public float modelScaleMultiplier = 1.0f;
|
||
|
|
|
||
|
|
[Space]
|
||
|
|
[Header ("Manual Scale (when Auto-Detect is OFF)")]
|
||
|
|
[Space]
|
||
|
|
|
||
|
|
[Range (0.3f, 3.0f)]
|
||
|
|
[Tooltip ("Manual scale multiplier for the model. Used only when autoDetectHeight is OFF.")]
|
||
|
|
public float characterHeightScale = 1.0f;
|
||
|
|
|
||
|
|
public float minHeightScale = 0.3f;
|
||
|
|
public float maxHeightScale = 3.0f;
|
||
|
|
|
||
|
|
[Space]
|
||
|
|
[Header ("State")]
|
||
|
|
[Space]
|
||
|
|
|
||
|
|
public bool heightBaseValuesCaptured;
|
||
|
|
public bool showDebugLog;
|
||
|
|
|
||
|
|
[Space]
|
||
|
|
[Header ("Captured Base Values")]
|
||
|
|
[Space]
|
||
|
|
|
||
|
|
public Vector3 baseModelLocalScale;
|
||
|
|
public Vector3 baseModelLocalPosition;
|
||
|
|
public Transform modelTransform;
|
||
|
|
|
||
|
|
[Tooltip ("Small vertical offset to fine-tune foot alignment (in Unity units). " +
|
||
|
|
"Positive = raise model, Negative = lower model.")]
|
||
|
|
public float footAlignmentOffset = 0.0f;
|
||
|
|
|
||
|
|
|
||
|
|
// Internal references
|
||
|
|
playerComponentsManager componentsManager;
|
||
|
|
playerController playerControllerManager;
|
||
|
|
IKSystem IKManager;
|
||
|
|
IKFootSystem IKFootManager;
|
||
|
|
Transform COMTransform;
|
||
|
|
|
||
|
|
|
||
|
|
// ==================== AUTO-DETECT HEIGHT ====================
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Measures the FBX model height using multiple methods.
|
||
|
|
/// Returns the height in Unity units.
|
||
|
|
/// </summary>
|
||
|
|
public float detectModelHeight (GameObject model)
|
||
|
|
{
|
||
|
|
if (model == null) {
|
||
|
|
Debug.LogWarning ("characterHeightScaler: No model provided for height detection.");
|
||
|
|
return gkcStandardHeight;
|
||
|
|
}
|
||
|
|
|
||
|
|
float measuredHeight = 0f;
|
||
|
|
string method = "none";
|
||
|
|
|
||
|
|
Animator animator = model.GetComponentInChildren<Animator> ();
|
||
|
|
|
||
|
|
// --- Method 1: Animator humanoid bones (head to foot) ---
|
||
|
|
if (animator != null && animator.isHuman && animator.avatar != null && animator.avatar.isHuman) {
|
||
|
|
Transform head = animator.GetBoneTransform (HumanBodyBones.Head);
|
||
|
|
Transform leftFoot = animator.GetBoneTransform (HumanBodyBones.LeftFoot);
|
||
|
|
Transform rightFoot = animator.GetBoneTransform (HumanBodyBones.RightFoot);
|
||
|
|
|
||
|
|
if (head != null && (leftFoot != null || rightFoot != null)) {
|
||
|
|
Transform foot = leftFoot != null ? leftFoot : rightFoot;
|
||
|
|
measuredHeight = head.position.y - foot.position.y;
|
||
|
|
|
||
|
|
// Head bone is at eye level, add ~10% for top of head
|
||
|
|
measuredHeight *= 1.1f;
|
||
|
|
method = "Animator bones (head-to-foot)";
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// --- Method 2: Animator humanScale ---
|
||
|
|
if (measuredHeight <= 0.01f && animator != null && animator.isHuman) {
|
||
|
|
measuredHeight = animator.humanScale * 1.8f;
|
||
|
|
method = "Animator humanScale";
|
||
|
|
}
|
||
|
|
|
||
|
|
// --- Method 3: SkinnedMeshRenderer bounds ---
|
||
|
|
if (measuredHeight <= 0.01f) {
|
||
|
|
SkinnedMeshRenderer[] skinnedRenderers = model.GetComponentsInChildren<SkinnedMeshRenderer> ();
|
||
|
|
|
||
|
|
if (skinnedRenderers.Length > 0) {
|
||
|
|
Bounds combinedBounds = new Bounds (skinnedRenderers [0].bounds.center, Vector3.zero);
|
||
|
|
|
||
|
|
for (int i = 0; i < skinnedRenderers.Length; i++) {
|
||
|
|
combinedBounds.Encapsulate (skinnedRenderers [i].bounds);
|
||
|
|
}
|
||
|
|
|
||
|
|
measuredHeight = combinedBounds.size.y;
|
||
|
|
method = "SkinnedMeshRenderer bounds";
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// --- Method 4: Regular Renderer bounds ---
|
||
|
|
if (measuredHeight <= 0.01f) {
|
||
|
|
Renderer[] renderers = model.GetComponentsInChildren<Renderer> ();
|
||
|
|
|
||
|
|
if (renderers.Length > 0) {
|
||
|
|
Bounds combinedBounds = new Bounds (renderers [0].bounds.center, Vector3.zero);
|
||
|
|
|
||
|
|
for (int i = 0; i < renderers.Length; i++) {
|
||
|
|
combinedBounds.Encapsulate (renderers [i].bounds);
|
||
|
|
}
|
||
|
|
|
||
|
|
measuredHeight = combinedBounds.size.y;
|
||
|
|
method = "Renderer bounds";
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// --- Fallback ---
|
||
|
|
if (measuredHeight <= 0.01f) {
|
||
|
|
measuredHeight = gkcStandardHeight;
|
||
|
|
method = "fallback (assuming standard)";
|
||
|
|
}
|
||
|
|
|
||
|
|
if (showDebugLog) {
|
||
|
|
Debug.Log ("characterHeightScaler: DETECT '" + method + "'"
|
||
|
|
+ " | measured=" + measuredHeight.ToString ("F3")
|
||
|
|
+ " | standard=" + gkcStandardHeight.ToString ("F3"));
|
||
|
|
}
|
||
|
|
|
||
|
|
return measuredHeight;
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Auto-detects model height and calculates the scale multiplier
|
||
|
|
/// to make the model match GKC standard height.
|
||
|
|
/// scale = gkcStandardHeight / detectedModelHeight
|
||
|
|
/// </summary>
|
||
|
|
public float autoDetectAndSetScale ()
|
||
|
|
{
|
||
|
|
resolveModelTransform ();
|
||
|
|
|
||
|
|
if (modelTransform == null) {
|
||
|
|
Debug.LogWarning ("characterHeightScaler: Cannot auto-detect — no model transform found.");
|
||
|
|
return 1.0f;
|
||
|
|
}
|
||
|
|
|
||
|
|
detectedModelHeight = detectModelHeight (modelTransform.gameObject);
|
||
|
|
|
||
|
|
if (detectedModelHeight > 0.01f) {
|
||
|
|
modelScaleMultiplier = gkcStandardHeight / detectedModelHeight;
|
||
|
|
modelScaleMultiplier = Mathf.Clamp (modelScaleMultiplier, minHeightScale, maxHeightScale);
|
||
|
|
} else {
|
||
|
|
modelScaleMultiplier = 1.0f;
|
||
|
|
}
|
||
|
|
|
||
|
|
characterHeightScale = modelScaleMultiplier;
|
||
|
|
|
||
|
|
if (showDebugLog) {
|
||
|
|
Debug.Log ("characterHeightScaler: AUTO-DETECT RESULT"
|
||
|
|
+ " | modelHeight=" + detectedModelHeight.ToString ("F3")
|
||
|
|
+ " | standardHeight=" + gkcStandardHeight.ToString ("F3")
|
||
|
|
+ " | scaleMultiplier=" + modelScaleMultiplier.ToString ("F3")
|
||
|
|
+ " (model will be scaled UP by " + modelScaleMultiplier.ToString ("F2") + "x)");
|
||
|
|
}
|
||
|
|
|
||
|
|
return modelScaleMultiplier;
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Full automatic: capture base values, detect height, scale model, align feet.
|
||
|
|
/// </summary>
|
||
|
|
public void autoDetectAndApply ()
|
||
|
|
{
|
||
|
|
captureBaseValues ();
|
||
|
|
autoDetectAndSetScale ();
|
||
|
|
applyHeightScale ();
|
||
|
|
|
||
|
|
if (showDebugLog) {
|
||
|
|
Debug.Log ("characterHeightScaler: FULL AUTO complete."
|
||
|
|
+ " Model scaled " + characterHeightScale.ToString ("F3") + "x"
|
||
|
|
+ " (from " + detectedModelHeight.ToString ("F3")
|
||
|
|
+ "m to ~" + gkcStandardHeight.ToString ("F3") + "m)");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
// ==================== CAPTURE / APPLY / RESET ====================
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Captures the model's current localScale and localPosition as the base (unscaled) values.
|
||
|
|
/// Call after building the character, before applying any scale.
|
||
|
|
/// </summary>
|
||
|
|
public void captureBaseValues ()
|
||
|
|
{
|
||
|
|
resolveReferences ();
|
||
|
|
|
||
|
|
resolveModelTransform ();
|
||
|
|
|
||
|
|
if (modelTransform != null) {
|
||
|
|
baseModelLocalScale = modelTransform.localScale;
|
||
|
|
baseModelLocalPosition = modelTransform.localPosition;
|
||
|
|
}
|
||
|
|
|
||
|
|
heightBaseValuesCaptured = true;
|
||
|
|
characterHeightScale = 1.0f;
|
||
|
|
modelScaleMultiplier = 1.0f;
|
||
|
|
|
||
|
|
if (showDebugLog) {
|
||
|
|
Debug.Log ("characterHeightScaler: CAPTURED"
|
||
|
|
+ " | model='" + (modelTransform != null ? modelTransform.name : "NULL") + "'"
|
||
|
|
+ " | baseScale=" + baseModelLocalScale
|
||
|
|
+ " | basePos=" + baseModelLocalPosition);
|
||
|
|
}
|
||
|
|
|
||
|
|
updateComponent ();
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Scales the model transform to match GKC standard height.
|
||
|
|
/// No COM, capsule, camera, IK, or weapon changes needed —
|
||
|
|
/// those all stay at GKC default values since we're fitting the model to the system,
|
||
|
|
/// not the system to the model.
|
||
|
|
/// </summary>
|
||
|
|
public void applyHeightScale ()
|
||
|
|
{
|
||
|
|
if (!heightBaseValuesCaptured) {
|
||
|
|
Debug.LogWarning ("characterHeightScaler: No base values captured. Click 'Capture Base Values' first.");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
resolveReferences ();
|
||
|
|
|
||
|
|
float scale = characterHeightScale;
|
||
|
|
|
||
|
|
if (showDebugLog) {
|
||
|
|
Debug.Log ("characterHeightScaler: ===== APPLYING MODEL SCALE = " + scale.ToString ("F3") + "x =====");
|
||
|
|
}
|
||
|
|
|
||
|
|
// 1. Scale the model
|
||
|
|
if (modelTransform != null) {
|
||
|
|
modelTransform.localScale = baseModelLocalScale * scale;
|
||
|
|
|
||
|
|
if (showDebugLog) {
|
||
|
|
Debug.Log (" [Model] '" + modelTransform.name + "' localScale = " + modelTransform.localScale);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 2. Align feet to ground (model origin may not be at feet)
|
||
|
|
alignFeetToGround ();
|
||
|
|
|
||
|
|
// 3. Recalculate IK foot initial values (bone positions changed due to model scale)
|
||
|
|
if (IKFootManager != null) {
|
||
|
|
IKFootManager.calculateInitialFootValues ();
|
||
|
|
|
||
|
|
if (showDebugLog) {
|
||
|
|
Debug.Log (" [IKFoot] Recalculated initial foot values");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
updateComponent ();
|
||
|
|
markComponentsDirty ();
|
||
|
|
|
||
|
|
if (showDebugLog) {
|
||
|
|
Debug.Log ("characterHeightScaler: ===== APPLY COMPLETE =====");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// After scaling the model, ensure feet are aligned with ground level.
|
||
|
|
/// Measures the lowest foot bone Y (or mesh bottom) and adjusts model localPosition.
|
||
|
|
/// </summary>
|
||
|
|
void alignFeetToGround ()
|
||
|
|
{
|
||
|
|
if (modelTransform == null) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
Animator animator = modelTransform.GetComponentInChildren<Animator> ();
|
||
|
|
|
||
|
|
float footWorldY = float.MaxValue;
|
||
|
|
bool footFound = false;
|
||
|
|
|
||
|
|
// Try humanoid foot bones (LeftToes / RightToes first, then LeftFoot / RightFoot)
|
||
|
|
if (animator != null && animator.isHuman) {
|
||
|
|
Transform leftToes = animator.GetBoneTransform (HumanBodyBones.LeftToes);
|
||
|
|
Transform rightToes = animator.GetBoneTransform (HumanBodyBones.RightToes);
|
||
|
|
Transform leftFoot = animator.GetBoneTransform (HumanBodyBones.LeftFoot);
|
||
|
|
Transform rightFoot = animator.GetBoneTransform (HumanBodyBones.RightFoot);
|
||
|
|
|
||
|
|
// Prefer toes (closest to ground), fall back to foot bone
|
||
|
|
if (leftToes != null || rightToes != null) {
|
||
|
|
if (leftToes != null) footWorldY = Mathf.Min (footWorldY, leftToes.position.y);
|
||
|
|
if (rightToes != null) footWorldY = Mathf.Min (footWorldY, rightToes.position.y);
|
||
|
|
footFound = true;
|
||
|
|
} else if (leftFoot != null || rightFoot != null) {
|
||
|
|
if (leftFoot != null) footWorldY = Mathf.Min (footWorldY, leftFoot.position.y);
|
||
|
|
if (rightFoot != null) footWorldY = Mathf.Min (footWorldY, rightFoot.position.y);
|
||
|
|
footFound = true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Fallback: SkinnedMeshRenderer bounds bottom
|
||
|
|
if (!footFound) {
|
||
|
|
SkinnedMeshRenderer[] renderers = modelTransform.GetComponentsInChildren<SkinnedMeshRenderer> ();
|
||
|
|
|
||
|
|
if (renderers.Length > 0) {
|
||
|
|
Bounds combinedBounds = renderers [0].bounds;
|
||
|
|
|
||
|
|
for (int i = 1; i < renderers.Length; i++) {
|
||
|
|
combinedBounds.Encapsulate (renderers [i].bounds);
|
||
|
|
}
|
||
|
|
|
||
|
|
footWorldY = combinedBounds.min.y;
|
||
|
|
footFound = true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!footFound) {
|
||
|
|
if (showDebugLog) {
|
||
|
|
Debug.LogWarning ("characterHeightScaler: Could not find foot bones or renderers for alignment.");
|
||
|
|
}
|
||
|
|
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Ground level = Player Controller transform.position.y (capsule bottom)
|
||
|
|
float groundWorldY = transform.position.y;
|
||
|
|
|
||
|
|
float offsetY = footWorldY - groundWorldY;
|
||
|
|
|
||
|
|
if (showDebugLog) {
|
||
|
|
Debug.Log (" [FootAlign] footWorldY=" + footWorldY.ToString ("F4")
|
||
|
|
+ " groundY=" + groundWorldY.ToString ("F4")
|
||
|
|
+ " offset=" + offsetY.ToString ("F4")
|
||
|
|
+ " userAdjust=" + footAlignmentOffset.ToString ("F4"));
|
||
|
|
}
|
||
|
|
|
||
|
|
// Adjust model localPosition to put feet on ground + user fine-tune offset
|
||
|
|
if (Mathf.Abs (offsetY) > 0.005f || Mathf.Abs (footAlignmentOffset) > 0.001f) {
|
||
|
|
Vector3 modelPos = modelTransform.localPosition;
|
||
|
|
modelPos.y = baseModelLocalPosition.y - offsetY + footAlignmentOffset;
|
||
|
|
modelTransform.localPosition = modelPos;
|
||
|
|
|
||
|
|
if (showDebugLog) {
|
||
|
|
Debug.Log (" [FootAlign] model localPos.y = " + modelPos.y.ToString ("F4"));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Resets model to original scale and position (1:1 with FBX import).
|
||
|
|
/// </summary>
|
||
|
|
public void resetHeightScale ()
|
||
|
|
{
|
||
|
|
if (modelTransform != null) {
|
||
|
|
modelTransform.localScale = baseModelLocalScale;
|
||
|
|
modelTransform.localPosition = baseModelLocalPosition;
|
||
|
|
}
|
||
|
|
|
||
|
|
characterHeightScale = 1.0f;
|
||
|
|
modelScaleMultiplier = 1.0f;
|
||
|
|
|
||
|
|
if (IKFootManager != null) {
|
||
|
|
IKFootManager.calculateInitialFootValues ();
|
||
|
|
}
|
||
|
|
|
||
|
|
updateComponent ();
|
||
|
|
markComponentsDirty ();
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
// ==================== INTERNAL ====================
|
||
|
|
|
||
|
|
void resolveReferences ()
|
||
|
|
{
|
||
|
|
if (componentsManager == null) {
|
||
|
|
GameObject playerObject = null;
|
||
|
|
|
||
|
|
buildPlayer builder = GetComponent<buildPlayer> ();
|
||
|
|
|
||
|
|
if (builder != null) {
|
||
|
|
playerObject = builder.player;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (playerObject == null) {
|
||
|
|
playerObject = transform.root.gameObject;
|
||
|
|
}
|
||
|
|
|
||
|
|
componentsManager = playerObject.GetComponent<playerComponentsManager> ();
|
||
|
|
}
|
||
|
|
|
||
|
|
if (componentsManager != null) {
|
||
|
|
if (playerControllerManager == null) {
|
||
|
|
playerControllerManager = componentsManager.getPlayerController ();
|
||
|
|
}
|
||
|
|
|
||
|
|
if (IKManager == null) {
|
||
|
|
IKManager = componentsManager.getIKSystem ();
|
||
|
|
}
|
||
|
|
|
||
|
|
if (IKFootManager == null) {
|
||
|
|
IKFootManager = GetComponentInChildren<IKFootSystem> ();
|
||
|
|
}
|
||
|
|
|
||
|
|
if (COMTransform == null && IKManager != null) {
|
||
|
|
COMTransform = IKManager.getIKBodyCOM ();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
resolveModelTransform ();
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
void resolveModelTransform ()
|
||
|
|
{
|
||
|
|
if (modelTransform == null) {
|
||
|
|
buildPlayer builder = GetComponent<buildPlayer> ();
|
||
|
|
|
||
|
|
if (builder != null && builder.currentCharacterModel != null) {
|
||
|
|
modelTransform = builder.currentCharacterModel.transform;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
void markComponentsDirty ()
|
||
|
|
{
|
||
|
|
#if UNITY_EDITOR
|
||
|
|
if (modelTransform != null) {
|
||
|
|
UnityEditor.EditorUtility.SetDirty (modelTransform);
|
||
|
|
UnityEditor.EditorUtility.SetDirty (modelTransform.gameObject);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (IKFootManager != null) {
|
||
|
|
UnityEditor.EditorUtility.SetDirty (IKFootManager);
|
||
|
|
}
|
||
|
|
|
||
|
|
UnityEditor.EditorUtility.SetDirty (this);
|
||
|
|
#endif
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
void updateComponent ()
|
||
|
|
{
|
||
|
|
#if UNITY_EDITOR
|
||
|
|
UnityEditor.EditorUtility.SetDirty (this);
|
||
|
|
#endif
|
||
|
|
}
|
||
|
|
}
|