Fix : 리타겟팅 스크립트 복구
This commit is contained in:
parent
2efd14a936
commit
4d539b56c9
BIN
Assets/Resources/Settings/PropDatabase.asset
(Stored with Git LFS)
BIN
Assets/Resources/Settings/PropDatabase.asset
(Stored with Git LFS)
Binary file not shown.
8
Assets/ResourcesData/Prop/무선핸드마이크/Thumbnail.meta
Normal file
8
Assets/ResourcesData/Prop/무선핸드마이크/Thumbnail.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 32e5e53ddf8eec04997e01474d080f0b
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
BIN
Assets/ResourcesData/Prop/무선핸드마이크/Thumbnail/핸드마이크_thumbnail.png
(Stored with Git LFS)
Normal file
BIN
Assets/ResourcesData/Prop/무선핸드마이크/Thumbnail/핸드마이크_thumbnail.png
(Stored with Git LFS)
Normal file
Binary file not shown.
@ -0,0 +1,117 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2c3e74ea3c1c06d4f8e65793a53014cb
|
||||
TextureImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 13
|
||||
mipmaps:
|
||||
mipMapMode: 0
|
||||
enableMipMap: 1
|
||||
sRGBTexture: 1
|
||||
linearTexture: 0
|
||||
fadeOut: 0
|
||||
borderMipMap: 0
|
||||
mipMapsPreserveCoverage: 0
|
||||
alphaTestReferenceValue: 0.5
|
||||
mipMapFadeDistanceStart: 1
|
||||
mipMapFadeDistanceEnd: 3
|
||||
bumpmap:
|
||||
convertToNormalMap: 0
|
||||
externalNormalMap: 0
|
||||
heightScale: 0.25
|
||||
normalMapFilter: 0
|
||||
flipGreenChannel: 0
|
||||
isReadable: 0
|
||||
streamingMipmaps: 0
|
||||
streamingMipmapsPriority: 0
|
||||
vTOnly: 0
|
||||
ignoreMipmapLimit: 0
|
||||
grayScaleToAlpha: 0
|
||||
generateCubemap: 6
|
||||
cubemapConvolution: 0
|
||||
seamlessCubemap: 0
|
||||
textureFormat: 1
|
||||
maxTextureSize: 2048
|
||||
textureSettings:
|
||||
serializedVersion: 2
|
||||
filterMode: 1
|
||||
aniso: 1
|
||||
mipBias: 0
|
||||
wrapU: 0
|
||||
wrapV: 0
|
||||
wrapW: 0
|
||||
nPOTScale: 1
|
||||
lightmap: 0
|
||||
compressionQuality: 50
|
||||
spriteMode: 0
|
||||
spriteExtrude: 1
|
||||
spriteMeshType: 1
|
||||
alignment: 0
|
||||
spritePivot: {x: 0.5, y: 0.5}
|
||||
spritePixelsToUnits: 100
|
||||
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
||||
spriteGenerateFallbackPhysicsShape: 1
|
||||
alphaUsage: 1
|
||||
alphaIsTransparency: 0
|
||||
spriteTessellationDetail: -1
|
||||
textureType: 0
|
||||
textureShape: 1
|
||||
singleChannelComponent: 0
|
||||
flipbookRows: 1
|
||||
flipbookColumns: 1
|
||||
maxTextureSizeSet: 0
|
||||
compressionQualitySet: 0
|
||||
textureFormatSet: 0
|
||||
ignorePngGamma: 0
|
||||
applyGammaDecoding: 0
|
||||
swizzle: 50462976
|
||||
cookieLightType: 0
|
||||
platformSettings:
|
||||
- serializedVersion: 4
|
||||
buildTarget: DefaultTexturePlatform
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 4
|
||||
buildTarget: Standalone
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
spriteSheet:
|
||||
serializedVersion: 2
|
||||
sprites: []
|
||||
outline: []
|
||||
customData:
|
||||
physicsShape: []
|
||||
bones: []
|
||||
spriteID:
|
||||
internalID: 0
|
||||
vertices: []
|
||||
indices:
|
||||
edges: []
|
||||
weights: []
|
||||
secondaryTextures: []
|
||||
spriteCustomMetadata:
|
||||
entries: []
|
||||
nameFileIdTable: {}
|
||||
mipmapLimitGroupName:
|
||||
pSDRemoveMatte: 0
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/ResourcesData/Prop/바람개비/Thumbnail.meta
Normal file
8
Assets/ResourcesData/Prop/바람개비/Thumbnail.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ee74e2afa9c7ebc4aa8d7262101231e0
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
BIN
Assets/ResourcesData/Prop/바람개비/Thumbnail/바람개비_thumbnail.png
(Stored with Git LFS)
Normal file
BIN
Assets/ResourcesData/Prop/바람개비/Thumbnail/바람개비_thumbnail.png
(Stored with Git LFS)
Normal file
Binary file not shown.
117
Assets/ResourcesData/Prop/바람개비/Thumbnail/바람개비_thumbnail.png.meta
Normal file
117
Assets/ResourcesData/Prop/바람개비/Thumbnail/바람개비_thumbnail.png.meta
Normal file
@ -0,0 +1,117 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2a6a709c17aef7143aaec294254d25e1
|
||||
TextureImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 13
|
||||
mipmaps:
|
||||
mipMapMode: 0
|
||||
enableMipMap: 1
|
||||
sRGBTexture: 1
|
||||
linearTexture: 0
|
||||
fadeOut: 0
|
||||
borderMipMap: 0
|
||||
mipMapsPreserveCoverage: 0
|
||||
alphaTestReferenceValue: 0.5
|
||||
mipMapFadeDistanceStart: 1
|
||||
mipMapFadeDistanceEnd: 3
|
||||
bumpmap:
|
||||
convertToNormalMap: 0
|
||||
externalNormalMap: 0
|
||||
heightScale: 0.25
|
||||
normalMapFilter: 0
|
||||
flipGreenChannel: 0
|
||||
isReadable: 0
|
||||
streamingMipmaps: 0
|
||||
streamingMipmapsPriority: 0
|
||||
vTOnly: 0
|
||||
ignoreMipmapLimit: 0
|
||||
grayScaleToAlpha: 0
|
||||
generateCubemap: 6
|
||||
cubemapConvolution: 0
|
||||
seamlessCubemap: 0
|
||||
textureFormat: 1
|
||||
maxTextureSize: 2048
|
||||
textureSettings:
|
||||
serializedVersion: 2
|
||||
filterMode: 1
|
||||
aniso: 1
|
||||
mipBias: 0
|
||||
wrapU: 0
|
||||
wrapV: 0
|
||||
wrapW: 0
|
||||
nPOTScale: 1
|
||||
lightmap: 0
|
||||
compressionQuality: 50
|
||||
spriteMode: 0
|
||||
spriteExtrude: 1
|
||||
spriteMeshType: 1
|
||||
alignment: 0
|
||||
spritePivot: {x: 0.5, y: 0.5}
|
||||
spritePixelsToUnits: 100
|
||||
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
||||
spriteGenerateFallbackPhysicsShape: 1
|
||||
alphaUsage: 1
|
||||
alphaIsTransparency: 0
|
||||
spriteTessellationDetail: -1
|
||||
textureType: 0
|
||||
textureShape: 1
|
||||
singleChannelComponent: 0
|
||||
flipbookRows: 1
|
||||
flipbookColumns: 1
|
||||
maxTextureSizeSet: 0
|
||||
compressionQualitySet: 0
|
||||
textureFormatSet: 0
|
||||
ignorePngGamma: 0
|
||||
applyGammaDecoding: 0
|
||||
swizzle: 50462976
|
||||
cookieLightType: 0
|
||||
platformSettings:
|
||||
- serializedVersion: 4
|
||||
buildTarget: DefaultTexturePlatform
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 4
|
||||
buildTarget: Standalone
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
spriteSheet:
|
||||
serializedVersion: 2
|
||||
sprites: []
|
||||
outline: []
|
||||
customData:
|
||||
physicsShape: []
|
||||
bones: []
|
||||
spriteID:
|
||||
internalID: 0
|
||||
vertices: []
|
||||
indices:
|
||||
edges: []
|
||||
weights: []
|
||||
secondaryTextures: []
|
||||
spriteCustomMetadata:
|
||||
entries: []
|
||||
nameFileIdTable: {}
|
||||
mipmapLimitGroupName:
|
||||
pSDRemoveMatte: 0
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/ResourcesData/Prop/핸드마이크 스텐드/Thumbnail.meta
Normal file
8
Assets/ResourcesData/Prop/핸드마이크 스텐드/Thumbnail.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 04a7ecefb26843c4c8692dffcb280df7
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
BIN
Assets/ResourcesData/Prop/핸드마이크 스텐드/Thumbnail/핸드마이크 스텐드_thumbnail.png
(Stored with Git LFS)
Normal file
BIN
Assets/ResourcesData/Prop/핸드마이크 스텐드/Thumbnail/핸드마이크 스텐드_thumbnail.png
(Stored with Git LFS)
Normal file
Binary file not shown.
@ -0,0 +1,117 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 990174238f48eda40bbf1f02fe5d5773
|
||||
TextureImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 13
|
||||
mipmaps:
|
||||
mipMapMode: 0
|
||||
enableMipMap: 1
|
||||
sRGBTexture: 1
|
||||
linearTexture: 0
|
||||
fadeOut: 0
|
||||
borderMipMap: 0
|
||||
mipMapsPreserveCoverage: 0
|
||||
alphaTestReferenceValue: 0.5
|
||||
mipMapFadeDistanceStart: 1
|
||||
mipMapFadeDistanceEnd: 3
|
||||
bumpmap:
|
||||
convertToNormalMap: 0
|
||||
externalNormalMap: 0
|
||||
heightScale: 0.25
|
||||
normalMapFilter: 0
|
||||
flipGreenChannel: 0
|
||||
isReadable: 0
|
||||
streamingMipmaps: 0
|
||||
streamingMipmapsPriority: 0
|
||||
vTOnly: 0
|
||||
ignoreMipmapLimit: 0
|
||||
grayScaleToAlpha: 0
|
||||
generateCubemap: 6
|
||||
cubemapConvolution: 0
|
||||
seamlessCubemap: 0
|
||||
textureFormat: 1
|
||||
maxTextureSize: 2048
|
||||
textureSettings:
|
||||
serializedVersion: 2
|
||||
filterMode: 1
|
||||
aniso: 1
|
||||
mipBias: 0
|
||||
wrapU: 0
|
||||
wrapV: 0
|
||||
wrapW: 0
|
||||
nPOTScale: 1
|
||||
lightmap: 0
|
||||
compressionQuality: 50
|
||||
spriteMode: 0
|
||||
spriteExtrude: 1
|
||||
spriteMeshType: 1
|
||||
alignment: 0
|
||||
spritePivot: {x: 0.5, y: 0.5}
|
||||
spritePixelsToUnits: 100
|
||||
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
||||
spriteGenerateFallbackPhysicsShape: 1
|
||||
alphaUsage: 1
|
||||
alphaIsTransparency: 0
|
||||
spriteTessellationDetail: -1
|
||||
textureType: 0
|
||||
textureShape: 1
|
||||
singleChannelComponent: 0
|
||||
flipbookRows: 1
|
||||
flipbookColumns: 1
|
||||
maxTextureSizeSet: 0
|
||||
compressionQualitySet: 0
|
||||
textureFormatSet: 0
|
||||
ignorePngGamma: 0
|
||||
applyGammaDecoding: 0
|
||||
swizzle: 50462976
|
||||
cookieLightType: 0
|
||||
platformSettings:
|
||||
- serializedVersion: 4
|
||||
buildTarget: DefaultTexturePlatform
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 4
|
||||
buildTarget: Standalone
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
spriteSheet:
|
||||
serializedVersion: 2
|
||||
sprites: []
|
||||
outline: []
|
||||
customData:
|
||||
physicsShape: []
|
||||
bones: []
|
||||
spriteID:
|
||||
internalID: 0
|
||||
vertices: []
|
||||
indices:
|
||||
edges: []
|
||||
weights: []
|
||||
secondaryTextures: []
|
||||
spriteCustomMetadata:
|
||||
entries: []
|
||||
nameFileIdTable: {}
|
||||
mipmapLimitGroupName:
|
||||
pSDRemoveMatte: 0
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -22,12 +22,23 @@ namespace KindRetargeting
|
||||
// IK 컴포넌트 참조
|
||||
[SerializeField] public TwoBoneIKSolver ikSolver = new TwoBoneIKSolver();
|
||||
|
||||
[Header("힙 위치 보정 (로컬 좌표계 기반)")]
|
||||
[SerializeField, Range(-1, 1)]
|
||||
private float hipsOffsetX = 0f; // 캐릭터 기준 좌우 (항상 Right 방향)
|
||||
[SerializeField, Range(-1, 1)]
|
||||
private float hipsOffsetY = 0f; // 캐릭터 기준 상하 (항상 Up 방향)
|
||||
[SerializeField, Range(-1, 1)]
|
||||
private float hipsOffsetZ = 0f; // 캐릭터 기준 앞뒤 (항상 Forward 방향)
|
||||
|
||||
[HideInInspector] public float HipsWeightOffset = 1f;
|
||||
[HideInInspector] public float ChairSeatHeightOffset = 0f; // 의자 좌석 높이 오프셋 (월드 Y 기준)
|
||||
|
||||
// 캐릭터 기준 "위(Up)" 방향을 담당하는 로컬 축 (자동 힙 보정에서 사용)
|
||||
// 예: (0,1,0) 이면 로컬 Y축이 월드 Up. 부호는 방향(0,-1,0이면 로컬 -Y가 Up)
|
||||
private Vector3 localAxisForWorldUp = Vector3.up;
|
||||
// 축 매핑: 월드 방향(Right/Up/Forward)을 담당하는 로컬 축을 저장
|
||||
// 예: localAxisForWorldRight = (0, 0, 1) 이면 로컬 Z축이 월드 Right 방향을 담당
|
||||
// 부호는 방향을 나타냄: (0, 0, -1)이면 로컬 -Z가 월드 Right를 담당
|
||||
private Vector3 localAxisForWorldRight = Vector3.right; // 월드 좌우(Right)를 담당하는 로컬 축
|
||||
private Vector3 localAxisForWorldUp = Vector3.up; // 월드 상하(Up)를 담당하는 로컬 축
|
||||
private Vector3 localAxisForWorldForward = Vector3.forward; // 월드 앞뒤(Forward)를 담당하는 로컬 축
|
||||
|
||||
[Header("축 정규화 정보 (읽기 전용)")]
|
||||
[SerializeField]
|
||||
@ -62,8 +73,15 @@ namespace KindRetargeting
|
||||
// 본별 회전 오프셋을 저장하는 딕셔너리
|
||||
private Dictionary<HumanBodyBones, Quaternion> rotationOffsets = new Dictionary<HumanBodyBones, Quaternion>();
|
||||
|
||||
// HumanBodyBones 본 순회 범위 (0~54: 몸체 + 손가락 전부)
|
||||
private const int lastBoneIndex = 55;
|
||||
// HumanBodyBones.LastBone을 이용한 본 순회 범위
|
||||
private int lastBoneIndex = 55; // 0~54: 몸체 + 손가락 전부
|
||||
|
||||
[Header("무릎 안/밖 조정")]
|
||||
[SerializeField, Range(-1f, 1f)]
|
||||
private float kneeInOutWeight = 0f; // 무릎 안/밖 위치 조정 가중치
|
||||
[Header("무릎 앞/뒤 조정")]
|
||||
[SerializeField, Range(-1f, 1f)]
|
||||
private float kneeFrontBackWeight = 0.4f; // 무릎 앞/뒤 위치 조정 가중치
|
||||
|
||||
[Header("발 IK 위치 조정")]
|
||||
[SerializeField, Range(-1f, 1f)]
|
||||
@ -135,6 +153,11 @@ namespace KindRetargeting
|
||||
[System.Serializable]
|
||||
private class RetargetingSettings
|
||||
{
|
||||
public float hipsOffsetX; // 변경: hipsWeight → hipsOffsetX/Y/Z
|
||||
public float hipsOffsetY;
|
||||
public float hipsOffsetZ;
|
||||
public float kneeInOutWeight;
|
||||
public float kneeFrontBackWeight;
|
||||
public float footFrontBackOffset; // 발 앞뒤 오프셋
|
||||
public float footInOutOffset; // 발 안쪽/바깥쪽 오프셋
|
||||
public float floorHeight;
|
||||
@ -152,9 +175,11 @@ namespace KindRetargeting
|
||||
}
|
||||
|
||||
|
||||
// IK 조인트 캐시 구조체 (팔꿈치 bendGoal 위치 결정용)
|
||||
// IK 조인트 싱을 위한 구조체
|
||||
private struct IKJoints
|
||||
{
|
||||
public Transform leftLowerLeg;
|
||||
public Transform rightLowerLeg;
|
||||
public Transform leftLowerArm;
|
||||
public Transform rightLowerArm;
|
||||
}
|
||||
@ -259,7 +284,7 @@ namespace KindRetargeting
|
||||
/// - 아바타 B: 로컬 Z가 월드 Up을 가리킴 → localAxisForWorldUp = (0, 0, 1)
|
||||
/// - 아바타 C: 로컬 -Z가 월드 Up을 가리킴 → localAxisForWorldUp = (0, 0, -1)
|
||||
///
|
||||
/// 이를 통해 자동 힙 보정은 항상 캐릭터 기준 "위/아래" 방향으로 작동합니다.
|
||||
/// 이를 통해 hipsOffsetY는 항상 "위/아래" 방향으로 작동합니다.
|
||||
/// </summary>
|
||||
private void CalculateAxisNormalizer()
|
||||
{
|
||||
@ -273,12 +298,19 @@ namespace KindRetargeting
|
||||
Vector3 localYInWorld = hips.TransformDirection(Vector3.up).normalized;
|
||||
Vector3 localZInWorld = hips.TransformDirection(Vector3.forward).normalized;
|
||||
|
||||
// 월드 Right/Up/Forward 에 가장 가까운 로컬 축을 분석 (디버그 표시 + Up 매핑용)
|
||||
Vector3 localAxisForWorldRight = FindBestLocalAxisForWorld(localXInWorld, localYInWorld, localZInWorld, Vector3.right, out string rightAxisName);
|
||||
localAxisForWorldUp = FindBestLocalAxisForWorld(localXInWorld, localYInWorld, localZInWorld, Vector3.up, out string upAxisName);
|
||||
Vector3 localAxisForWorldForward = FindBestLocalAxisForWorld(localXInWorld, localYInWorld, localZInWorld, Vector3.forward, out string forwardAxisName);
|
||||
// 월드 Right(X)에 가장 가까운 로컬 축 찾기
|
||||
localAxisForWorldRight = FindBestLocalAxisForWorld(localXInWorld, localYInWorld, localZInWorld, Vector3.right, out string rightAxisName);
|
||||
|
||||
// 디버그용: X=좌우, Y=상하, Z=앞뒤 매핑 (1=X, 2=Y, 3=Z, 부호=방향)
|
||||
// 월드 Up(Y)에 가장 가까운 로컬 축 찾기
|
||||
localAxisForWorldUp = FindBestLocalAxisForWorld(localXInWorld, localYInWorld, localZInWorld, Vector3.up, out string upAxisName);
|
||||
|
||||
// 월드 Forward(Z)에 가장 가까운 로컬 축 찾기
|
||||
localAxisForWorldForward = FindBestLocalAxisForWorld(localXInWorld, localYInWorld, localZInWorld, Vector3.forward, out string forwardAxisName);
|
||||
|
||||
// 디버그용: 각 오프셋이 어느 로컬 축에 매핑되는지 표시
|
||||
// X: 좌우 오프셋이 사용하는 로컬 축 (1=X, 2=Y, 3=Z, 부호는 방향)
|
||||
// Y: 상하 오프셋이 사용하는 로컬 축
|
||||
// Z: 앞뒤 오프셋이 사용하는 로컬 축
|
||||
debugAxisNormalizer = new Vector3(
|
||||
GetAxisIndex(localAxisForWorldRight),
|
||||
GetAxisIndex(localAxisForWorldUp),
|
||||
@ -425,6 +457,11 @@ namespace KindRetargeting
|
||||
|
||||
var settings = new RetargetingSettings
|
||||
{
|
||||
hipsOffsetX = hipsOffsetX,
|
||||
hipsOffsetY = hipsOffsetY,
|
||||
hipsOffsetZ = hipsOffsetZ,
|
||||
kneeInOutWeight = kneeInOutWeight,
|
||||
kneeFrontBackWeight = kneeFrontBackWeight,
|
||||
footFrontBackOffset = footFrontBackOffset,
|
||||
footInOutOffset = footInOutOffset,
|
||||
floorHeight = floorHeight,
|
||||
@ -478,6 +515,11 @@ namespace KindRetargeting
|
||||
var settings = JsonUtility.FromJson<RetargetingSettings>(json);
|
||||
|
||||
// 설정 적용
|
||||
hipsOffsetX = settings.hipsOffsetX;
|
||||
hipsOffsetY = settings.hipsOffsetY;
|
||||
hipsOffsetZ = settings.hipsOffsetZ;
|
||||
kneeInOutWeight = settings.kneeInOutWeight;
|
||||
kneeFrontBackWeight = settings.kneeFrontBackWeight;
|
||||
footFrontBackOffset = settings.footFrontBackOffset;
|
||||
footInOutOffset = settings.footInOutOffset;
|
||||
floorHeight = settings.floorHeight;
|
||||
@ -663,9 +705,11 @@ namespace KindRetargeting
|
||||
return;
|
||||
}
|
||||
|
||||
// IK 조인트 캐싱 (팔꿈치 bendGoal 용)
|
||||
// IK 조인트들 캐싱
|
||||
sourceIKJoints = new IKJoints
|
||||
{
|
||||
leftLowerLeg = sourceIKRoot.Find("LeftLowerLeg"),
|
||||
rightLowerLeg = sourceIKRoot.Find("RightLowerLeg"),
|
||||
leftLowerArm = sourceIKRoot.Find("LeftLowerArm"),
|
||||
rightLowerArm = sourceIKRoot.Find("RightLowerArm")
|
||||
};
|
||||
@ -735,10 +779,14 @@ namespace KindRetargeting
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 머리 본에 회전 오프셋을 적용합니다. headBone 멤버 캐시 재사용 (ApplyHeadScale과 일관).
|
||||
/// 머리 본에 회전 오프셋을 적용합니다.
|
||||
/// </summary>
|
||||
private void ApplyHeadRotationOffset()
|
||||
{
|
||||
if (targetAnimator == null) return;
|
||||
|
||||
// 머리 본 가져오기
|
||||
Transform headBone = targetAnimator.GetBoneTransform(HumanBodyBones.Head);
|
||||
if (headBone == null) return;
|
||||
|
||||
// 오프셋이 모두 0이면 스킵
|
||||
@ -888,12 +936,30 @@ namespace KindRetargeting
|
||||
targetHips.rotation = finalHipsRotation;
|
||||
}
|
||||
|
||||
// 2. 다리 길이 차 + Hips↔UpperLeg 갭으로 힙 상하 자동 보정 (캐릭터 Up 방향)
|
||||
Vector3 characterUp = finalHipsRotation * localAxisForWorldUp;
|
||||
float autoHipsOffsetY = ComputeAutoHipsOffsetY();
|
||||
Vector3 characterOffset = characterUp * (autoHipsOffsetY * HipsWeightOffset);
|
||||
// 2. 캐릭터 기준 로컬 오프셋 계산 (축 정규화 적용)
|
||||
//
|
||||
// 문제: 아바타마다 힙의 로컬 축 방향이 다름
|
||||
// - 아바타 A: 로컬 Y가 "위", 로컬 Z가 "앞"
|
||||
// - 아바타 B: 로컬 Z가 "위", 로컬 X가 "앞"
|
||||
//
|
||||
// 해결: T-포즈에서 계산한 축 매핑을 사용
|
||||
// - localAxisForWorldRight: 실제로 "오른쪽"을 가리키는 로컬 축
|
||||
// - localAxisForWorldUp: 실제로 "위"를 가리키는 로컬 축
|
||||
// - localAxisForWorldForward: 실제로 "앞"을 가리키는 로컬 축
|
||||
//
|
||||
// 이렇게 하면 모든 아바타에서 동일하게 작동합니다.
|
||||
|
||||
// 3. 힙 위치 동기화 + 자동 보정 오프셋 적용
|
||||
// 힙의 현재 회전을 기준으로, 정규화된 방향 벡터 계산
|
||||
Vector3 characterRight = finalHipsRotation * localAxisForWorldRight;
|
||||
Vector3 characterUp = finalHipsRotation * localAxisForWorldUp;
|
||||
Vector3 characterForward = finalHipsRotation * localAxisForWorldForward;
|
||||
|
||||
Vector3 characterOffset =
|
||||
characterRight * (hipsOffsetX * HipsWeightOffset) + // 캐릭터 기준 좌우
|
||||
characterUp * (hipsOffsetY * HipsWeightOffset) + // 캐릭터 기준 상하
|
||||
characterForward * (hipsOffsetZ * HipsWeightOffset); // 캐릭터 기준 앞뒤
|
||||
|
||||
// 3. 힙 위치 동기화 + 캐릭터 기준 오프셋 적용
|
||||
Vector3 adjustedPosition = sourceHips.position + characterOffset;
|
||||
|
||||
// 4. 바닥 높이 추가 (월드 Y축 - 바닥은 항상 월드 기준)
|
||||
@ -912,51 +978,6 @@ namespace KindRetargeting
|
||||
SyncBoneRotations(skipBone: HumanBodyBones.Hips);
|
||||
}
|
||||
|
||||
// 자동 힙 보정 캐시 (다리 본 길이는 스케일 변경 시에만 갱신됨)
|
||||
private float cachedAutoHipsOffsetY = 0f;
|
||||
private bool autoHipsOffsetCacheValid = false;
|
||||
|
||||
/// <summary>
|
||||
/// 매 프레임 호출되는 자동 힙 보정값. 본 길이는 변하지 않으므로 캐시된 값을 반환.
|
||||
/// avatarScale 변경 시 ApplyScale → RefreshAutoHipsOffsetCache 로 자동 갱신.
|
||||
/// </summary>
|
||||
private float ComputeAutoHipsOffsetY()
|
||||
{
|
||||
if (!autoHipsOffsetCacheValid) RefreshAutoHipsOffsetCache();
|
||||
return cachedAutoHipsOffsetY;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 자동 힙 보정 캐시를 재계산합니다 (Initialize / avatarScale 변경 시 호출).
|
||||
/// = (타겟 다리길이 - 소스 다리길이) + (타겟 Hips↔UpperLeg 갭) × avatarScale
|
||||
/// </summary>
|
||||
private void RefreshAutoHipsOffsetCache()
|
||||
{
|
||||
cachedAutoHipsOffsetY = 0f;
|
||||
autoHipsOffsetCacheValid = true;
|
||||
|
||||
if (optitrackSource == null || targetAnimator == null) return;
|
||||
|
||||
Transform sUp = optitrackSource.GetBoneTransform(HumanBodyBones.LeftUpperLeg);
|
||||
Transform sLo = optitrackSource.GetBoneTransform(HumanBodyBones.LeftLowerLeg);
|
||||
Transform sFt = optitrackSource.GetBoneTransform(HumanBodyBones.LeftFoot);
|
||||
Transform tUp = targetAnimator.GetBoneTransform(HumanBodyBones.LeftUpperLeg);
|
||||
Transform tLo = targetAnimator.GetBoneTransform(HumanBodyBones.LeftLowerLeg);
|
||||
Transform tFt = targetAnimator.GetBoneTransform(HumanBodyBones.LeftFoot);
|
||||
Transform tHi = targetAnimator.GetBoneTransform(HumanBodyBones.Hips);
|
||||
|
||||
if (sUp == null || sLo == null || sFt == null
|
||||
|| tUp == null || tLo == null || tFt == null || tHi == null)
|
||||
return;
|
||||
|
||||
float sourceLeg = Vector3.Distance(sUp.position, sLo.position) + Vector3.Distance(sLo.position, sFt.position);
|
||||
float targetLeg = Vector3.Distance(tUp.position, tLo.position) + Vector3.Distance(tLo.position, tFt.position);
|
||||
if (sourceLeg < 0.01f || targetLeg < 0.01f) return;
|
||||
|
||||
float hipsToLegGap = tHi.position.y - tUp.position.y;
|
||||
cachedAutoHipsOffsetY = (targetLeg - sourceLeg) + hipsToLegGap * avatarScale;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 힙을 제외한 모든 본의 회전을 오프셋을 적용하여 동기화합니다.
|
||||
/// </summary>
|
||||
@ -1218,9 +1239,11 @@ namespace KindRetargeting
|
||||
UpdateEndTarget(ikSolver.leftLeg.target, HumanBodyBones.LeftFoot);
|
||||
UpdateEndTarget(ikSolver.rightLeg.target, HumanBodyBones.RightFoot);
|
||||
|
||||
// 팔 bendGoal만 갱신 (다리는 ComputeKneePosFromSource가 소스 무릎으로 풀이하므로 bendGoal 사용 안 함)
|
||||
updatejointTarget(ikSolver.leftArm.bendGoal, HumanBodyBones.LeftLowerArm);
|
||||
updatejointTarget(ikSolver.rightArm.bendGoal, HumanBodyBones.RightLowerArm);
|
||||
updatejointTarget(ikSolver.leftLeg.bendGoal, HumanBodyBones.LeftLowerLeg);
|
||||
updatejointTarget(ikSolver.rightLeg.bendGoal, HumanBodyBones.RightLowerLeg);
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -1285,10 +1308,21 @@ namespace KindRetargeting
|
||||
Vector3 targetBone = targetAnimator.GetBoneTransform(bone).position;
|
||||
Vector3 sourceIKpoint;
|
||||
float zOffset = 0f;
|
||||
float xOffset = 0f;
|
||||
float yOffset = 0f;
|
||||
|
||||
// bone의 이름에 따라 적절한 IK 조인트 오프셋 선택
|
||||
switch (bone)
|
||||
{
|
||||
case HumanBodyBones.LeftLowerLeg:
|
||||
case HumanBodyBones.RightLowerLeg:
|
||||
sourceIKpoint = bone == HumanBodyBones.LeftLowerLeg ?
|
||||
sourceIKJoints.leftLowerLeg.position :
|
||||
sourceIKJoints.rightLowerLeg.position;
|
||||
zOffset = kneeFrontBackWeight; // 무릎 앞/뒤 조정
|
||||
yOffset = floorHeight;
|
||||
xOffset = kneeInOutWeight * (bone == HumanBodyBones.LeftLowerLeg ? -1f : 1f); // 무릎 안/밖 조정
|
||||
break;
|
||||
case HumanBodyBones.LeftLowerArm:
|
||||
case HumanBodyBones.RightLowerArm:
|
||||
sourceIKpoint = bone == HumanBodyBones.LeftLowerArm ?
|
||||
@ -1306,8 +1340,11 @@ namespace KindRetargeting
|
||||
target.position = targetBone;
|
||||
target.rotation = LookatIK;
|
||||
|
||||
// LookatIK 기준으로 z축 오프셋 적용 (팔꿈치 뒤쪽)
|
||||
target.position += LookatIK * new Vector3(0f, 0f, zOffset);
|
||||
// LookatIK 기준으로 프셋 적용
|
||||
Vector3 offset = LookatIK * new Vector3(xOffset, 0, zOffset);
|
||||
Vector3 offset2 = new Vector3(0, yOffset, 0);
|
||||
target.position += offset;
|
||||
target.position += offset2;
|
||||
}
|
||||
|
||||
#endregion
|
||||
@ -1393,6 +1430,18 @@ namespace KindRetargeting
|
||||
return File.Exists(filePath);
|
||||
}
|
||||
|
||||
// 무릎 앞/뒤뒤 위치 조정을 위한 public 메서드 추가
|
||||
public void SetKneeFrontBackOffset(float offset)
|
||||
{
|
||||
kneeFrontBackWeight = offset;
|
||||
}
|
||||
|
||||
// 무릎 조정을 위한 public 메서드들
|
||||
public void SetKneeInOutOffset(float offset)
|
||||
{
|
||||
kneeInOutWeight = offset;
|
||||
}
|
||||
|
||||
public void ResetPoseAndCache()
|
||||
{
|
||||
// 캐시 파일 삭제
|
||||
@ -1496,10 +1545,6 @@ namespace KindRetargeting
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 스케일 변경으로 본 길이가 바뀌었으니 IK 캐시 + 자동 힙 보정 캐시 갱신
|
||||
ikSolver?.RefreshLimbLengths();
|
||||
RefreshAutoHipsOffsetCache();
|
||||
}
|
||||
|
||||
// 리셋 기능 추가
|
||||
@ -1528,6 +1573,12 @@ namespace KindRetargeting
|
||||
avatarScale = Mathf.Clamp(scale, 0.1f, 3f);
|
||||
}
|
||||
|
||||
// 현재 스케일을 가져오는 메서드
|
||||
public float GetAvatarScale()
|
||||
{
|
||||
return avatarScale;
|
||||
}
|
||||
|
||||
// 외부에서 머리 크기를 설정할 수 있는 public 메서드
|
||||
public void SetHeadScale(float scale)
|
||||
{
|
||||
|
||||
@ -34,6 +34,16 @@ namespace KindRetargeting
|
||||
// ── 힙 위치 보정 ──
|
||||
root.Add(BuildHipsSection());
|
||||
|
||||
// ── 무릎 위치 조정 ──
|
||||
var kneeFoldout = new Foldout { text = "무릎 위치 조정", value = false };
|
||||
var kneeFB = new Slider("무릎 앞/뒤", -1f, 1f) { showInputField = true };
|
||||
kneeFB.BindProperty(serializedObject.FindProperty("kneeFrontBackWeight"));
|
||||
kneeFoldout.Add(kneeFB);
|
||||
var kneeIO = new Slider("무릎 안/밖", -1f, 1f) { showInputField = true };
|
||||
kneeIO.BindProperty(serializedObject.FindProperty("kneeInOutWeight"));
|
||||
kneeFoldout.Add(kneeIO);
|
||||
root.Add(kneeFoldout);
|
||||
|
||||
// ── 발 IK 위치 조정 ──
|
||||
var footFoldout = new Foldout { text = "발 IK 위치 조정", value = false };
|
||||
var footFB = new Slider("발 앞/뒤", -1f, 1f) { showInputField = true };
|
||||
@ -74,17 +84,54 @@ namespace KindRetargeting
|
||||
}
|
||||
|
||||
// ========== 힙 위치 보정 ==========
|
||||
// 힙 상하 보정은 매 프레임 자동 계산됨 (CustomRetargetingScript.ComputeAutoHipsOffsetY).
|
||||
// 의자 앉기 높이만 수동 조정 가능.
|
||||
|
||||
private VisualElement BuildHipsSection()
|
||||
{
|
||||
var foldout = new Foldout { text = "힙 위치 보정", value = true };
|
||||
var foldout = new Foldout { text = "힙 위치 보정 (로컬 좌표계)", value = true };
|
||||
|
||||
var axisInfo = new HelpBox("플레이 모드에서 축 매핑 정보가 표시됩니다.", HelpBoxMessageType.Info);
|
||||
foldout.Add(axisInfo);
|
||||
|
||||
foldout.schedule.Execute(() =>
|
||||
{
|
||||
if (target == null) return;
|
||||
serializedObject.Update();
|
||||
var axisProp = serializedObject.FindProperty("debugAxisNormalizer");
|
||||
if (axisProp == null || !Application.isPlaying) { axisInfo.text = "플레이 모드에서 축 매핑 정보가 표시됩니다."; return; }
|
||||
Vector3 m = axisProp.vector3Value;
|
||||
if (m == Vector3.one) { axisInfo.text = "플레이 모드에서 축 매핑 정보가 표시됩니다."; return; }
|
||||
string A(float v) => Mathf.RoundToInt(Mathf.Abs(v)) switch { 1 => (v > 0 ? "+X" : "-X"), 2 => (v > 0 ? "+Y" : "-Y"), 3 => (v > 0 ? "+Z" : "-Z"), _ => "?" };
|
||||
axisInfo.text = $"축 매핑: 좌우→{A(m.x)} 상하→{A(m.y)} 앞뒤→{A(m.z)}";
|
||||
}).Every(500);
|
||||
|
||||
var hx = new Slider("← 좌우 →", -1f, 1f) { showInputField = true };
|
||||
hx.BindProperty(serializedObject.FindProperty("hipsOffsetX"));
|
||||
foldout.Add(hx);
|
||||
var hy = new Slider("↓ 상하 ↑", -1f, 1f) { showInputField = true };
|
||||
hy.BindProperty(serializedObject.FindProperty("hipsOffsetY"));
|
||||
foldout.Add(hy);
|
||||
var hz = new Slider("← 앞뒤 →", -1f, 1f) { showInputField = true };
|
||||
hz.BindProperty(serializedObject.FindProperty("hipsOffsetZ"));
|
||||
foldout.Add(hz);
|
||||
|
||||
// 의자 앉기 높이
|
||||
var chairSlider = new Slider("의자 앉기 높이", -1f, 1f) { showInputField = true, tooltip = "의자에 앉을 때 엉덩이 높이 조정" };
|
||||
chairSlider.BindProperty(serializedObject.FindProperty("limbWeight.chairSeatHeightOffset"));
|
||||
foldout.Add(chairSlider);
|
||||
|
||||
// 다리 길이 자동 보정 버튼
|
||||
var autoHipsBtn = new Button(() =>
|
||||
{
|
||||
if (!Application.isPlaying) { Debug.LogWarning("플레이 모드에서만 사용 가능합니다."); return; }
|
||||
var script = (CustomRetargetingScript)target;
|
||||
float offset = CalculateHipsOffsetFromLegDifference(script);
|
||||
serializedObject.FindProperty("hipsOffsetY").floatValue = offset;
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
script.SaveSettings();
|
||||
}) { text = "다리 길이 자동 보정", tooltip = "소스/타겟 다리 길이 차이로 힙 상하 오프셋을 자동 계산합니다." };
|
||||
autoHipsBtn.style.marginTop = 4; autoHipsBtn.style.height = 25;
|
||||
foldout.Add(autoHipsBtn);
|
||||
|
||||
return foldout;
|
||||
}
|
||||
|
||||
@ -372,6 +419,7 @@ namespace KindRetargeting
|
||||
|
||||
script.ResetScale();
|
||||
so.FindProperty("avatarScale").floatValue = 1f;
|
||||
so.FindProperty("hipsOffsetY").floatValue = CalculateHipsOffsetFromLegDifference(script);
|
||||
so.ApplyModifiedProperties();
|
||||
|
||||
EditorApplication.delayCall += () =>
|
||||
@ -392,6 +440,7 @@ namespace KindRetargeting
|
||||
{
|
||||
if (script == null) return;
|
||||
var so3 = new SerializedObject(script);
|
||||
so3.FindProperty("hipsOffsetY").floatValue = CalculateHipsOffsetFromLegDifference(script);
|
||||
|
||||
var xP = so3.FindProperty("headRotationOffsetX");
|
||||
var yP = so3.FindProperty("headRotationOffsetY");
|
||||
@ -408,6 +457,37 @@ namespace KindRetargeting
|
||||
|
||||
// ========== 유틸리티 ==========
|
||||
|
||||
private float CalculateHipsOffsetFromLegDifference(CustomRetargetingScript script)
|
||||
{
|
||||
var source = script.optitrackSource;
|
||||
Animator targetAnim = script.targetAnimator;
|
||||
if (source == null || targetAnim == null) return 0f;
|
||||
|
||||
float sourceLeg = GetSourceLegLength(source);
|
||||
float targetLeg = GetLegLength(targetAnim);
|
||||
if (sourceLeg < 0.01f || targetLeg < 0.01f) return 0f;
|
||||
|
||||
return targetLeg - sourceLeg;
|
||||
}
|
||||
|
||||
private float GetLegLength(Animator animator)
|
||||
{
|
||||
Transform upper = animator.GetBoneTransform(HumanBodyBones.LeftUpperLeg);
|
||||
Transform lower = animator.GetBoneTransform(HumanBodyBones.LeftLowerLeg);
|
||||
Transform foot = animator.GetBoneTransform(HumanBodyBones.LeftFoot);
|
||||
if (upper == null || lower == null || foot == null) return 0f;
|
||||
return Vector3.Distance(upper.position, lower.position) + Vector3.Distance(lower.position, foot.position);
|
||||
}
|
||||
|
||||
private float GetSourceLegLength(OptitrackSkeletonAnimator_Mingle source)
|
||||
{
|
||||
Transform upper = source.GetBoneTransform(HumanBodyBones.LeftUpperLeg);
|
||||
Transform lower = source.GetBoneTransform(HumanBodyBones.LeftLowerLeg);
|
||||
Transform foot = source.GetBoneTransform(HumanBodyBones.LeftFoot);
|
||||
if (upper == null || lower == null || foot == null) return 0f;
|
||||
return Vector3.Distance(upper.position, lower.position) + Vector3.Distance(lower.position, foot.position);
|
||||
}
|
||||
|
||||
private void CalibrateHeadToForward(SerializedObject so, SerializedProperty xProp, SerializedProperty yProp, SerializedProperty zProp)
|
||||
{
|
||||
CustomRetargetingScript script = so.targetObject as CustomRetargetingScript;
|
||||
|
||||
@ -198,6 +198,19 @@ public class RetargetingControlWindow : EditorWindow
|
||||
// 힙 위치 보정
|
||||
panel.Add(BuildHipsSection(script, so));
|
||||
|
||||
// 무릎 위치 조정
|
||||
var kneeFoldout = new Foldout { text = "무릎 위치 조정", value = false };
|
||||
var kneeContainer = new VisualElement();
|
||||
var kneeFB = new Slider("무릎 앞/뒤 가중치", -1f, 1f) { showInputField = true };
|
||||
kneeFB.BindProperty(so.FindProperty("kneeFrontBackWeight"));
|
||||
kneeContainer.Add(kneeFB);
|
||||
var kneeIO = new Slider("무릎 안/밖 가중치", -1f, 1f) { showInputField = true };
|
||||
kneeIO.BindProperty(so.FindProperty("kneeInOutWeight"));
|
||||
kneeContainer.Add(kneeIO);
|
||||
kneeFoldout.Add(kneeContainer);
|
||||
kneeContainer.Bind(so);
|
||||
panel.Add(kneeFoldout);
|
||||
|
||||
// 발 IK 위치 조정
|
||||
var footFoldout = new Foldout { text = "발 IK 위치 조정", value = false };
|
||||
var footContainer = new VisualElement();
|
||||
@ -232,6 +245,16 @@ public class RetargetingControlWindow : EditorWindow
|
||||
if (headScaleProp != null)
|
||||
scaleContainer.Add(new PropertyField(headScaleProp, "머리 크기"));
|
||||
|
||||
// 아바타 크기 변경 시 다리 길이 자동 보정 (실시간)
|
||||
scaleContainer.TrackPropertyValue(so.FindProperty("avatarScale"), _ =>
|
||||
{
|
||||
if (!Application.isPlaying || script == null) return;
|
||||
var sox = new SerializedObject(script);
|
||||
sox.FindProperty("hipsOffsetY").floatValue = CalculateHipsOffsetFromLegDifference(script);
|
||||
sox.ApplyModifiedProperties();
|
||||
sox.Dispose();
|
||||
});
|
||||
|
||||
scaleContainer.Bind(so);
|
||||
scaleFoldout.Add(scaleContainer);
|
||||
panel.Add(scaleFoldout);
|
||||
@ -313,18 +336,65 @@ public class RetargetingControlWindow : EditorWindow
|
||||
}
|
||||
|
||||
// ========== Hips Settings ==========
|
||||
// 힙 상하 보정은 매 프레임 자동 (CustomRetargetingScript.ComputeAutoHipsOffsetY).
|
||||
// 의자 앉기 높이만 수동 조정.
|
||||
|
||||
private VisualElement BuildHipsSection(CustomRetargetingScript script, SerializedObject so)
|
||||
{
|
||||
var foldout = new Foldout { text = "힙 위치 보정", value = false };
|
||||
var foldout = new Foldout { text = "힙 위치 보정 (로컬)", value = false };
|
||||
var container = new VisualElement();
|
||||
|
||||
// 축 매핑 정보
|
||||
var axisLabel = new Label { style = { fontSize = 10, color = new Color(0.6f, 0.6f, 0.6f) } };
|
||||
container.Add(axisLabel);
|
||||
|
||||
container.schedule.Execute(() =>
|
||||
{
|
||||
try { if (so == null || so.targetObject == null) return; }
|
||||
catch (System.Exception) { return; }
|
||||
so.Update();
|
||||
var normProp = so.FindProperty("debugAxisNormalizer");
|
||||
if (normProp == null || !Application.isPlaying) { axisLabel.text = ""; return; }
|
||||
Vector3 m = normProp.vector3Value;
|
||||
if (m == Vector3.one) { axisLabel.text = ""; return; }
|
||||
string A(float v) => Mathf.RoundToInt(Mathf.Abs(v)) switch { 1 => (v > 0 ? "+X" : "-X"), 2 => (v > 0 ? "+Y" : "-Y"), 3 => (v > 0 ? "+Z" : "-Z"), _ => "?" };
|
||||
axisLabel.text = $"축 매핑: 좌우→{A(m.x)} 상하→{A(m.y)} 앞뒤→{A(m.z)}";
|
||||
}).Every(500);
|
||||
|
||||
var hx = new Slider("← 좌우 →", -1f, 1f) { showInputField = true };
|
||||
hx.BindProperty(so.FindProperty("hipsOffsetX"));
|
||||
container.Add(hx);
|
||||
|
||||
var hy = new Slider("↓ 상하 ↑", -1f, 1f) { showInputField = true };
|
||||
hy.BindProperty(so.FindProperty("hipsOffsetY"));
|
||||
container.Add(hy);
|
||||
|
||||
var hz = new Slider("← 앞뒤 →", -1f, 1f) { showInputField = true };
|
||||
hz.BindProperty(so.FindProperty("hipsOffsetZ"));
|
||||
container.Add(hz);
|
||||
|
||||
// 의자 앉기 높이
|
||||
var chairSlider = new Slider("의자 앉기 높이", -1f, 1f) { showInputField = true, tooltip = "의자에 앉을 때 엉덩이 높이 조정 (월드 Y 기준)" };
|
||||
chairSlider.BindProperty(so.FindProperty("limbWeight.chairSeatHeightOffset"));
|
||||
container.Add(chairSlider);
|
||||
|
||||
// 다리 길이 자동 보정 버튼
|
||||
var autoHipsBtn = new Button(() =>
|
||||
{
|
||||
if (!Application.isPlaying)
|
||||
{
|
||||
Debug.LogWarning("다리 길이 자동 보정은 플레이 모드에서만 사용 가능합니다.");
|
||||
return;
|
||||
}
|
||||
float offset = CalculateHipsOffsetFromLegDifference(script);
|
||||
var hipsProp = so.FindProperty("hipsOffsetY");
|
||||
hipsProp.floatValue = offset;
|
||||
so.ApplyModifiedProperties();
|
||||
script.SaveSettings();
|
||||
Debug.Log($"자동 보정 완료: hipsOffsetY = {offset:F4}");
|
||||
}) { text = "다리 길이 자동 보정", tooltip = "소스/타겟 다리 길이 차이로 힙 상하 오프셋을 자동 계산합니다. (플레이 모드 전용)" };
|
||||
autoHipsBtn.style.marginTop = 4;
|
||||
autoHipsBtn.style.height = 25;
|
||||
container.Add(autoHipsBtn);
|
||||
|
||||
container.Bind(so);
|
||||
foldout.Add(container);
|
||||
return foldout;
|
||||
@ -766,8 +836,7 @@ public class RetargetingControlWindow : EditorWindow
|
||||
// ========== Auto Full Calibration ==========
|
||||
|
||||
/// <summary>
|
||||
/// 소스/타겟 목 높이 비율로 avatarScale을 맞추고, 머리 정면을 캘리브레이션합니다.
|
||||
/// (힙 상하 보정은 매 프레임 자동 처리됨)
|
||||
/// 소스/타겟 목 높이 비율로 avatarScale을 맞추고, 다리 길이 차이로 hipsOffsetY를 보정합니다.
|
||||
/// </summary>
|
||||
private void AutoCalibrateAll(CustomRetargetingScript script, SerializedObject so)
|
||||
{
|
||||
@ -780,10 +849,11 @@ public class RetargetingControlWindow : EditorWindow
|
||||
return;
|
||||
}
|
||||
|
||||
// ── 프레임 1: 스케일 리셋 ──
|
||||
// ── 프레임 1: 스케일 리셋 + 다리 보정 ──
|
||||
script.ResetScale();
|
||||
var scaleProp = so.FindProperty("avatarScale");
|
||||
scaleProp.floatValue = 1f;
|
||||
so.FindProperty("hipsOffsetY").floatValue = CalculateHipsOffsetFromLegDifference(script);
|
||||
so.ApplyModifiedProperties();
|
||||
|
||||
// ── 프레임 2: 리타게팅 반영 후 목 높이 측정 → avatarScale 설정 ──
|
||||
@ -809,12 +879,14 @@ public class RetargetingControlWindow : EditorWindow
|
||||
|
||||
Debug.Log($"크기 보정: 소스 목 Y={sourceNeckY:F4}, 타겟 목 Y={targetNeckY:F4} → avatarScale = {scaleRatio:F3}");
|
||||
|
||||
// ── 프레임 3: 머리 정면 캘리 + 저장 ──
|
||||
// ── 프레임 3: 스케일 반영 후 다리 재보정 + 머리 정면 캘리 + 저장 ──
|
||||
EditorApplication.delayCall += () =>
|
||||
{
|
||||
if (script == null) return;
|
||||
|
||||
var so3 = new SerializedObject(script);
|
||||
float finalHipsOffset = CalculateHipsOffsetFromLegDifference(script);
|
||||
so3.FindProperty("hipsOffsetY").floatValue = finalHipsOffset;
|
||||
|
||||
var xProp = so3.FindProperty("headRotationOffsetX");
|
||||
var yProp = so3.FindProperty("headRotationOffsetY");
|
||||
@ -826,11 +898,69 @@ public class RetargetingControlWindow : EditorWindow
|
||||
so3.Dispose();
|
||||
|
||||
script.SaveSettings();
|
||||
Debug.Log($"전체 자동 보정 완료: avatarScale = {scaleRatio:F3}");
|
||||
Debug.Log($"전체 자동 보정 완료: avatarScale = {scaleRatio:F3}, hipsOffsetY = {finalHipsOffset:F4}m");
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
// ========== Auto Hips Offset ==========
|
||||
|
||||
/// <summary>
|
||||
/// 소스/타겟 다리 길이 차이로 힙 상하 오프셋을 계산합니다.
|
||||
/// 타겟 다리가 소스보다 짧으면 → 양수 (힙을 올려서 다리를 펴줌)
|
||||
/// 타겟 다리가 소스보다 길면 → 음수 (힙을 내려서 다리를 펴줌)
|
||||
/// </summary>
|
||||
private float CalculateHipsOffsetFromLegDifference(CustomRetargetingScript script)
|
||||
{
|
||||
var source = script.optitrackSource;
|
||||
Animator target = script.targetAnimator;
|
||||
|
||||
if (source == null || target == null || !target.isHuman)
|
||||
{
|
||||
Debug.LogWarning("소스 OptiTrack 또는 타겟 Animator가 설정되지 않았습니다.");
|
||||
return 0f;
|
||||
}
|
||||
|
||||
float sourceLeg = GetSourceLegLength(source);
|
||||
float targetLeg = GetLegLength(target);
|
||||
|
||||
if (sourceLeg < 0.01f || targetLeg < 0.01f)
|
||||
{
|
||||
Debug.LogWarning("다리 길이를 계산할 수 없습니다. 본이 올바르게 설정되어 있는지 확인해주세요.");
|
||||
return 0f;
|
||||
}
|
||||
|
||||
// 소스 다리가 더 길면 타겟이 뜨므로 힙을 내려야 함 (음수)
|
||||
// 소스 다리가 더 짧으면 타겟 다리가 구부러지므로 힙을 올려야 함 (양수)
|
||||
float diff = targetLeg - sourceLeg;
|
||||
Debug.Log($"소스 다리 길이: {sourceLeg:F4}, 타겟 다리 길이: {targetLeg:F4}, 힙 오프셋: {diff:F4}m");
|
||||
return diff;
|
||||
}
|
||||
|
||||
private float GetLegLength(Animator animator)
|
||||
{
|
||||
Transform upperLeg = animator.GetBoneTransform(HumanBodyBones.LeftUpperLeg);
|
||||
Transform lowerLeg = animator.GetBoneTransform(HumanBodyBones.LeftLowerLeg);
|
||||
Transform foot = animator.GetBoneTransform(HumanBodyBones.LeftFoot);
|
||||
|
||||
if (upperLeg == null || lowerLeg == null || foot == null) return 0f;
|
||||
|
||||
return Vector3.Distance(upperLeg.position, lowerLeg.position)
|
||||
+ Vector3.Distance(lowerLeg.position, foot.position);
|
||||
}
|
||||
|
||||
private float GetSourceLegLength(OptitrackSkeletonAnimator_Mingle source)
|
||||
{
|
||||
Transform upperLeg = source.GetBoneTransform(HumanBodyBones.LeftUpperLeg);
|
||||
Transform lowerLeg = source.GetBoneTransform(HumanBodyBones.LeftLowerLeg);
|
||||
Transform foot = source.GetBoneTransform(HumanBodyBones.LeftFoot);
|
||||
|
||||
if (upperLeg == null || lowerLeg == null || foot == null) return 0f;
|
||||
|
||||
return Vector3.Distance(upperLeg.position, lowerLeg.position)
|
||||
+ Vector3.Distance(lowerLeg.position, foot.position);
|
||||
}
|
||||
|
||||
private void CalibrateHeadToForward(SerializedObject so, SerializedProperty xProp, SerializedProperty yProp, SerializedProperty zProp)
|
||||
{
|
||||
CustomRetargetingScript script = so.targetObject as CustomRetargetingScript;
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
using UnityEngine;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace KindRetargeting
|
||||
{
|
||||
@ -8,10 +9,8 @@ namespace KindRetargeting
|
||||
private Animator animator;
|
||||
private HumanPoseHandler humanPoseHandler;
|
||||
|
||||
// 매 프레임 재사용되는 캐시 (GC 압박 제거)
|
||||
private Transform[] cachedNonFingerBones;
|
||||
private Quaternion[] savedBoneRotations;
|
||||
private HumanPose cachedHumanPose;
|
||||
// 손가락을 제외한 모든 본의 로컬 회전 저장용 (SetHumanPose 호출 시 몸 복원용)
|
||||
private Dictionary<HumanBodyBones, Quaternion> savedBoneLocalRotations = new Dictionary<HumanBodyBones, Quaternion>();
|
||||
|
||||
// 손가락을 제외한 모든 휴먼본 목록
|
||||
private static readonly HumanBodyBones[] nonFingerBones = new HumanBodyBones[]
|
||||
@ -72,18 +71,6 @@ namespace KindRetargeting
|
||||
if (animator == null || !animator.isHuman) return;
|
||||
|
||||
humanPoseHandler = new HumanPoseHandler(animator.avatar, animator.transform);
|
||||
|
||||
// Transform 배열 + 회전 배열 사전 캐싱 (매 프레임 GetBoneTransform 50회 → 0회)
|
||||
cachedNonFingerBones = new Transform[nonFingerBones.Length];
|
||||
savedBoneRotations = new Quaternion[nonFingerBones.Length];
|
||||
for (int i = 0; i < nonFingerBones.Length; i++)
|
||||
{
|
||||
cachedNonFingerBones[i] = animator.GetBoneTransform(nonFingerBones[i]);
|
||||
}
|
||||
|
||||
// HumanPose 사전 할당 (muscles 배열 95개 float, GC 회피)
|
||||
humanPoseHandler.GetHumanPose(ref cachedHumanPose);
|
||||
|
||||
isInitialized = true;
|
||||
}
|
||||
|
||||
@ -105,29 +92,40 @@ namespace KindRetargeting
|
||||
|
||||
private void UpdateMuscleValues()
|
||||
{
|
||||
// 1. 비손가락 본의 로컬 회전을 배열에 저장 (캐시된 Transform 재사용)
|
||||
for (int i = 0; i < cachedNonFingerBones.Length; i++)
|
||||
// 1. 손가락을 제외한 모든 본의 로컬 회전 저장 (SetHumanPose 호출 전)
|
||||
savedBoneLocalRotations.Clear();
|
||||
for (int i = 0; i < nonFingerBones.Length; i++)
|
||||
{
|
||||
Transform bone = cachedNonFingerBones[i];
|
||||
if (bone != null) savedBoneRotations[i] = bone.localRotation;
|
||||
Transform bone = animator.GetBoneTransform(nonFingerBones[i]);
|
||||
if (bone != null)
|
||||
{
|
||||
savedBoneLocalRotations[nonFingerBones[i]] = bone.localRotation;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. HumanPose 가져오기 (재사용 필드, GC 없음) + 손가락 머슬 설정
|
||||
humanPoseHandler.GetHumanPose(ref cachedHumanPose);
|
||||
// 2. HumanPose 가져오기 및 손가락 머슬 설정
|
||||
HumanPose humanPose = new HumanPose();
|
||||
humanPoseHandler.GetHumanPose(ref humanPose);
|
||||
|
||||
// 왼손 제어
|
||||
SetHandMuscles(true, leftThumbCurl, leftIndexCurl, leftMiddleCurl, leftRingCurl,
|
||||
leftPinkyCurl, leftSpreadFingers, ref cachedHumanPose);
|
||||
leftPinkyCurl, leftSpreadFingers, ref humanPose);
|
||||
|
||||
// 오른손 제어
|
||||
SetHandMuscles(false, rightThumbCurl, rightIndexCurl, rightMiddleCurl, rightRingCurl,
|
||||
rightPinkyCurl, rightSpreadFingers, ref cachedHumanPose);
|
||||
rightPinkyCurl, rightSpreadFingers, ref humanPose);
|
||||
|
||||
// 3. 머슬 포즈 적용 (손가락 포함 전체 본에 영향)
|
||||
humanPoseHandler.SetHumanPose(ref cachedHumanPose);
|
||||
humanPoseHandler.SetHumanPose(ref humanPose);
|
||||
|
||||
// 4. 비손가락 본 로컬 회전 복원 (본 길이 변형 방지)
|
||||
for (int i = 0; i < cachedNonFingerBones.Length; i++)
|
||||
// 4. 손가락을 제외한 모든 본의 로컬 회전 복원 (본 길이 변형 방지)
|
||||
foreach (var kvp in savedBoneLocalRotations)
|
||||
{
|
||||
Transform bone = cachedNonFingerBones[i];
|
||||
if (bone != null) bone.localRotation = savedBoneRotations[i];
|
||||
Transform bone = animator.GetBoneTransform(kvp.Key);
|
||||
if (bone != null)
|
||||
{
|
||||
bone.localRotation = kvp.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -39,14 +39,13 @@ namespace KindRetargeting
|
||||
private CustomRetargetingScript crs;
|
||||
private Transform characterRoot;
|
||||
|
||||
// 가중치 배열: 인덱스 의미는 Initialize 주석 참조 (크기 고정 → float[] 사용으로 GC/인덱싱 비용 절감)
|
||||
readonly float[] leftArmEndWeights = new float[2]; // [0] 양손거리 [1] 프랍거리
|
||||
readonly float[] rightArmEndWeights = new float[2];
|
||||
readonly float[] leftLegEndWeights = new float[2]; // [0] 앉기 수평거리 [1] 발 높이
|
||||
readonly float[] rightLegEndWeights = new float[2];
|
||||
List<float> leftArmEndWeights = new List<float>();
|
||||
List<float> rightArmEndWeights = new List<float>();
|
||||
List<float> leftLegEndWeights = new List<float>();
|
||||
List<float> rightLegEndWeights = new List<float>();
|
||||
|
||||
readonly float[] leftLegBendWeights = new float[1] { 1f };
|
||||
readonly float[] rightLegBendWeights = new float[1] { 1f };
|
||||
List<float> leftLegBendWeights = new List<float>();
|
||||
List<float> rightLegBendWeights = new List<float>();
|
||||
|
||||
private float MasterleftArmEndWeights = 0f;
|
||||
private float MasterrightArmEndWeights = 0f;
|
||||
@ -56,11 +55,9 @@ namespace KindRetargeting
|
||||
private float MasterrightLegBendWeights = 0f;
|
||||
|
||||
public List<Transform> props = new List<Transform>();
|
||||
// SitChairDistances 매 프레임 GetComponent 비용 회피용 (Initialize 시점 한 번 캐싱)
|
||||
private List<Transform> chairProps = new List<Transform>();
|
||||
|
||||
// 힙스 가중치: [0] 의자 거리 [1] 지면 높이
|
||||
readonly float[] hipsWeights = new float[2] { 1f, 1f };
|
||||
// 힙스 가중치 리스트 추가
|
||||
List<float> hipsWeights = new List<float>();
|
||||
private float MasterHipsWeight = 1f;
|
||||
|
||||
// 의자 좌석 높이 오프셋 (월드 Y 기준)
|
||||
@ -79,26 +76,44 @@ namespace KindRetargeting
|
||||
|
||||
InitWeightLayers();
|
||||
|
||||
//프랍 오브젝트 찾기 + 의자 타입 별도 캐싱
|
||||
var allPropControllers = Object.FindObjectsByType<PropTypeController>(FindObjectsSortMode.None);
|
||||
props = allPropControllers.Select(c => c.transform).ToList();
|
||||
chairProps = allPropControllers
|
||||
.Where(c => c.propType == EnumsList.PropType.Chair)
|
||||
.Select(c => c.transform)
|
||||
.ToList();
|
||||
//프랍 오브젝트 찾기
|
||||
props = Object.FindObjectsByType<PropTypeController>(FindObjectsSortMode.None).Select(controller => controller.transform).ToList();
|
||||
|
||||
// 다른 캐릭터의 손을 props에 추가
|
||||
GetHand();
|
||||
|
||||
// 가중치 배열은 필드 선언 시 고정 크기로 초기화됨 (다리 EndWeight[1]만 1f 기본값)
|
||||
leftLegEndWeights[1] = 1f;
|
||||
rightLegEndWeights[1] = 1f;
|
||||
//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); // 기본 가중치
|
||||
|
||||
if (this.characterRoot == null)
|
||||
{
|
||||
this.characterRoot = crs.transform;
|
||||
}
|
||||
|
||||
// 힙스 가중치 초기화 인덱스 0번
|
||||
hipsWeights.Add(1f); // 의자 거리 기반 가중치
|
||||
|
||||
// 지면 높이 기반 가중치 초기화 인덱스 1번
|
||||
hipsWeights.Add(1f); // 지면 높이 기반 가중치
|
||||
|
||||
// 발 높이 기반 가중치 초기화 인덱스 1번
|
||||
leftLegEndWeights.Add(1f);
|
||||
rightLegEndWeights.Add(1f);
|
||||
|
||||
isInitialized = true;
|
||||
}
|
||||
|
||||
@ -222,7 +237,7 @@ namespace KindRetargeting
|
||||
}
|
||||
}
|
||||
|
||||
private void ProcessSitLegWeight(Transform hips, Transform footTarget, float[] weightArr, int weightIndex)
|
||||
private void ProcessSitLegWeight(Transform hips, Transform footTarget, List<float> weightList, int weightIndex)
|
||||
{
|
||||
if (footTarget == null) return;
|
||||
|
||||
@ -241,7 +256,7 @@ namespace KindRetargeting
|
||||
float weight = 1f - Mathf.Clamp01((horizontalDistance - MIN_LEG_DISTANCE_RATIO) /
|
||||
(MAX_LEG_DISTANCE_RATIO - MIN_LEG_DISTANCE_RATIO));
|
||||
|
||||
weightArr[weightIndex] = weight;
|
||||
weightList[weightIndex] = weight;
|
||||
}
|
||||
|
||||
void PropDistances()
|
||||
@ -283,29 +298,40 @@ namespace KindRetargeting
|
||||
{
|
||||
if (crs == null) return;
|
||||
|
||||
Transform hipsTransform = crs?.optitrackSource?.GetBoneTransform(HumanBodyBones.Hips);
|
||||
if (hipsTransform == null || chairProps.Count == 0)
|
||||
Transform hipsTransform = crs.optitrackSource.GetBoneTransform(HumanBodyBones.Hips);
|
||||
if (hipsTransform != null && props != null)
|
||||
{
|
||||
hipsWeights[0] = 1f;
|
||||
targetChairSeatOffset = 0f;
|
||||
return;
|
||||
}
|
||||
float minDistance = float.MaxValue;
|
||||
bool foundChair = false;
|
||||
|
||||
// chairProps는 Initialize에서 미리 필터링됨 (매 프레임 GetComponent 호출 회피)
|
||||
float minDistance = float.MaxValue;
|
||||
for (int i = 0; i < chairProps.Count; i++)
|
||||
{
|
||||
Transform chair = chairProps[i];
|
||||
if (chair == null) continue;
|
||||
Vector3 chairPos = chair.childCount > 0 ? chair.GetChild(0).position : chair.position;
|
||||
float distance = Vector3.Distance(hipsTransform.position, chairPos);
|
||||
if (distance < minDistance) minDistance = distance;
|
||||
}
|
||||
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;
|
||||
// t가 0에 가까울수록 의자에 가까움 → 좌석 오프셋 더 적용
|
||||
targetChairSeatOffset = chairSeatHeightOffset * (1f - t);
|
||||
float t = Mathf.Clamp01((minDistance - hipsMinDistance) / (hipsMaxDistance - hipsMinDistance));
|
||||
hipsWeights[0] = t; // 직접 HipsWeightOffset 수정 대신 배열에 저장
|
||||
|
||||
// 의자 좌석 높이 오프셋 계산 (가까울수록 더 적용) - 캐릭터별 설정 사용
|
||||
if (foundChair)
|
||||
{
|
||||
// t가 0에 가까울수록 의자에 가까움 → 좌석 오프셋 더 적용
|
||||
targetChairSeatOffset = chairSeatHeightOffset * (1f - t);
|
||||
}
|
||||
else
|
||||
{
|
||||
targetChairSeatOffset = 0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -374,18 +400,18 @@ namespace KindRetargeting
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 배열에서 최대값을 찾습니다.
|
||||
/// 리스트에서 최대값을 찾습니다.
|
||||
/// </summary>
|
||||
private float GetMaxValue(float[] arr)
|
||||
private float GetMaxValue(List<float> list)
|
||||
{
|
||||
if (arr.Length == 0) return 0f;
|
||||
if (list.Count == 0) return 0f;
|
||||
|
||||
float max = arr[0];
|
||||
for (int i = 1; i < arr.Length; i++)
|
||||
float max = list[0];
|
||||
for (int i = 1; i < list.Count; i++)
|
||||
{
|
||||
if (arr[i] > max)
|
||||
if (list[i] > max)
|
||||
{
|
||||
max = arr[i];
|
||||
max = list[i];
|
||||
}
|
||||
}
|
||||
return max;
|
||||
@ -393,18 +419,18 @@ namespace KindRetargeting
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 배열에서 최소값을 찾습니다.
|
||||
/// 리스트에서 최소값을 찾습니다.
|
||||
/// </summary>
|
||||
private float GetMinValue(float[] arr)
|
||||
private float GetMinValue(List<float> list)
|
||||
{
|
||||
if (arr.Length == 0) return 0f;
|
||||
if (list.Count == 0) return 0f;
|
||||
|
||||
float min = arr[0];
|
||||
for (int i = 1; i < arr.Length; i++)
|
||||
float min = list[0];
|
||||
for (int i = 1; i < list.Count; i++)
|
||||
{
|
||||
if (arr[i] < min)
|
||||
if (list[i] < min)
|
||||
{
|
||||
min = arr[i];
|
||||
min = list[i];
|
||||
}
|
||||
}
|
||||
return min;
|
||||
@ -447,7 +473,7 @@ namespace KindRetargeting
|
||||
{
|
||||
if (crs == null || ikSolver == null) return;
|
||||
|
||||
Transform hipsTransform = crs?.optitrackSource?.GetBoneTransform(HumanBodyBones.Hips);
|
||||
Transform hipsTransform = crs.optitrackSource.GetBoneTransform(HumanBodyBones.Hips);
|
||||
if (hipsTransform != null)
|
||||
{
|
||||
float groundHeight = characterRoot.position.y;
|
||||
@ -480,7 +506,7 @@ namespace KindRetargeting
|
||||
);
|
||||
}
|
||||
|
||||
private void ProcessFootHeightWeight(Transform footTarget, float[] weightArr, int weightIndex)
|
||||
private void ProcessFootHeightWeight(Transform footTarget, List<float> weightList, int weightIndex)
|
||||
{
|
||||
if (footTarget == null) return;
|
||||
|
||||
@ -492,7 +518,7 @@ namespace KindRetargeting
|
||||
(footHeightMaxThreshold - footHeightMinThreshold));
|
||||
|
||||
// 계산된 가중치 설정
|
||||
weightArr[weightIndex] = weight;
|
||||
weightList[weightIndex] = weight;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BIN
Assets/Scripts/KindRetargeting/README.md
(Stored with Git LFS)
BIN
Assets/Scripts/KindRetargeting/README.md
(Stored with Git LFS)
Binary file not shown.
@ -166,6 +166,13 @@ namespace KindRetargeting.Remote
|
||||
}
|
||||
break;
|
||||
|
||||
case "autoHipsOffset":
|
||||
{
|
||||
string charId = json["characterId"]?.ToString();
|
||||
AutoHipsOffset(charId);
|
||||
}
|
||||
break;
|
||||
|
||||
case "autoCalibrateAll":
|
||||
{
|
||||
string charId = json["characterId"]?.ToString();
|
||||
@ -226,6 +233,15 @@ namespace KindRetargeting.Remote
|
||||
|
||||
var data = new Dictionary<string, object>
|
||||
{
|
||||
// 힙 위치 보정 (로컬)
|
||||
{ "hipsVertical", GetPrivateField<float>(script, "hipsOffsetY") },
|
||||
{ "hipsForward", GetPrivateField<float>(script, "hipsOffsetZ") },
|
||||
{ "hipsHorizontal", GetPrivateField<float>(script, "hipsOffsetX") },
|
||||
|
||||
// 무릎 위치 조정
|
||||
{ "kneeFrontBackWeight", GetPrivateField<float>(script, "kneeFrontBackWeight") },
|
||||
{ "kneeInOutWeight", GetPrivateField<float>(script, "kneeInOutWeight") },
|
||||
|
||||
// 발 IK 위치 조정
|
||||
{ "feetForwardBackward", GetPrivateField<float>(script, "footFrontBackOffset") },
|
||||
{ "feetNarrow", GetPrivateField<float>(script, "footInOutOffset") },
|
||||
@ -300,6 +316,25 @@ namespace KindRetargeting.Remote
|
||||
|
||||
switch (property)
|
||||
{
|
||||
// 힙 위치 보정
|
||||
case "hipsVertical":
|
||||
SetPrivateField(script, "hipsOffsetY", value);
|
||||
break;
|
||||
case "hipsForward":
|
||||
SetPrivateField(script, "hipsOffsetZ", value);
|
||||
break;
|
||||
case "hipsHorizontal":
|
||||
SetPrivateField(script, "hipsOffsetX", value);
|
||||
break;
|
||||
|
||||
// 무릎 위치 조정
|
||||
case "kneeFrontBackWeight":
|
||||
SetPrivateField(script, "kneeFrontBackWeight", value);
|
||||
break;
|
||||
case "kneeInOutWeight":
|
||||
SetPrivateField(script, "kneeInOutWeight", value);
|
||||
break;
|
||||
|
||||
// 발 IK 위치 조정
|
||||
case "feetForwardBackward":
|
||||
SetPrivateField(script, "footFrontBackOffset", value);
|
||||
@ -535,6 +570,19 @@ namespace KindRetargeting.Remote
|
||||
SendStatus(true, "정면 캘리브레이션 완료");
|
||||
}
|
||||
|
||||
private void AutoHipsOffset(string characterId)
|
||||
{
|
||||
var script = FindCharacter(characterId);
|
||||
if (script == null) return;
|
||||
|
||||
float offset = CalculateHipsOffsetFromLegDifference(script);
|
||||
SetPrivateField(script, "hipsOffsetY", offset);
|
||||
script.SaveSettings();
|
||||
|
||||
SendCharacterData(characterId);
|
||||
SendStatus(true, $"다리 길이 자동 보정 완료: hipsOffsetY={offset:F4}");
|
||||
}
|
||||
|
||||
private void AutoCalibrateAll(string characterId)
|
||||
{
|
||||
var script = FindCharacter(characterId);
|
||||
@ -548,9 +596,10 @@ namespace KindRetargeting.Remote
|
||||
return;
|
||||
}
|
||||
|
||||
// Step 1: 크기 초기화
|
||||
// Step 1: 크기 초기화 + 힙 오프셋 계산
|
||||
script.ResetScale();
|
||||
SetPrivateField(script, "avatarScale", 1f);
|
||||
SetPrivateField(script, "hipsOffsetY", CalculateHipsOffsetFromLegDifference(script));
|
||||
|
||||
// Step 2: 1프레임 후 목 높이 비율로 크기 조정
|
||||
StartCoroutine(AutoCalibrateCoroutine(script, characterId));
|
||||
@ -577,7 +626,8 @@ namespace KindRetargeting.Remote
|
||||
|
||||
yield return null; // 1프레임 대기
|
||||
|
||||
// Step 3: 머리 정면 캘리브레이션 + 저장 (힙 보정은 매 프레임 자동)
|
||||
// Step 3: 힙 오프셋 재계산 + 머리 정면 캘리브레이션
|
||||
SetPrivateField(script, "hipsOffsetY", CalculateHipsOffsetFromLegDifference(script));
|
||||
script.CalibrateHeadToForward();
|
||||
script.SaveSettings();
|
||||
|
||||
@ -585,6 +635,37 @@ namespace KindRetargeting.Remote
|
||||
SendStatus(true, $"전체 자동 보정 완료: avatarScale={scaleRatio:F3}");
|
||||
}
|
||||
|
||||
private float CalculateHipsOffsetFromLegDifference(CustomRetargetingScript script)
|
||||
{
|
||||
var source = script.optitrackSource;
|
||||
Animator targetAnim = script.targetAnimator;
|
||||
if (source == null || targetAnim == null) return 0f;
|
||||
|
||||
float sourceLeg = GetSourceLegLength(source);
|
||||
float targetLeg = GetLegLength(targetAnim);
|
||||
if (sourceLeg < 0.01f || targetLeg < 0.01f) return 0f;
|
||||
|
||||
return targetLeg - sourceLeg;
|
||||
}
|
||||
|
||||
private float GetSourceLegLength(OptitrackSkeletonAnimator_Mingle source)
|
||||
{
|
||||
Transform upper = source.GetBoneTransform(HumanBodyBones.LeftUpperLeg);
|
||||
Transform lower = source.GetBoneTransform(HumanBodyBones.LeftLowerLeg);
|
||||
Transform foot = source.GetBoneTransform(HumanBodyBones.LeftFoot);
|
||||
if (upper == null || lower == null || foot == null) return 0f;
|
||||
return Vector3.Distance(upper.position, lower.position) + Vector3.Distance(lower.position, foot.position);
|
||||
}
|
||||
|
||||
private float GetLegLength(Animator animator)
|
||||
{
|
||||
Transform upper = animator.GetBoneTransform(HumanBodyBones.LeftUpperLeg);
|
||||
Transform lower = animator.GetBoneTransform(HumanBodyBones.LeftLowerLeg);
|
||||
Transform foot = animator.GetBoneTransform(HumanBodyBones.LeftFoot);
|
||||
if (upper == null || lower == null || foot == null) return 0f;
|
||||
return Vector3.Distance(upper.position, lower.position) + Vector3.Distance(lower.position, foot.position);
|
||||
}
|
||||
|
||||
private CustomRetargetingScript FindCharacter(string characterId)
|
||||
{
|
||||
foreach (var script in registeredCharacters)
|
||||
@ -624,25 +705,11 @@ namespace KindRetargeting.Remote
|
||||
|
||||
#region Reflection Helpers
|
||||
|
||||
// 슬라이더 드래그(60fps) 시 매번 GetField 호출되는 것을 방지.
|
||||
// (Type, fieldName) → FieldInfo 1회 lookup 후 캐싱.
|
||||
private static readonly Dictionary<(System.Type, string), FieldInfo> _fieldCache = new Dictionary<(System.Type, string), FieldInfo>();
|
||||
private const BindingFlags FIELD_FLAGS = BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public;
|
||||
|
||||
private static FieldInfo ResolveField(System.Type type, string fieldName)
|
||||
{
|
||||
var key = (type, fieldName);
|
||||
if (_fieldCache.TryGetValue(key, out FieldInfo cached)) return cached;
|
||||
FieldInfo field = type.GetField(fieldName, FIELD_FLAGS);
|
||||
_fieldCache[key] = field;
|
||||
return field;
|
||||
}
|
||||
|
||||
private T GetPrivateField<T>(object obj, string fieldName)
|
||||
{
|
||||
try
|
||||
{
|
||||
var field = ResolveField(obj.GetType(), fieldName);
|
||||
var field = obj.GetType().GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public);
|
||||
if (field != null)
|
||||
return (T)field.GetValue(obj);
|
||||
|
||||
@ -660,7 +727,7 @@ namespace KindRetargeting.Remote
|
||||
{
|
||||
try
|
||||
{
|
||||
var field = ResolveField(obj.GetType(), fieldName);
|
||||
var field = obj.GetType().GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public);
|
||||
if (field != null)
|
||||
{
|
||||
field.SetValue(obj, value);
|
||||
|
||||
@ -5,9 +5,6 @@ using KindRetargeting;
|
||||
[DefaultExecutionOrder(16001)]
|
||||
public class SimplePoseTransfer : MonoBehaviour
|
||||
{
|
||||
// 0~54: 휴머노이드 본 (몸체 + 손가락). HumanBodyBones.LastBone 직전까지.
|
||||
private const int BoneCount = 55;
|
||||
|
||||
[System.Serializable]
|
||||
public class TargetEntry
|
||||
{
|
||||
@ -124,7 +121,7 @@ public class SimplePoseTransfer : MonoBehaviour
|
||||
|
||||
private void InitializeTargetBones()
|
||||
{
|
||||
boneRotationDifferences = new Quaternion[targets.Count, BoneCount];
|
||||
boneRotationDifferences = new Quaternion[targets.Count, 55];
|
||||
|
||||
for (int i = 0; i < targets.Count; i++)
|
||||
{
|
||||
@ -136,7 +133,7 @@ public class SimplePoseTransfer : MonoBehaviour
|
||||
}
|
||||
|
||||
// 55개의 휴머노이드 본에 대해 회전 차이 계산
|
||||
for (int j = 0; j < BoneCount; j++)
|
||||
for (int j = 0; j < 55; j++)
|
||||
{
|
||||
Transform sourceBoneTransform = sourceBone.GetBoneTransform((HumanBodyBones)j);
|
||||
Transform targetBoneTransform = animator.GetBoneTransform((HumanBodyBones)j);
|
||||
@ -152,19 +149,19 @@ public class SimplePoseTransfer : MonoBehaviour
|
||||
private void CacheAllBoneTransforms()
|
||||
{
|
||||
// 소스 본 캐싱
|
||||
cachedSourceBones = new Transform[BoneCount];
|
||||
for (int i = 0; i < BoneCount; i++)
|
||||
cachedSourceBones = new Transform[55];
|
||||
for (int i = 0; i < 55; i++)
|
||||
{
|
||||
cachedSourceBones[i] = sourceBone.GetBoneTransform((HumanBodyBones)i);
|
||||
}
|
||||
|
||||
// 타겟 본 캐싱
|
||||
cachedTargetBones = new Transform[targets.Count, BoneCount];
|
||||
cachedTargetBones = new Transform[targets.Count, 55];
|
||||
for (int t = 0; t < targets.Count; t++)
|
||||
{
|
||||
Animator animator = targets[t].animator;
|
||||
if (animator == null) continue;
|
||||
for (int i = 0; i < BoneCount; i++)
|
||||
for (int i = 0; i < 55; i++)
|
||||
{
|
||||
cachedTargetBones[t, i] = animator.GetBoneTransform((HumanBodyBones)i);
|
||||
}
|
||||
@ -208,7 +205,7 @@ public class SimplePoseTransfer : MonoBehaviour
|
||||
}
|
||||
|
||||
// 모든 본에 대해 포즈 전송
|
||||
for (int i = 0; i < BoneCount; i++)
|
||||
for (int i = 0; i < 55; i++)
|
||||
{
|
||||
Transform targetBoneTransform = cachedTargetBones[targetIndex, i];
|
||||
Transform sourceBoneTransform = cachedSourceBones[i];
|
||||
|
||||
@ -45,8 +45,6 @@ namespace KindRetargeting
|
||||
public LimbIK leftLeg = new LimbIK();
|
||||
public LimbIK rightLeg = new LimbIK();
|
||||
|
||||
[HideInInspector] public int fabrikIterations = 6;
|
||||
|
||||
private bool isInitialized;
|
||||
|
||||
public void Initialize(Animator targetAnimator)
|
||||
@ -88,25 +86,6 @@ namespace KindRetargeting
|
||||
limb.localBendNormal = Quaternion.Inverse(limb.upper.rotation) * bendNormal;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// avatarScale 등으로 아바타 크기가 바뀌었을 때 호출하여 본 길이 캐시를 재계산.
|
||||
/// </summary>
|
||||
public void RefreshLimbLengths()
|
||||
{
|
||||
if (!isInitialized) return;
|
||||
RecacheLength(leftArm);
|
||||
RecacheLength(rightArm);
|
||||
RecacheLength(leftLeg);
|
||||
RecacheLength(rightLeg);
|
||||
}
|
||||
|
||||
private void RecacheLength(LimbIK limb)
|
||||
{
|
||||
if (limb.upper == null || limb.lower == null || limb.end == null) return;
|
||||
limb.upperLength = Vector3.Distance(limb.upper.position, limb.lower.position);
|
||||
limb.lowerLength = Vector3.Distance(limb.lower.position, limb.end.position);
|
||||
}
|
||||
|
||||
public void OnUpdate()
|
||||
{
|
||||
if (!isInitialized) return;
|
||||
@ -130,9 +109,8 @@ namespace KindRetargeting
|
||||
Quaternion fkLowerRot = limb.lower.rotation;
|
||||
Quaternion fkEndRot = limb.end.rotation;
|
||||
|
||||
// 본 길이는 Initialize/RefreshLimbLengths에서 캐싱됨 (avatarScale 변경 시 갱신)
|
||||
float upperLen = limb.upperLength;
|
||||
float lowerLen = limb.lowerLength;
|
||||
float upperLen = Vector3.Distance(limb.upper.position, limb.lower.position);
|
||||
float lowerLen = Vector3.Distance(limb.lower.position, limb.end.position);
|
||||
|
||||
Vector3 targetPos = limb.target.position;
|
||||
|
||||
@ -171,61 +149,54 @@ namespace KindRetargeting
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 소스 무릎 위치를 타겟 프레임으로 옮긴 raw 위치를 초기값으로 두고,
|
||||
/// FABRIK 2회 반복으로 hip↔knee=upperLen, knee↔target=lowerLen 을 모두 만족시킵니다.
|
||||
///
|
||||
/// 정규화(normalize)에 의한 부호 증폭이 없어 입력 노이즈가 출력에 그대로 비례 전달됩니다.
|
||||
/// → 다리 거의 펴진 상태에서 모캡 노이즈로 굽힘 부호가 흔들려도 점프 없이 연속적.
|
||||
/// → 의도된 역관절도 자연스럽게 따라감 (소스 무릎이 어느 쪽이든 raw로 받음).
|
||||
/// 소스 무릎 위치를 hip→foot 직선 기준으로 분해(투영+수직)한 뒤
|
||||
/// 타겟 비율로 스케일하여 타겟 무릎 위치를 결정합니다.
|
||||
/// 소스가 역관절이면 수직 성분이 반대쪽 → 타겟도 자연스럽게 역관절.
|
||||
/// </summary>
|
||||
private Vector3 ComputeKneePosFromSource(LimbIK limb, float upperLen, float lowerLen, Vector3 targetPos)
|
||||
{
|
||||
float chainLength = upperLen + lowerLen;
|
||||
|
||||
// 소스 hip→foot 방향
|
||||
Vector3 sourceHipToFoot = limb.sourceEnd.position - limb.sourceUpper.position;
|
||||
float sourceHipToFootMag = sourceHipToFoot.magnitude;
|
||||
if (sourceHipToFootMag < 0.001f) return limb.lower.position;
|
||||
Vector3 sourceHipToFootDir = sourceHipToFoot / sourceHipToFootMag;
|
||||
|
||||
// 소스 hip→knee (굽힘 정보 raw, 정규화 X → 노이즈 증폭 없음)
|
||||
Vector3 sourceHipToKnee = limb.sourceLower.position - limb.sourceUpper.position;
|
||||
float sourceUpperLen = sourceHipToKnee.magnitude;
|
||||
// 소스 체인 길이
|
||||
float sourceUpperLen = Vector3.Distance(limb.sourceUpper.position, limb.sourceLower.position);
|
||||
float sourceLowerLen = Vector3.Distance(limb.sourceLower.position, limb.sourceEnd.position);
|
||||
float sourceChain = sourceUpperLen + sourceLowerLen;
|
||||
if (sourceChain < 0.001f) return limb.lower.position;
|
||||
float targetChain = upperLen + lowerLen;
|
||||
|
||||
// 타겟 hip→target 방향 / 거리
|
||||
Vector3 toTarget = targetPos - limb.upper.position;
|
||||
float targetDist = toTarget.magnitude;
|
||||
if (targetDist < 0.001f) return limb.lower.position;
|
||||
Vector3 toTargetDir = toTarget / targetDist;
|
||||
if (sourceChain < 0.001f || targetChain < 0.001f)
|
||||
return limb.lower.position;
|
||||
|
||||
// 도달 불가 거리는 클램프 (FABRIK 진동 방지)
|
||||
// |upper-lower| 보다 가깝거나 chainLength 보다 멀면 본 길이 제약을 만족하는 무릎 위치 없음
|
||||
float effectiveDist = Mathf.Clamp(targetDist, Mathf.Abs(upperLen - lowerLen) + 0.001f, chainLength - 0.001f);
|
||||
Vector3 effectiveTarget = limb.upper.position + toTargetDir * effectiveDist;
|
||||
// 소스 hip → foot 방향
|
||||
Vector3 sourceHipToFoot = limb.sourceEnd.position - limb.sourceUpper.position;
|
||||
float sourceHipToFootMag = sourceHipToFoot.magnitude;
|
||||
if (sourceHipToFootMag < 0.001f)
|
||||
return limb.lower.position;
|
||||
Vector3 sourceHipToFootDir = sourceHipToFoot / sourceHipToFootMag;
|
||||
|
||||
// 소스 → 타겟 프레임 회전 + 다리 길이 비율 스케일 → 초기 무릎 위치
|
||||
Quaternion frameRotation = Quaternion.FromToRotation(sourceHipToFootDir, toTargetDir);
|
||||
float scale = (upperLen + lowerLen) / sourceChain;
|
||||
Vector3 kneePos = limb.upper.position + frameRotation * (sourceHipToKnee * scale);
|
||||
// 소스 hip → knee 벡터를 투영(직선 성분)과 수직(오프셋 성분)으로 분해
|
||||
Vector3 sourceHipToKnee = limb.sourceLower.position - limb.sourceUpper.position;
|
||||
float projection = Vector3.Dot(sourceHipToKnee, sourceHipToFootDir);
|
||||
Vector3 rejection = sourceHipToKnee - projection * sourceHipToFootDir;
|
||||
|
||||
// FABRIK: knee 를 lowerLen 구면(target 중심) ↔ upperLen 구면(hip 중심) 사이에서 번갈아 투영
|
||||
for (int i = 0; i < fabrikIterations; i++)
|
||||
{
|
||||
// 1) target 중심 lowerLen 구면에 투영 → knee↔target = lowerLen 보장
|
||||
Vector3 fromTarget = kneePos - effectiveTarget;
|
||||
float distFromTarget = fromTarget.magnitude;
|
||||
if (distFromTarget > 0.001f)
|
||||
kneePos = effectiveTarget + (fromTarget / distFromTarget) * lowerLen;
|
||||
// 소스 체인 길이 대비 비율로 정규화 → 타겟 체인 길이로 스케일
|
||||
float scale = targetChain / sourceChain;
|
||||
float scaledProjection = projection * scale;
|
||||
Vector3 scaledRejection = rejection * scale;
|
||||
|
||||
// 2) hip 중심 upperLen 구면에 투영 → hip↔knee = upperLen 보장
|
||||
Vector3 fromHip = kneePos - limb.upper.position;
|
||||
float distFromHip = fromHip.magnitude;
|
||||
if (distFromHip > 0.001f)
|
||||
kneePos = limb.upper.position + (fromHip / distFromHip) * upperLen;
|
||||
}
|
||||
// 타겟 hip → foot 방향
|
||||
Vector3 targetHipToFoot = targetPos - limb.upper.position;
|
||||
float targetHipToFootMag = targetHipToFoot.magnitude;
|
||||
if (targetHipToFootMag < 0.001f)
|
||||
return limb.lower.position;
|
||||
Vector3 targetHipToFootDir = targetHipToFoot / targetHipToFootMag;
|
||||
|
||||
// 소스 프레임 → 타겟 프레임으로 수직 성분 회전
|
||||
// (소스와 타겟의 사지 방향이 다를 때 팔꿈치/무릎 오프셋 방향 보정)
|
||||
Quaternion frameRotation = Quaternion.FromToRotation(sourceHipToFootDir, targetHipToFootDir);
|
||||
Vector3 rotatedRejection = frameRotation * scaledRejection;
|
||||
|
||||
// 타겟 관절 위치: 타겟 upper에서 투영 + 회전된 수직 성분 적용
|
||||
Vector3 kneePos = limb.upper.position
|
||||
+ targetHipToFootDir * scaledProjection
|
||||
+ rotatedRejection;
|
||||
|
||||
return kneePos;
|
||||
}
|
||||
@ -292,5 +263,24 @@ namespace KindRetargeting
|
||||
bendDir = Vector3.Cross(Vector3.up, toTargetDir);
|
||||
return bendDir.normalized;
|
||||
}
|
||||
|
||||
public float CalculateAutoFloorHeight(float comfortRatio = 0.98f)
|
||||
{
|
||||
if (animator == null || leftLeg.upper == null || leftLeg.lower == null || leftLeg.end == null) return 0f;
|
||||
|
||||
float upperLen = Vector3.Distance(leftLeg.upper.position, leftLeg.lower.position);
|
||||
float lowerLen = Vector3.Distance(leftLeg.lower.position, leftLeg.end.position);
|
||||
float totalLegLength = upperLen + lowerLen;
|
||||
float comfortHeight = totalLegLength * comfortRatio;
|
||||
|
||||
Transform hips = animator.GetBoneTransform(HumanBodyBones.Hips);
|
||||
Transform foot = leftLeg.end;
|
||||
if (hips == null || foot == null) return 0f;
|
||||
|
||||
float currentHipToFoot = hips.position.y - foot.position.y;
|
||||
float heightDiff = currentHipToFoot - comfortHeight;
|
||||
|
||||
return -heightDiff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user