Streamingle_URP/Assets/Scripts/KindRetargeting/LimbWeightController.cs
2025-12-01 03:36:18 +09:00

561 lines
22 KiB
C#

using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using RootMotion.FinalIK;
namespace KindRetargeting
{
[DefaultExecutionOrder(4)]
public class LimbWeightController : MonoBehaviour
{
[Header("거리 기반 가중치 설정")]
[SerializeField, Range(0.3f, 1f)] public float maxDistance = 0.5f; // 가중치가 0이 되는 최대 거리
[SerializeField, Range(0.05f, 0.3f)] public float minDistance = 0.1f; // 가중치가 1이 되는 최소 거리
[Header("마스터 가중치 변화 속도")]
[SerializeField, Range(0.1f, 20f)] public float weightSmoothSpeed = 10f; // 가중치 변화 속도
[Header("허리 가중치 설정")]
[SerializeField, Range(0.1f, 1f)] public float hipsMinDistance = 0.2f; // 최소 거리 (가중치 1)
[SerializeField, Range(0.1f, 1f)] public float hipsMaxDistance = 0.6f; // 최대 거리 (가중치 0)
[Header("바닥 기준 히프 보정")]
[SerializeField, Range(0.3f, 1f)] public float groundHipsMinHeight = 0f; // 최소 높이 (가중치 0)
[SerializeField, Range(0.5f, 1.5f)] public float groundHipsMaxHeight = 0.5f; // 최대 높이 (가중치 1)
[Header("발 높이 기반 가중치 설정")]
[SerializeField, Range(0.1f, 0.5f)] public float footHeightMinThreshold = 0.2f; // 최소 높이 (가중치 1)
[SerializeField, Range(0.5f, 1.0f)] public float footHeightMaxThreshold = 0.5f; // 최대 높이 (가중치 0)
[Header("IK 활성화 설정")]
[SerializeField] public bool enableLeftArmIK = true; // 왼팔 IK 활성화 여부
[SerializeField] public bool enableRightArmIK = true; // 오른팔 IK 활성화 여부
[Header("의자 앉기 높이 설정")]
[Tooltip("의자에 앉을 때 엉덩이 높이 조정 (월드 Y 기준, +: 위로, -: 아래로)")]
[SerializeField, Range(-1f, 1f)]
public float chairSeatHeightOffset = 0.05f;
private FullBodyInverseKinematics_RND fbik;
private CustomRetargetingScript crs;
private Dictionary<string, Dictionary<int, float>> weightLayers = new Dictionary<string, Dictionary<int, float>>();
List<float> leftArmEndWeights = new List<float>();
List<float> rightArmEndWeights = new List<float>();
List<float> leftLegEndWeights = new List<float>();
List<float> rightLegEndWeights = new List<float>();
List<float> leftLegBendWeights = new List<float>();
List<float> rightLegBendWeights = new List<float>();
private float MasterleftArmEndWeights = 0f;
private float MasterrightArmEndWeights = 0f;
private float MasterleftLegEndWeights = 0f;
private float MasterrightLegEndWeights = 0f;
private float MasterleftLegBendWeights = 0f;
private float MasterrightLegBendWeights = 0f;
public List<Transform> props = new List<Transform>();
public Transform characterRoot;
// 힙스 가중치 리스트 추가
List<float> hipsWeights = new List<float>();
private float MasterHipsWeight = 1f;
// 의자 좌석 높이 오프셋 (월드 Y 기준)
private float currentChairSeatOffset = 0f;
private float targetChairSeatOffset = 0f;
void Update()
{
//손의 거리를 기반으로한 가중치 적용
HandDistances();
//프랍과의 거리에 따른 가중치 적용
PropDistances();
// 마스터 변수에 가중치 적용
FinalizeWeights();
// 앉아있을 때 다리와의 거리에 따른 가중치 적용
SitLegDistances();
// 의자 타입 프랍과의 거리에 따른 가중치 적용
SitChairDistances();
// 바닥 기준 히프 보정 적용
UpdateGroundBasedHipsWeight();
// 발 높이에 따른 가중치 적용
UpdateFootHeightBasedWeight();
// FBIK에 최종 가중치 적용
ApplyWeightsToFBIK();
}
void Start()
{
fbik = GetComponent<FullBodyInverseKinematics_RND>();
crs = GetComponent<CustomRetargetingScript>();
InitWeightLayers();
//프랍 오브젝트 찾기
props = FindObjectsByType<PropTypeController>(FindObjectsSortMode.None).Select(controller => controller.transform).ToList();
// 프랍 오브젝트 찾기
GetHand();
//HandDistances();에서 사용을 위한 리스트 추가
//손 거리에 따른 웨이트 업데이트 인덱스 0번
leftArmEndWeights.Add(0);
rightArmEndWeights.Add(0);
// 프랍과의 거리에 따른 웨이트 업데이트 인덱스 1번
leftArmEndWeights.Add(0);
rightArmEndWeights.Add(0);
// 앉아있을 때 다리와의 거리에 따른 가중치 적용 인덱스 0번
leftLegEndWeights.Add(0);
rightLegEndWeights.Add(0);
// 다리 골 가중치 초기화
leftLegBendWeights.Add(1f); // 기본 가중치
rightLegBendWeights.Add(1f); // 기본 가중치
// CharacterController가 있는 루트 오브젝트 찾기
if (characterRoot == null)
{
characterRoot = transform;
}
// 힙스 가중치 초기화 인덱스 0번
hipsWeights.Add(1f); // 의자 거리 기반 가중치
// 지면 높이 기반 가중치 초기화 인덱스 1번
hipsWeights.Add(1f); // 지면 높이 기반 가중치
// 발 높이 기반 가중치 초기화 인덱스 1번
leftLegEndWeights.Add(1f);
rightLegEndWeights.Add(1f);
}
private void GetHand()
{
// 모든 LimbWeightController 찾기
LimbWeightController[] allControllers = FindObjectsOfType<LimbWeightController>();
foreach (LimbWeightController controller in allControllers)
{
// 자기 자신은 제외
if (controller == this) continue;
// CustomRetargetingScript 가져오기
CustomRetargetingScript otherCrs = controller.GetComponent<CustomRetargetingScript>();
if (otherCrs == null) continue;
// 왼손과 오른손 Transform 가져오기
Transform leftHand = otherCrs?.sourceAnimator?.GetBoneTransform(HumanBodyBones.LeftHand);
Transform rightHand = otherCrs?.sourceAnimator?.GetBoneTransform(HumanBodyBones.RightHand);
// 손이 존재하면 props 리스트에 추가
if (leftHand != null)
{
props.Add(leftHand);
}
if (rightHand != null)
{
props.Add(rightHand);
}
}
}
private void InitWeightLayers()
{
fbik.solver.leftArm.positionWeight = 0f;
fbik.solver.leftArm.rotationWeight = 0f;
fbik.solver.leftArm.bendGoalWeight = 0f;
fbik.solver.rightArm.positionWeight = 0f;
fbik.solver.rightArm.rotationWeight = 0f;
fbik.solver.rightArm.bendGoalWeight = 0f;
}
/// <summary>
/// 타겟과의 거리에 따라 가중치를 업데이트합니다.
/// </summary>
private void HandDistances()
{
if (fbik == null || crs == null) return;
// 왼쪽 팔 가중치 업데이트
if (fbik.solver.leftArm.target != null && fbik.solver.rightArm.target != null)
{
Transform leftHandTransform = crs?.sourceAnimator?.GetBoneTransform(HumanBodyBones.LeftHand);
Transform rightHandTransform = crs?.sourceAnimator?.GetBoneTransform(HumanBodyBones.RightHand);
if (leftHandTransform != null && rightHandTransform != null &&
props != null && props.Count > 0)
{
float Distance = Vector3.Distance(leftHandTransform.position, rightHandTransform.position);
float Weight = 1f - Mathf.Clamp01((Distance - minDistance) / (maxDistance - minDistance));
leftArmEndWeights[0] = Weight;
rightArmEndWeights[0] = Weight;
}
else
{
// props가 없을 때 기본 가중치 설정
leftArmEndWeights[0] = 0f;
rightArmEndWeights[0] = 0f;
}
}
}
void SitLegDistances()
{
if (fbik == null || crs == null || characterRoot == null) return;
Transform hips = crs?.sourceAnimator?.GetBoneTransform(HumanBodyBones.Hips);
if (hips == null) return;
// 캐릭터 루트 기준으로 히프의 로컬 높이 계산
float hipHeight = characterRoot.InverseTransformPoint(hips.position).y;
// 기본적으로 다리 가중치를 1로 설정
leftLegEndWeights[0] = 1f;
rightLegEndWeights[0] = 1f;
// 바닥 기준 히프 보정 변수들을 활용한 앉아있는 판단
// groundHipsMaxHeight 이하일 때 앉아있다고 판단
if (hipHeight <= groundHipsMaxHeight)
{
// 왼쪽 다리 처리
ProcessSitLegWeight(hips, fbik.solver.leftLeg.target, leftLegEndWeights, 0);
// 오른쪽 다리 처리
ProcessSitLegWeight(hips, fbik.solver.rightLeg.target, rightLegEndWeights, 0);
}
}
private void ProcessSitLegWeight(Transform hips, Transform footTarget, List<float> weightList, int weightIndex)
{
if (footTarget == null) return;
// Y축을 제외한 실제 수평 거리 계산
Vector3 hipPos = hips.position;
Vector3 footPos = footTarget.position;
Vector3 hipPosFlat = new Vector3(hipPos.x, 0, hipPos.z);
Vector3 footPosFlat = new Vector3(footPos.x, 0, footPos.z);
float horizontalDistance = Vector3.Distance(hipPosFlat, footPosFlat);
// 다리 길이를 기준으로 최대/최소 허용 거리 설정
const float MAX_LEG_DISTANCE_RATIO = 0.8f; // 다리 길이의 80%
const float MIN_LEG_DISTANCE_RATIO = 0.3f; // 다리 길이의 30%
// 거리가 멀수록 가중치 감소
float weight = 1f - Mathf.Clamp01((horizontalDistance - MIN_LEG_DISTANCE_RATIO) /
(MAX_LEG_DISTANCE_RATIO - MIN_LEG_DISTANCE_RATIO));
weightList[weightIndex] = weight;
}
void PropDistances()
{
if (fbik == null || crs == null) return;
// 프랍과의 거리에 따른 웨이트 업데이트
Transform leftHandTransform = crs?.sourceAnimator?.GetBoneTransform(HumanBodyBones.LeftHand);
Transform rightHandTransform = crs?.sourceAnimator?.GetBoneTransform(HumanBodyBones.RightHand);
if (leftHandTransform != null && rightHandTransform != null && props != null)
{
// 왼손과 프랍 사이의 최소 거리 계산
float minLeftDistance = float.MaxValue;
foreach (Transform prop in props)
{
float distance = Vector3.Distance(leftHandTransform.position, prop.position);
minLeftDistance = Mathf.Min(minLeftDistance, distance);
}
// 오른손과 프랍 사이의 최소 거리 계산
float minRightDistance = float.MaxValue;
foreach (Transform prop in props)
{
float distance = Vector3.Distance(rightHandTransform.position, prop.position);
minRightDistance = Mathf.Min(minRightDistance, distance);
}
// 거리에 따른 가중치 계산
float leftWeight = 1f - Mathf.Clamp01((minLeftDistance - minDistance) / (maxDistance - minDistance));
float rightWeight = 1f - Mathf.Clamp01((minRightDistance - minDistance) / (maxDistance - minDistance));
leftArmEndWeights[1] = leftWeight;
rightArmEndWeights[1] = rightWeight;
}
}
void SitChairDistances()
{
if (crs == null) return;
Transform hipsTransform = crs.sourceAnimator.GetBoneTransform(HumanBodyBones.Hips);
if (hipsTransform != null && props != null)
{
float minDistance = float.MaxValue;
bool foundChair = false;
foreach (Transform prop in props)
{
PropTypeController ptc = prop.GetComponent<PropTypeController>();
if (ptc != null && ptc.propType == EnumsList.PropType.Chair)
{
float distance = Vector3.Distance(hipsTransform.position, prop.childCount > 0 ? prop.GetChild(0).position : prop.position);
if (distance < minDistance)
{
minDistance = distance;
foundChair = true;
}
}
}
float t = Mathf.Clamp01((minDistance - hipsMinDistance) / (hipsMaxDistance - hipsMinDistance));
hipsWeights[0] = t; // 직접 HipsWeightOffset 수정 대신 배열에 저장
// 의자 좌석 높이 오프셋 계산 (가까울수록 더 적용) - 캐릭터별 설정 사용
if (foundChair)
{
// t가 0에 가까울수록 의자에 가까움 → 좌석 오프셋 더 적용
targetChairSeatOffset = chairSeatHeightOffset * (1f - t);
}
else
{
targetChairSeatOffset = 0f;
}
}
}
/// <summary>
/// 각 리스트의 최대 가중치 값을 마스터 변수에 부드럽게 설정합니다.
/// </summary>
public void FinalizeWeights()
{
float deltaTime = Time.deltaTime;
// 팔 가중치 계산 (최대값 사용)
MasterleftArmEndWeights = Mathf.Lerp(
MasterleftArmEndWeights,
GetMaxValue(leftArmEndWeights),
weightSmoothSpeed * deltaTime
);
MasterrightArmEndWeights = Mathf.Lerp(
MasterrightArmEndWeights,
GetMaxValue(rightArmEndWeights),
weightSmoothSpeed * deltaTime
);
// 다리 가중치 계산 (최소값 사용)
MasterleftLegEndWeights = Mathf.Lerp(
MasterleftLegEndWeights,
GetMinValue(leftLegEndWeights),
weightSmoothSpeed * deltaTime
);
MasterrightLegEndWeights = Mathf.Lerp(
MasterrightLegEndWeights,
GetMinValue(rightLegEndWeights),
weightSmoothSpeed * deltaTime
);
// 다리 골 가중치 계산
MasterleftLegBendWeights = Mathf.Lerp(
MasterleftLegBendWeights,
GetMinValue(leftLegBendWeights),
weightSmoothSpeed * deltaTime
);
MasterrightLegBendWeights = Mathf.Lerp(
MasterrightLegBendWeights,
GetMinValue(rightLegBendWeights),
weightSmoothSpeed * deltaTime
);
// 힙스 마스터 가중치 업데이트
MasterHipsWeight = Mathf.Lerp(
MasterHipsWeight,
GetMinValue(hipsWeights),
weightSmoothSpeed * deltaTime
);
// 의자 좌석 높이 오프셋 스무딩 (월드 Y 기준)
currentChairSeatOffset = Mathf.Lerp(
currentChairSeatOffset,
targetChairSeatOffset,
weightSmoothSpeed * deltaTime
);
// CustomRetargetingScript에 최종 가중치 전달
if (crs != null)
{
crs.HipsWeightOffset = MasterHipsWeight;
crs.ChairSeatHeightOffset = currentChairSeatOffset;
}
}
/// <summary>
/// 리스트에서 최대값을 찾습니다.
/// </summary>
private float GetMaxValue(List<float> list)
{
if (list.Count == 0) return 0f;
float max = list[0];
for (int i = 1; i < list.Count; i++)
{
if (list[i] > max)
{
max = list[i];
}
}
return max;
}
/// <summary>
/// 리스트에서 최소값을 찾습니다.
/// </summary>
private float GetMinValue(List<float> list)
{
if (list.Count == 0) return 0f;
float min = list[0];
for (int i = 1; i < list.Count; i++)
{
if (list[i] < min)
{
min = list[i];
}
}
return min;
}
/// <summary>
/// 마스터 가중치 값을 FBIK에 적용합니다.
/// </summary>
private void ApplyWeightsToFBIK()
{
if (fbik == null) return;
// 왼쪽 팔 가중치 적용
if (fbik.solver.leftArm != null)
{
if (fbik.solver.leftArm.target != null)
{
float finalLeftArmWeight = enableLeftArmIK ? MasterleftArmEndWeights : 0f;
fbik.solver.leftArm.positionWeight = finalLeftArmWeight;
fbik.solver.leftArm.rotationWeight = finalLeftArmWeight;
}
if (fbik.solver.leftArm.bendGoal != null)
fbik.solver.leftArm.bendGoalWeight = enableLeftArmIK ? MasterleftArmEndWeights : 0f;
}
// 오른쪽 팔 가중치 적용
if (fbik.solver.rightArm != null)
{
if (fbik.solver.rightArm.target != null)
{
float finalRightArmWeight = enableRightArmIK ? MasterrightArmEndWeights : 0f;
fbik.solver.rightArm.positionWeight = finalRightArmWeight;
fbik.solver.rightArm.rotationWeight = finalRightArmWeight;
}
if (fbik.solver.rightArm.bendGoal != null)
fbik.solver.rightArm.bendGoalWeight = enableRightArmIK ? MasterrightArmEndWeights : 0f;
}
// 왼쪽 다리 가중치 적용
if (fbik.solver.leftLeg != null)
{
if (fbik.solver.leftLeg.target != null)
{
fbik.solver.leftLeg.positionWeight = MasterleftLegEndWeights;
fbik.solver.leftLeg.rotationWeight = MasterleftLegEndWeights;
}
if (fbik.solver.leftLeg.bendGoal != null)
fbik.solver.leftLeg.bendGoalWeight = MasterleftLegBendWeights; // 골 가중치 적용
}
// 오른쪽 다리 가중치 적용
if (fbik.solver.rightLeg != null)
{
if (fbik.solver.rightLeg.target != null)
{
fbik.solver.rightLeg.positionWeight = MasterrightLegEndWeights;
fbik.solver.rightLeg.rotationWeight = MasterrightLegEndWeights;
}
if (fbik.solver.rightLeg.bendGoal != null)
fbik.solver.rightLeg.bendGoalWeight = MasterrightLegBendWeights; // 골 가중치 적용
}
}
/// <summary>
/// 바닥으로부터의 높이에 따라 히프 보정값을 조정합니다.
/// </summary>
private void UpdateGroundBasedHipsWeight()
{
if (crs == null || fbik == null || fbik == null) return;
Transform hipsTransform = crs.sourceAnimator.GetBoneTransform(HumanBodyBones.Hips);
if (hipsTransform != null)
{
float groundHeight = characterRoot.position.y;
float hipsHeight = hipsTransform.position.y - groundHeight;
float weight = Mathf.Clamp01((hipsHeight - groundHipsMinHeight) / (groundHipsMaxHeight - groundHipsMinHeight));
hipsWeights[1] = weight; // 직접 HipsWeightOffset 수정 대신 배열에 저장
}
}
/// <summary>
/// 발의 높이에 따라 IK 가중치를 조절합니다.
/// </summary>
private void UpdateFootHeightBasedWeight()
{
if (fbik == null || crs == null || characterRoot == null) return;
// 왼발 처리
ProcessFootHeightWeight(
fbik.solver.leftLeg.target,
leftLegEndWeights,
1 // 새로 추가된 레이어의 인덱스
);
// 오른발 처리
ProcessFootHeightWeight(
fbik.solver.rightLeg.target,
rightLegEndWeights,
1 // 새로 추가된 레이어의 인덱스
);
}
private void ProcessFootHeightWeight(Transform footTarget, List<float> weightList, int weightIndex)
{
if (footTarget == null) return;
// 캐릭터 루트 기준으로 발의 로컬 높이 계산
float footHeight = characterRoot.InverseTransformPoint(footTarget.position).y;
// 높이에 따른 가중치 계산 (높이가 높을수록 가중치 감소)
float weight = 1f - Mathf.Clamp01((footHeight - footHeightMinThreshold) /
(footHeightMaxThreshold - footHeightMinThreshold));
// 계산된 가중치 설정
weightList[weightIndex] = weight;
}
}
}