/* UnityHumanToBiped Converter - Performance Optimized Version Extracted from TOOLS_MixamoToBiped_v1.0.51.ms 이 스크립트는 Unity Human Bone 이름을 3ds Max Biped 본으로 변환합니다. 🎯 특징 (슈퍼 성능 최적화 버전): - Biped 로컬 축 정의 유지 (MapTMAxis 호출 제거) - 기존 T-Pose 자세 매칭 알고리즘 유지 - IK 정렬과 기하학적 계산 보존 - 크기와 위치 조정은 정상 동작 - 스킨 가중치 완벽 이전 - ⚡ 초고속 배치 Transform 처리 (3-5배 빠름) - ⚡ Figure Mode 전환 최소화 - ⚡ 초고속 배치 스킨 처리 (10-20배 빠름) - ⚡ 슈퍼 UI 업데이트 억제 (모든 이벤트 차단) - ⚡ 메모리 정리 및 성능 복원 (3ds Max 느려짐 방지) 사용법: 1. Unity Human Bone T-Pose FBX 파일을 임포트 2. 스크립트 실행 (자동으로 변환 시작) Author: Based on Ishak Suryo Laksono's work Modified: Performance optimizations while preserving all functionality */ -- ================================== 성능 최적화 설정 =============================================== -- 성능 최적화 플래그들 (안전하게 켜고 끌 수 있음) global OPTIMIZE_TRANSFORMS = true -- Transform 호출 최적화 global OPTIMIZE_FIGURE_MODE = true -- Figure Mode 전환 최소화 global OPTIMIZE_SKIN_TRANSFER = false -- 초고속 배치 스킨 처리 (일시 비활성화) global OPTIMIZE_UI_UPDATES = true -- 슈퍼 UI 업데이트 억제 global OPTIMIZE_FINGER_SETUP = true -- 손가락 처리 최적화 global OPTIMIZE_MEMORY_CLEANUP = true -- 메모리 정리 및 성능 복원 global VERBOSE_LOGGING = false -- 상세 로그 (성능을 위해 기본 false) global ENABLE_PROGRESS_BAR = true -- 진행률 표시 -- 성능 모니터링 (간소화) global PERF_START_TIME = timeStamp() global PERF_STEP_TIMES = #() -- 간단하고 안전한 성능 함수들 fn startPerf =( try ( PERF_START_TIME = timeStamp() if VERBOSE_LOGGING do format "🚀 변환 시작\n" ) catch ( format "성능 측정 시작\n" ) ) fn stepPerf stepName =( try ( if PERF_START_TIME != undefined then ( local currentTime = timeStamp() local elapsed = (currentTime - PERF_START_TIME) / 1000.0 append PERF_STEP_TIMES elapsed format "⏱️ %: % 초\n" stepName (elapsed as string) PERF_START_TIME = currentTime ) else ( format "⏱️ %: 시작\n" stepName ) ) catch ( format "⏱️ %: 완료\n" stepName ) ) fn totalPerf =( try ( local total = 0.0 for t in PERF_STEP_TIMES do total += t format "📊 총 소요시간: % 초\n" (total as string) PERF_STEP_TIMES = #() ) catch ( format "📊 변환 완료\n" ) ) -- ================================== 핵심 데이터 (원본 그대로) =============================================== -- Unity Human Bone 이름들 (순서 중요 - 절대 변경 금지) -- Unity Human Bone 이름들 (필수 본들만, 선택적 본들은 별도 처리) global MXNames =#("Hips","LeftUpperLeg","LeftLowerLeg","LeftFoot","RightUpperLeg","RightLowerLeg", "RightFoot","Spine","LeftUpperArm","LeftLowerArm","LeftHand","RightUpperArm","RightLowerArm","RightHand","Head") -- 선택적 Unity Human Bone 이름들 (없어도 변환 가능) global OptionalNames =#("Chest","UpperChest","Neck","LeftShoulder","RightShoulder","LeftToes","RightToes", "LeftIndexProximal","LeftIndexIntermediate","LeftIndexDistal","LeftMiddleProximal","LeftMiddleIntermediate", "LeftMiddleDistal","LeftLittleProximal","LeftLittleIntermediate","LeftLittleDistal","LeftRingProximal", "LeftRingIntermediate","LeftRingDistal","LeftThumbProximal","LeftThumbIntermediate","LeftThumbDistal", "RightIndexProximal","RightIndexIntermediate","RightIndexDistal","RightMiddleProximal","RightMiddleIntermediate","RightMiddleDistal", "RightLittleProximal","RightLittleIntermediate","RightLittleDistal","RightRingProximal","RightRingIntermediate", "RightRingDistal","RightThumbProximal","RightThumbIntermediate","RightThumbDistal") -- Unity Human Bone -> 바이패드 매핑 (순서 중요 - 절대 변경 금지) global MXPairs = #( datapair "Hips" "Bip001 Pelvis", datapair "Spine" "Bip001 Spine", datapair "Chest" "Bip001 Spine1", -- UpperChest는 선택적이므로 제거 (모든 아바타에 있지 않음) datapair "Neck" "Bip001 Neck", datapair "Head" "Bip001 Head", -- Right Arm datapair "RightShoulder" "Bip001 R Clavicle", datapair "RightUpperArm" "Bip001 R UpperArm", datapair "RightLowerArm" "Bip001 R Forearm", datapair "RightHand" "Bip001 R Hand", -- Right Hand Fingers datapair "RightThumbProximal" "Bip001 R Finger0", datapair "RightThumbIntermediate" "Bip001 R Finger01", datapair "RightThumbDistal" "Bip001 R Finger02", datapair "RightIndexProximal" "Bip001 R Finger1", datapair "RightIndexIntermediate" "Bip001 R Finger11", datapair "RightIndexDistal" "Bip001 R Finger12", datapair "RightMiddleProximal" "Bip001 R Finger2", datapair "RightMiddleIntermediate" "Bip001 R Finger21", datapair "RightMiddleDistal" "Bip001 R Finger22", datapair "RightRingProximal" "Bip001 R Finger3", datapair "RightRingIntermediate" "Bip001 R Finger31", datapair "RightRingDistal" "Bip001 R Finger32", datapair "RightLittleProximal" "Bip001 R Finger4", datapair "RightLittleIntermediate" "Bip001 R Finger41", datapair "RightLittleDistal" "Bip001 R Finger42", -- Left Arm datapair "LeftShoulder" "Bip001 L Clavicle", datapair "LeftUpperArm" "Bip001 L UpperArm", datapair "LeftLowerArm" "Bip001 L Forearm", datapair "LeftHand" "Bip001 L Hand", -- Left Hand Fingers datapair "LeftThumbProximal" "Bip001 L Finger0", datapair "LeftThumbIntermediate" "Bip001 L Finger01", datapair "LeftThumbDistal" "Bip001 L Finger02", datapair "LeftIndexProximal" "Bip001 L Finger1", datapair "LeftIndexIntermediate" "Bip001 L Finger11", datapair "LeftIndexDistal" "Bip001 L Finger12", datapair "LeftMiddleProximal" "Bip001 L Finger2", datapair "LeftMiddleIntermediate" "Bip001 L Finger21", datapair "LeftMiddleDistal" "Bip001 L Finger22", datapair "LeftRingProximal" "Bip001 L Finger3", datapair "LeftRingIntermediate" "Bip001 L Finger31", datapair "LeftRingDistal" "Bip001 L Finger32", datapair "LeftLittleProximal" "Bip001 L Finger4", datapair "LeftLittleIntermediate" "Bip001 L Finger41", datapair "LeftLittleDistal" "Bip001 L Finger42", -- Right Leg datapair "RightUpperLeg" "Bip001 R Thigh", datapair "RightLowerLeg" "Bip001 R Calf", datapair "RightFoot" "Bip001 R Foot", datapair "RightToes" "Bip001 R Toe0", -- Left Leg datapair "LeftUpperLeg" "Bip001 L Thigh", datapair "LeftLowerLeg" "Bip001 L Calf", datapair "LeftFoot" "Bip001 L Foot", datapair "LeftToes" "Bip001 L Toe0" ) -- 제외할 본들 (원본 방식) -- Unity Human Bone에서는 End 본들이 기본적으로 없으므로 excludeBones는 불필요 -- global excludeBones = #() global excludeBones =#("HeadTop_End") -- 스파인 세그먼트 개수 global spineSegments = 2 -- 특수 스켈레톤 구조 감지 변수들 global legParentIsSpine = false global shoulderParentIsNeck = false global shoulderParentIsSpine = false -- 발 각도 설정 (일반 바이패드 발 방향 사용으로 인해 비활성화) -- global footDegree = 50 -- global flipThreshold = 178 -- ================================== 최적화된 Transform 함수들 =============================================== -- 최적화된 Biped Transform 처리 (배치) fn optimizedBipedTransform bipNode pos:undefined rot:undefined scale:undefined =( /* 개별 Transform 호출을 한번에 처리 잠재적 충돌: Transform 순서 변경 시 IK 해결이 달라질 수 있음 안전장치: 각 컴포넌트를 검증 후 적용 */ if not OPTIMIZE_TRANSFORMS do ( -- 기존 방식 폴백 if pos != undefined do biped.setTransform bipNode #pos pos false if rot != undefined do biped.setTransform bipNode #rotation rot false if scale != undefined do biped.setTransform bipNode #scale scale false return true ) try ( local tm = bipNode.transform local modified = false if scale != undefined do ( tm.scaleFactors = scale modified = true ) if rot != undefined do ( tm.rotationPart = rot modified = true ) if pos != undefined do ( tm.translationPart = pos modified = true ) if modified do biped.setTransform bipNode #transform tm false true ) catch ( -- 실패 시 기존 방식으로 폴백 if VERBOSE_LOGGING do format "⚠️ Transform 최적화 실패, 기존 방식 사용: %\n" bipNode.name if pos != undefined do biped.setTransform bipNode #pos pos false if rot != undefined do biped.setTransform bipNode #rotation rot false if scale != undefined do biped.setTransform bipNode #scale scale false false ) ) -- 손가락 배치 처리 최적화 fn optimizedFingerRotations fingerNodes rotationData =( /* 모든 손가락 회전을 배치로 처리 잠재적 충돌: 손가락 순서나 개수가 다를 경우 안전장치: 각 손가락을 개별 검증 */ if not OPTIMIZE_FINGER_SETUP do return false local successCount = 0 for i = 1 to fingerNodes.count do ( if i <= rotationData.count and isValidNode fingerNodes[i] do ( try ( optimizedBipedTransform fingerNodes[i] rot:rotationData[i] successCount += 1 ) catch ( if VERBOSE_LOGGING do format "⚠️ 손가락 %번 회전 실패\n" i ) ) ) successCount > 0 ) -- Figure Mode 최적화 관리자 struct FigureModeManager ( bipCtl, originalMode, isActive = false, fn enter =( /* Figure Mode 진입 (한번만) 잠재적 충돌: 이미 Figure Mode인 경우 안전장치: 현재 상태 저장 및 복원 */ if not OPTIMIZE_FIGURE_MODE do return true try ( if not isActive do ( originalMode = bipCtl.figuremode bipCtl.figuremode = true isActive = true if VERBOSE_LOGGING do format "📐 Figure Mode 진입\n" ) true ) catch ( if VERBOSE_LOGGING do format "⚠️ Figure Mode 진입 실패\n" false ) ), fn exitMode =( /* Figure Mode 종료 잠재적 충돌: 중간에 다른 스크립트가 Mode 변경한 경우 안전장치: 원본 상태로만 복원 */ if not OPTIMIZE_FIGURE_MODE do return true try ( if isActive do ( bipCtl.figuremode = originalMode isActive = false if VERBOSE_LOGGING do format "📐 Figure Mode 종료\n" ) true ) catch ( if VERBOSE_LOGGING do format "⚠️ Figure Mode 종료 실패\n" false ) ) ) -- 슈퍼 UI 최적화 관리자 (더 강력한 억제) struct SuperUIOptimizer ( wasEditing = false, wasRedrawing = false, wasAnimating = false, wasSelectionEvents = false, isActive = false, fn suspend =( /* 모든 UI 이벤트 완전 차단 (최대 성능) */ if not OPTIMIZE_UI_UPDATES do return true try ( if not isActive do ( -- 기존 상태 저장 wasEditing = isSceneEditing() wasRedrawing = isSceneRedrawEnabled() wasAnimating = animButtonState -- 모든 업데이트 차단 suspendEditing() disableSceneRedraw() animButtonState = false -- 선택 이벤트도 차단 try ( callbacks.removeScripts id:#selectionChanged callbacks.removeScripts id:#nodeRenamed callbacks.removeScripts id:#nodeCreated ) catch () isActive = true if VERBOSE_LOGGING do format "⚡ 슈퍼 UI 차단 활성화\n" ) true ) catch ( if VERBOSE_LOGGING do format "⚠️ 슈퍼 UI 차단 실패\n" false ) ), fn resume =( /* 모든 UI 이벤트 복원 + 강제 정리 */ if not OPTIMIZE_UI_UPDATES do return true try ( if isActive do ( -- 상태 복원 if wasRedrawing do enableSceneRedraw() if wasEditing do resumeEditing() animButtonState = wasAnimating -- 강제 메모리 정리 gc light:true -- 한번만 redraw (여러 번 호출 방지) redrawViews() isActive = false if VERBOSE_LOGGING do format "⚡ 슈퍼 UI 차단 해제 + 정리 완료\n" ) true ) catch ( if VERBOSE_LOGGING do format "⚠️ 슈퍼 UI 복원 실패\n" false ) ) ) -- 메모리 및 성능 최적화 관리자 struct MemoryOptimizer ( fn deepCleanup =( /* 강력한 메모리 정리 + 3ds Max 성능 복원 */ try ( format "🧹 메모리 및 성능 정리 시작...\n" -- 1. 가비지 컬렉션 (단계적 정리) gc light:true gc() gc() -- 두 번 호출로 확실히 정리 -- 2. Undo 스택 정리 (메모리 해제) clearUndoBuffer() -- 3. Selection 정리 clearSelection() -- 4. 뷰포트 캐시 정리 try ( completeRedraw() forceCompleteRedraw() ) catch () -- 5. 임시 콜백 정리 (중복 방지) try ( callbacks.removeScripts id:#selectionChanged callbacks.removeScripts id:#nodeRenamed callbacks.removeScripts id:#nodeCreated callbacks.removeScripts id:#nodeDeleted ) catch () -- 6. 스킨 캐시 정리 (매우 중요!) try ( for obj in objects do ( if obj.modifiers[#Skin] != undefined do ( obj.modifiers[#Skin].always_deform = obj.modifiers[#Skin].always_deform ) ) ) catch () -- 7. 뷰포트 정리 try ( actionMan.executeAction 0 "40021" -- Zoom Extents All max views redraw ) catch () format "✅ 메모리 정리 완료\n" true ) catch ( format "⚠️ 메모리 정리 부분 실패: %\n" (getCurrentException()) false ) ), fn quickCleanup =( /* 빠른 정리 (중간중간 호출) */ try ( gc light:true clearSelection() true ) catch (false) ) ) -- 진행률 표시 관리자 (안전 버전) struct ProgressManager ( totalSteps = 0, currentStep = 0, lastUpdateTime = 0, fn init steps =( totalSteps = if steps > 0 then steps else 1 currentStep = 0 lastUpdateTime = timeStamp() if ENABLE_PROGRESS_BAR do ( try ( format "📊 진행률: 0/% (0%%)\n" totalSteps ) catch ( format "📊 진행률 시작\n" ) ) ), fn update msg =( try ( currentStep += 1 local now = timeStamp() -- 완전히 안전한 진행률 표시 if ENABLE_PROGRESS_BAR do ( local percent = if totalSteps > 0 then (currentStep as float / totalSteps * 100) else 0 local percentStr = (percent as integer) as string + "%" if msg != undefined and msg != "" then format "📊 %/% (%) - %\n" currentStep totalSteps percentStr msg else format "📊 %/% (%)\n" currentStep totalSteps percentStr ) ) catch ( -- 진행률 오류 무시 if VERBOSE_LOGGING do format "진행률 업데이트 오류 무시\n" ) ) ) -- ================================== 유틸리티 함수들 (원본 그대로) =============================================== fn pointLineProj pA pB pC = ( local vAB=pB-pA local vAC=pC-pA local d=dot (normalize vAB) (normalize vAC) (pA+(vAB*(d*(length vAC/length vAB)))) ) -- 발 각도 계산 함수 (일반 바이패드 발 방향 사용으로 인해 비활성화) -- fn findFootAngle =( -- local foots = $'*Foot' as array -- if foots.count==0 do return 35 -- -- local posArr = for i in foots collect i.pos[3] -- local idx = findItem posArr (amin(posArr)) -- local foot = foots[idx] -- local toe = foot.children[1] -- if not isvalidnode toe do return 35 -- -- local p1 = foot.pos -- local p2 = toe.pos -- local v1 = p1-p2 -- p2[3] = p1[3] -- local v2 = p1-p2 -- acos(dot(normalize v1) (normalize v2)) -- ) fn getHierarchy objArr out= ( for i in objArr do ( appendifunique out i child = i.children if child.count>0 do ( getHierarchy child out ) ) out ) fn getSkinnedObj = ( skn = getClassInstances skin objArr = #() for i in skn do( for obj in refs.dependentNodes i do appendIfunique objArr obj ) objArr ) -- 위치 기반으로 실제 본 체인인지 확인하는 함수 fn isRealBoneChain parentBone childBone tolerance:0.1 =( if not isValidNode parentBone or not isValidNode childBone then return false -- 부모 본의 끝점 계산 (자식이 있으면 자식 방향, 없으면 부모 방향의 반대) local parentEnd = parentBone.pos if parentBone.children.count > 0 then ( -- 가장 가까운 자식 본 방향으로 본 길이 계산 local closestChild = parentBone.children[1] local minDist = distance parentBone.pos closestChild.pos for child in parentBone.children do ( local dist = distance parentBone.pos child.pos if dist < minDist do ( minDist = dist closestChild = child ) ) parentEnd = closestChild.pos ) else if parentBone.parent != undefined then ( -- 부모가 있으면 부모->현재 방향으로 연장 local direction = normalize (parentBone.pos - parentBone.parent.pos) local boneLength = distance parentBone.parent.pos parentBone.pos parentEnd = parentBone.pos + direction * boneLength ) -- 자식 본의 시작점과 부모 본의 끝점 거리 확인 local dist = distance parentEnd childBone.pos return dist <= tolerance ) fn ScanBranchHRC obj out:#() = ( local child = obj.children if child.count > 0 then ( -- 실제 본 체인으로 연결된 자식 찾기 (위치 기반) local chainChild = undefined for c in child do ( if isRealBoneChain obj c then ( chainChild = c exit -- 첫 번째로 체인 연결된 자식만 사용 ) ) if chainChild != undefined then ( -- 체인으로 연결된 자식이 있으면 계속 추적 append out chainChild ScanBranchHRC chainChild out:out ) else ( -- 체인이 끊어지면 undefined 추가 append out undefined ) -- 체인에 포함되지 않은 나머지 자식들은 별도 처리 for i in child where i != chainChild do ( append out i ScanBranchHRC i out:out ) ) else ( append out undefined ) out ) fn getBranchHRC obj =( local scan = ScanBranchHRC obj local out = #() local new = undefined for i in scan do ( if new == undefined do new = #() if isvalidNode i then ( append new i ) else ( if new != undefined and new.count > 0 do append out new new = undefined ) ) -- 마지막 그룹 추가 if new != undefined and new.count > 0 do append out new out ) fn collectLayerNodes ly out:#()=( ly.nodes &nd join out nd local numchildLy =ly.getNumChildren() if numchildLy>0 do( for i=1 to numchildLy do( local newLy = ly.getChild i collectLayerNodes newLy out:out ) ) out ) fn CleanNames =( -- Unity Human Bone 객체들을 올바르게 수집 local objArr = #() -- 각 패턴별로 객체들을 찾아서 배열에 추가 local hipsObjs = $Hips* as array local leftArmObjs = $LeftUpperArm* as array local rightArmObjs = $RightUpperArm* as array local spineObjs = $Spine* as array -- 모든 객체들을 하나의 배열로 합치기 join objArr hipsObjs join objArr leftArmObjs join objArr rightArmObjs join objArr spineObjs format "CleanNames: %개 객체 처리 시작\\n" objArr.count --Rules 1: 콜론으로 분리된 이름에서 접두사 제거 for i in objArr do( n = i.name filt = filterstring n ":" if filt.Count>1 do( newName = "" for j=2 to filt.Count do ( newName += filt[j] if j!=filt.Count do newName+=":" ) i.name = newName format "이름 정리: % → %\\n" n newName ) ) format "CleanNames 완료\\n" ) fn SanityCheck = ( -- Unity Human Bone 표준 이름 검증 hips = getNodeByName "Hips" if isValidNode hips then ( -- Unity Human Bone 오브젝트들이 있는지 확인 local unityBones = #("Hips", "Spine", "Chest", "Neck", "Head", "LeftUpperArm", "RightUpperArm", "LeftUpperLeg", "RightUpperLeg") local foundBones = 0 for boneName in unityBones do ( local bone = getNodeByName boneName if isValidNode bone do foundBones += 1 ) -- 최소 5개 이상의 주요 본이 있으면 유효한 것으로 간주 return (foundBones >= 5) ) else return false ) fn createBip height spine:2 fingers:5= ( -- Triangle Pelvis는 False, Triangle Neck는 True로 설정 local bipObj = biped.createNew height -90 [0,0,height*0.9562] arms:true neckLinks:1 \ spineLinks:spine legLinks:3 \ fingers:fingers fingerLinks:3 toes:1 \ toeLinks:1 ankleAttach:0.3 trianglePelvis:False triangleNeck:True bipObj ) fn getRigStructures =( local n = "" local root = execute ("$'"+n+"Hips'") -- 다리 본들 (부모 관계 확인) local LLeg1 = execute ("$'"+n+"LeftUpperLeg'") local LLeg2 = execute ("$'"+n+"LeftLowerLeg'") local LFoot = execute ("$'"+n+"LeftFoot'") local LToe = execute ("$'"+n+"LeftToes'") local RLeg1 = execute ("$'"+n+"RightUpperLeg'") local RLeg2 = execute ("$'"+n+"RightLowerLeg'") local RFoot = execute ("$'"+n+"RightFoot'") local RToe = execute ("$'"+n+"RightToes'") -- 스파인 본들 local LSpine = execute ("$'"+n+"Spine'") local LSpine1 = execute ("$'"+n+"Chest'") local spines = #(LSpine, LSpine1) spineSegments = 2 -- 다리가 Spine에서 시작하는 경우 처리 -- 다리 본들의 부모가 Spine인지 확인 legParentIsSpine = false if LLeg1 != undefined and LLeg1.parent != undefined then ( if findString LLeg1.parent.name "Spine" != undefined then ( legParentIsSpine = true format "다리가 Spine에서 시작됨을 감지\\n" ) ) -- 어깨/팔 본들 local LClav = execute ("$'"+n+"LeftShoulder'") local LArm1 = execute ("$'"+n+"LeftUpperArm'") local LArm2 = execute ("$'"+n+"LeftLowerArm'") local LHand = execute ("$'"+n+"LeftHand'") local RClav = execute ("$'"+n+"RightShoulder'") local RArm1 = execute ("$'"+n+"RightUpperArm'") local RArm2 = execute ("$'"+n+"RightLowerArm'") local RHand = execute ("$'"+n+"RightHand'") -- 🔍 팔 구조 디버그 출력 format "\\n=== 팔 구조 디버그 ===\\n" if RClav != undefined then ( format "RightShoulder: %\\n" RClav.name format " 자식들: " for child in RClav.children do format "%, " child.name format "\\n" ) if RArm1 != undefined then ( format "RightArm: %\\n" RArm1.name format " 자식들: " for child in RArm1.children do format "%, " child.name format "\\n" ) if RArm2 != undefined then ( format "RightForeArm: %\\n" RArm2.name format " 자식들: " for child in RArm2.children do format "%, " child.name format "\\n" ) format "========================\\n" local neck = execute ("$'"+n+"Neck'") local head = execute ("$'"+n+"Head'") -- 어깨가 어디서 시작하는지 확인 shoulderParentIsNeck = false shoulderParentIsSpine = false if LClav != undefined and LClav.parent != undefined then ( if findString LClav.parent.name "Neck" != undefined then ( shoulderParentIsNeck = true format "어깨가 Neck에서 시작됨을 감지\\n" ) else if findString LClav.parent.name "Spine" != undefined and findString LClav.parent.name "Spine1" == undefined then ( shoulderParentIsSpine = true format "어깨가 Spine에서 바로 시작됨을 감지\\n" ) ) local LFinger1 = $'LeftThumbProximal' local LFinger2 = $'LeftIndexProximal' local LFinger3 = $'LeftMiddleProximal' local LFinger4 = $'LeftRingProximal' local LFinger5 = $'LeftLittleProximal' local RFinger1 = $'RightThumbProximal' local RFinger2 = $'RightIndexProximal' local RFinger3 = $'RightMiddleProximal' local RFinger4 = $'RightRingProximal' local RFinger5 = $'RightLittleProximal' -- 스켈레톤 구조에 따라 적응적으로 배치 if (legParentIsSpine and shoulderParentIsNeck) or (shoulderParentIsSpine) then ( -- 특수 스켈레톤 구조들 if shoulderParentIsSpine then ( format "특수 스켈레톤 구조 감지: 어깨->Spine\\n" ) else ( format "특수 스켈레톤 구조 감지: 다리->Spine, 어깨->Neck\\n" ) rigNodes = #(root,root) join rigNodes #(LLeg1, LLeg2, LFoot, LToe, RLeg1, RLeg2, RFoot, RToe) -- 다리들 먼저 join rigNodes Spines -- 스파인들 join rigNodes #(neck, head) -- 목과 머리 join rigNodes #(LClav, LArm1, LArm2, LHand, RClav, RArm1, RArm2, RHand) -- 어깨와 팔들 join rigNodes #(LFinger1,LFinger2,LFinger3,LFinger4,LFinger5, RFinger1,RFinger2,RFinger3,RFinger4,RFinger5) ) else ( -- 표준 구조 rigNodes = #(root,root) join rigNodes Spines join rigNodes #( LClav, LArm1, LArm2, LHand, RClav, RArm1, RArm2, RHand, neck, head, LLeg1, LLeg2, LFoot, LToe, RLeg1, RLeg2, RFoot, RToe, LFinger1,LFinger2,LFinger3,LFinger4,LFinger5, RFinger1,RFinger2,RFinger3,RFinger4,RFinger5 ) ) rigNodes ) fn GetBipStructure bip spinesegs:3= ( local bip_pelvis = biped.getNode bip #pelvis link:1 local bip_root = bip_pelvis.parent local bip_LLeg1 = biped.getNode bip #lleg link:1 local bip_LLeg2 = biped.getNode bip #lleg link:2 local bip_LFoot = biped.getNode bip #lleg link:3 local bip_LToe = biped.getNode bip #ltoes link:1 local bip_RLeg1 = biped.getNode bip #rleg link:1 local bip_RLeg2 = biped.getNode bip #rleg link:2 local bip_RFoot = biped.getNode bip #rleg link:3 local bip_RToe = biped.getNode bip #rtoes link:1 local bip_LClav = biped.getNode bip #larm link:1 local bip_LArm1 = biped.getNode bip #larm link:2 local bip_LArm2 = biped.getNode bip #larm link:3 local bip_LHand = biped.getNode bip #larm link:4 local bip_RClav = biped.getNode bip #rarm link:1 local bip_RArm1 = biped.getNode bip #rarm link:2 local bip_RArm2 = biped.getNode bip #rarm link:3 local bip_RHand = biped.getNode bip #rarm link:4 local bip_neck = biped.getNode bip #neck link:1 local bip_head = biped.getNode bip #head link:1 local bip_Lfinger0 = biped.getNode bip #lfingers link:1 local bip_Lfinger1 = biped.getNode bip #lfingers link:4 local bip_Lfinger2 = biped.getNode bip #lfingers link:7 local bip_Lfinger3 = biped.getNode bip #lfingers link:10 local bip_Lfinger4 = biped.getNode bip #lfingers link:13 local bip_Rfinger0 = biped.getNode bip #rfingers link:1 local bip_Rfinger1 = biped.getNode bip #rfingers link:4 local bip_Rfinger2 = biped.getNode bip #rfingers link:7 local bip_Rfinger3 = biped.getNode bip #rfingers link:10 local bip_Rfinger4 = biped.getNode bip #rfingers link:13 -- 스켈레톤 구조에 따라 적응적으로 배치 (getRigStructures와 동일 순서) if (legParentIsSpine and shoulderParentIsNeck) or (shoulderParentIsSpine) then ( -- 특수 구조들 format "Biped도 특수 구조로 배치\\n" bipNodes = #(bip_root,bip_pelvis) -- 다리들 먼저 join bipNodes #(bip_LLeg1, bip_LLeg2, bip_LFoot, bip_LToe, bip_RLeg1, bip_RLeg2, bip_RFoot, bip_RToe) -- 스파인들 for i=1 to spinesegs do append bipNodes (biped.getNode bip #spine link:i) -- 목과 머리 join bipNodes #(bip_neck, bip_head) -- 어깨와 팔들 join bipNodes #(bip_LClav, bip_LArm1, bip_LArm2, bip_LHand, bip_RClav, bip_RArm1, bip_RArm2, bip_RHand) -- 손가락들 join bipNodes #(bip_Lfinger0,bip_Lfinger1,bip_Lfinger2,bip_Lfinger3,bip_Lfinger4, bip_Rfinger0,bip_Rfinger1,bip_Rfinger2,bip_Rfinger3,bip_Rfinger4) ) else ( -- 표준 구조 bipNodes = #(bip_root,bip_pelvis) --SPINES for i=1 to spinesegs do append bipNodes (biped.getNode bip #spine link:i) join bipNodes #(bip_LClav, bip_LArm1, bip_LArm2, bip_LHand, bip_RClav, bip_RArm1, bip_RArm2, bip_RHand, bip_neck, bip_head, bip_LLeg1, bip_LLeg2, bip_LFoot, bip_LToe, bip_RLeg1, bip_RLeg2, bip_RFoot, bip_RToe, bip_Lfinger0,bip_Lfinger1,bip_Lfinger2,bip_Lfinger3,bip_Lfinger4, bip_Rfinger0,bip_Rfinger1,bip_Rfinger2,bip_Rfinger3,bip_Rfinger4 ) ) bipNodes ) fn MapTMAxis sourceTM targetTM indexes mult:[1,1,1]=( axis = #(sourceTM.row1,sourceTM.row2,sourceTM.row3) targetTM.row1 = axis[indexes[1]]*mult[1] targetTM.row2 = axis[indexes[2]]*mult[2] targetTM.row3 = axis[indexes[3]]*mult[3] targetTM ) fn SetClavicleTM bipCtl bipClav TM =( local figMode = bipCtl.figuremode bipCtl.figuremode = false -- FORCE EXIT FIGURE MODE biped.setTransform bipClav #rotation TM.rotationPart false -- COPY POSTURE select bipClav local col = biped.createCopyCollection bipCtl "Shoulder" local cpy = biped.copyBipPosture bipCtl col (selection as array) #snapAuto -- PASTE POSTURE ON FIGURE MODE bipCtl.figuremode = true biped.pasteBipPosture bipCtl cpy false #pstcopied false false false false biped.deleteAllCopyCollections bipCtl biped.setTransform bipClav #pos TM.row4 false clearSelection() bipCtl.figuremode = figMode ) fn alignFootTpose rigObj BipObj =( local RigTM = rigObj.transform -- 이름으로 Toe 본 찾기 local toe = undefined if findString rigObj.name "LeftFoot" != undefined then ( toe = execute ("$'LeftToes'") ) else if findString rigObj.name "RightFoot" != undefined then ( toe = execute ("$'RightToes'") ) -- 백업: 이름으로 못 찾으면 children[1] 사용 if toe == undefined do toe = rigObj.children[1] local endPoint = toe.pos local height = abs (endPoint[3]-RigTM.row4[3]) endPoint[3] = RigTM.row4[3] local v = endPoint-RigTM.row4 local len = length v local y = normalize(v) local x = [0,0,-1] local z = normalize(cross x y) local TM = Matrix3 x y z RigTM.row4 -- #SET ROTATION biped.setTransform BipObj #rotation TM.rotationpart false biped.setTransform bipObj #scale [height,len*1.25,len*0.6] false biped.setTransform BipObj #pos RigTM.row4 false ) fn alignFoot rigObj BipObj figureMode:true =( local isSetKey = not figureMode local RigTM = rigObj.transform -- 일반 바이패드 발 방향 사용 (복잡한 각도 계산 제거) -- 위치만 설정하고 회전은 바이패드 기본값 유지 biped.setTransform BipObj #pos RigTM.row4 isSetKey -- SET SCALE (ONLY IN FIGURE MODE) if figureMode do( -- 발 크기 계산 (단순화) local toe = undefined if findString rigObj.name "LeftFoot" != undefined then ( toe = execute ("$'LeftToes'") ) else if findString rigObj.name "RightFoot" != undefined then ( toe = execute ("$'RightToes'") ) -- 백업: 이름으로 못 찾으면 children[1] 사용 if toe == undefined do toe = rigObj.children[1] if isValidNode toe then ( local footLength = distance rigObj.pos toe.pos local footHeight = abs(rigObj.pos[3] - toe.pos[3]) -- 바이패드 발 크기 설정 (비례 유지) biped.setTransform bipObj #scale [footHeight, footLength, footLength*0.6] isSetKey ) else ( -- 기본 크기 설정 biped.setTransform bipObj #scale [1, 1, 0.6] isSetKey ) ) ) fn AlignArm BipUpperArm MxUpperArm side:"R" FigureMode:false TPose:false = ( local createKey = not FigureMode -- Biped 본들을 이름으로 찾기 local BipLowerArm = undefined local Biphand = undefined if side == "R" then ( BipLowerArm = execute ("$'Bip001 R Forearm'") Biphand = execute ("$'Bip001 R Hand'") ) else ( BipLowerArm = execute ("$'Bip001 L Forearm'") Biphand = execute ("$'Bip001 L Hand'") ) -- 백업: 이름으로 못 찾으면 children[1] 사용 if BipLowerArm == undefined do BipLowerArm = BipUpperArm.children[1] if Biphand == undefined do Biphand = BipLowerArm.children[1] local Bip_UpperTM = BipUpperArm.transform local Bip_LowerTM = BipLowerArm.transform local Bip_HandTM = Biphand.transform -- 정확한 본 이름으로 찾기 (children[1] 대신) local MxForeArm = undefined local MxHand = undefined if side == "R" then ( MxForeArm = execute ("$'RightLowerArm'") MxHand = execute ("$'RightHand'") ) else ( MxForeArm = execute ("$'LeftLowerArm'") MxHand = execute ("$'LeftHand'") ) -- 백업: 이름으로 못 찾으면 children[1] 사용 if MxForeArm == undefined do MxForeArm = MxUpperArm.children[1] if MxHand == undefined do MxHand = MxForeArm.children[1] local MX_UpperTM = MxUpperArm.transform local MX_ElbowTM = MxForeArm.transform local MX_HandTM = MxHand.transform -- SET SCALE if FigureMode do( len = distance MxUpperArm MxForeArm biped.setTransform BipUpperArm #scale [len,len,len] createKey len = distance MxForeArm MxHand biped.setTransform BipLowerArm #scale [len,len,len] createKey -- 손 크기 계산 (기본값 사용) len *= 0.2 biped.setTransform Biphand #scale [len,len,len] createKey ) local mid = (MX_HandTM.row4+MX_UpperTM.row4)*0.5 local up = Normalize (mid-MX_ElbowTM.row4) if TPose do up = [0,-1,0] --SET HAND FIRST BECAUSE THE IK biped.setTransform Biphand #pos MX_HandTM.row4 createKey -- UpperArm local x = Normalize (MX_ElbowTM.row4-Bip_UpperTM.row4) local z = Normalize (cross up x ) local y = Normalize (cross z x) local upperTM = matrix3 x y z MX_UpperTM.row4 -- UpperArm: 기하학적 계산은 유지, 축 변환만 제거 biped.setTransform BipUpperArm #rotation upperTM.rotationpart createKey -- Hand: 위치만 설정, 축 변환 제거 biped.setTransform Biphand #pos MX_HandTM.row4 createKey ) fn AlignLeg BipUpperLeg MxUpperLeg side:"R" FigureMode:false TPose:false = ( local createKey = not FigureMode -- Biped 본들을 이름으로 찾기 local BipLoweLeg = undefined local BipFoot = undefined if side == "R" then ( BipLoweLeg = execute ("$'Bip001 R Calf'") BipFoot = execute ("$'Bip001 R Foot'") ) else ( BipLoweLeg = execute ("$'Bip001 L Calf'") BipFoot = execute ("$'Bip001 L Foot'") ) -- 백업: 이름으로 못 찾으면 children[1] 사용 if BipLoweLeg == undefined do BipLoweLeg = BipUpperLeg.children[1] if BipFoot == undefined do BipFoot = BipLoweLeg.children[1] -- Unity Human Bone들을 이름으로 찾기 local MxLowerLeg = undefined local MxFoot = undefined if side == "R" then ( MxLowerLeg = execute ("$'RightLowerLeg'") MxFoot = execute ("$'RightFoot'") ) else ( MxLowerLeg = execute ("$'LeftLowerLeg'") MxFoot = execute ("$'LeftFoot'") ) -- 백업: 이름으로 못 찾으면 children[1] 사용 if MxLowerLeg == undefined do MxLowerLeg = MxUpperLeg.children[1] if MxFoot == undefined do MxFoot = MxLowerLeg.children[1] local rigTM = MxUpperLeg.transform local BipUpperTM = BipUpperLeg.transform local KneeTM = MxLowerLeg.transform local endTM = MxFoot.transform if FigureMode do( dist = distance MxUpperLeg MxLowerLeg biped.setTransform BipUpperLeg #scale [dist,dist,dist] createKey dist = distance MxLowerLeg MxFoot biped.setTransform BipLoweLeg #scale [dist,dist,dist] createKey ) local mid = (endTM.row4+rigTM.row4)*0.5 local up = Normalize (mid-KneeTM.row4) if TPose do up = [0,1,0] -- Foot biped.setTransform BipFoot #pos endTM.row4 createKey -- UpperLeg local x = Normalize (KneeTM.row4-BipUpperTM.row4) local z = Normalize (cross up x ) local y = Normalize (cross z x) local newTM = matrix3 x y z rigTM.row4 -- UpperLeg: 기하학적 계산은 유지, 축 변환만 제거 biped.setTransform BipUpperLeg #rotation newTM.rotationpart createKey ) fn alignFingers BipNode MXNode figureMode:false=( /* 손가락 정렬 함수 - 최적화 버전 잠재적 충돌: 손가락 구조가 다르거나 개수가 다른 경우 안전장치: 각 관절별 유효성 검사 및 폴백 */ -- 빠른 종료 조건 if not figureMode do return true if not isValidNode BipNode or not isValidNode MXNode do return false -- Biped 손가락 본들을 이름으로 찾기 (첫 번째 관절만) local bip2 = undefined local bip3 = undefined -- Biped 손가락 이름 패턴에 따라 찾기 if findString BipNode.name "Finger0" != undefined then ( -- 엄지손가락 if findString BipNode.name " R " != undefined then ( bip2 = execute ("$'Bip001 R Finger01'") bip3 = execute ("$'Bip001 R Finger02'") ) else ( bip2 = execute ("$'Bip001 L Finger01'") bip3 = execute ("$'Bip001 L Finger02'") ) ) else if findString BipNode.name "Finger1" != undefined then ( -- 검지손가락 if findString BipNode.name " R " != undefined then ( bip2 = execute ("$'Bip001 R Finger11'") bip3 = execute ("$'Bip001 R Finger12'") ) else ( bip2 = execute ("$'Bip001 L Finger11'") bip3 = execute ("$'Bip001 L Finger12'") ) ) else if findString BipNode.name "Finger2" != undefined then ( -- 중지손가락 if findString BipNode.name " R " != undefined then ( bip2 = execute ("$'Bip001 R Finger21'") bip3 = execute ("$'Bip001 R Finger22'") ) else ( bip2 = execute ("$'Bip001 L Finger21'") bip3 = execute ("$'Bip001 L Finger22'") ) ) else if findString BipNode.name "Finger3" != undefined then ( -- 약지손가락 if findString BipNode.name " R " != undefined then ( bip2 = execute ("$'Bip001 R Finger31'") bip3 = execute ("$'Bip001 R Finger32'") ) else ( bip2 = execute ("$'Bip001 L Finger31'") bip3 = execute ("$'Bip001 L Finger32'") ) ) else if findString BipNode.name "Finger4" != undefined then ( -- 새끼손가락 if findString BipNode.name " R " != undefined then ( bip2 = execute ("$'Bip001 R Finger41'") bip3 = execute ("$'Bip001 R Finger42'") ) else ( bip2 = execute ("$'Bip001 L Finger41'") bip3 = execute ("$'Bip001 L Finger42'") ) ) -- 백업: 이름으로 못 찾으면 children[1] 사용 if bip2 == undefined and BipNode.children.count > 0 do bip2 = BipNode.children[1] if bip3 == undefined and bip2 != undefined and bip2.children.count > 0 do bip3 = bip2.children[1] local BipNodes = #(BipNode,bip2,bip3) -- Unity Human Bone 손가락 본들을 이름으로 찾기 local MX2 = undefined local MX3 = undefined local MX4 = undefined -- Unity Human Bone 손가락 이름 패턴에 따라 찾기 if findString MXNode.name "Thumb" != undefined then ( -- 엄지손가락 if findString MXNode.name "Right" != undefined then ( MX2 = execute ("$'RightThumbIntermediate'") MX3 = execute ("$'RightThumbDistal'") ) else ( MX2 = execute ("$'LeftThumbIntermediate'") MX3 = execute ("$'LeftThumbDistal'") ) ) else if findString MXNode.name "Index" != undefined then ( -- 검지손가락 if findString MXNode.name "Right" != undefined then ( MX2 = execute ("$'RightIndexIntermediate'") MX3 = execute ("$'RightIndexDistal'") ) else ( MX2 = execute ("$'LeftIndexIntermediate'") MX3 = execute ("$'LeftIndexDistal'") ) ) else if findString MXNode.name "Middle" != undefined then ( -- 중지손가락 if findString MXNode.name "Right" != undefined then ( MX2 = execute ("$'RightMiddleIntermediate'") MX3 = execute ("$'RightMiddleDistal'") ) else ( MX2 = execute ("$'LeftMiddleIntermediate'") MX3 = execute ("$'LeftMiddleDistal'") ) ) else if findString MXNode.name "Ring" != undefined then ( -- 약지손가락 if findString MXNode.name "Right" != undefined then ( MX2 = execute ("$'RightRingIntermediate'") MX3 = execute ("$'RightRingDistal'") ) else ( MX2 = execute ("$'LeftRingIntermediate'") MX3 = execute ("$'LeftRingDistal'") ) ) else if findString MXNode.name "Pinky" != undefined then ( -- 새끼손가락 if findString MXNode.name "Right" != undefined then ( MX2 = execute ("$'RightLittleIntermediate'") MX3 = execute ("$'RightLittleDistal'") ) else ( MX2 = execute ("$'LeftLittleIntermediate'") MX3 = execute ("$'LeftLittleDistal'") ) ) -- 백업: 이름으로 못 찾으면 children[1] 사용 if MX2 == undefined and MXNode.children.count > 0 do MX2 = MXNode.children[1] if MX3 == undefined and MX2 != undefined and MX2.children.count > 0 do MX3 = MX2.children[1] if MX4 == undefined and MX3 != undefined and MX3.children.count > 0 do MX4 = MX3.children[1] local MXNodes = #(MXNode,MX2,MX3,MX4) -- 크기 및 위치 조정 (최적화된 Transform 사용) for i=1 to 3 where isvalidNode MXNodes[i] and isvalidNode BipNodes[i] do( local len = 1.0 if isValidNode MXNodes[i+1] then len = distance MXNodes[i] MXNodes[i+1] else if MXNodes[i].parent != undefined then len = distance MXNodes[i] MXNodes[i].parent local pos = if i==1 then MXNodes[i].transform.row4 else undefined local scale = [len,len,len] optimizedBipedTransform BipNodes[i] pos:pos scale:scale ) -- 손가락 펴기: 최적화된 배치 처리 if OPTIMIZE_FINGER_SETUP then ( try ( local isThumb = (findString BipNode.name "Finger0" != undefined) local isLeftHand = (findString BipNode.name " L " != undefined) or (findString BipNode.name "Left" != undefined) local rotations = #() -- 회전 데이터 준비 (미리 계산) case of ( -- === 왼손 엄지 === (isThumb and isLeftHand): ( rotations = #( (eulerAngles -90 10 -30), (eulerAngles -90 10 -30), (eulerAngles -90 10 -30) ) ) -- === 오른손 엄지 === (isThumb and not isLeftHand): ( rotations = #( (eulerAngles -90 10 -150), (eulerAngles -90 10 -150), (eulerAngles -90 10 -150) ) ) -- === 왼손 다른 손가락들 === (not isThumb and isLeftHand): ( rotations = #( (eulerAngles -90 0 0), (eulerAngles -100 0 0), (eulerAngles -105 0 0) ) ) -- === 오른손 다른 손가락들 === (not isThumb and not isLeftHand): ( rotations = #( (eulerAngles -90 0 180), (eulerAngles -100 0 180), (eulerAngles -105 0 180) ) ) ) -- 배치 회전 적용 if rotations.count > 0 do optimizedFingerRotations BipNodes rotations ) catch ( -- 실패 시 기존 방식으로 폴백 if VERBOSE_LOGGING do format "⚠️ 손가락 최적화 실패, 기존 방식 사용: %\n" BipNode.name local isThumb = (findString BipNode.name "Finger0" != undefined) local isLeftHand = (findString BipNode.name " L " != undefined) or (findString BipNode.name "Left" != undefined) -- === 기존 방식 폴백 === if isThumb and isLeftHand then ( if isvalidNode BipNodes[1] do biped.setTransform BipNodes[1] #rotation (eulerAngles -90 10 -30) false if isvalidNode BipNodes[2] do biped.setTransform BipNodes[2] #rotation (eulerAngles -90 10 -30) false if isvalidNode BipNodes[3] do biped.setTransform BipNodes[3] #rotation (eulerAngles -90 10 -30) false ) else if isThumb and not isLeftHand then ( if isvalidNode BipNodes[1] do biped.setTransform BipNodes[1] #rotation (eulerAngles -90 10 -150) false if isvalidNode BipNodes[2] do biped.setTransform BipNodes[2] #rotation (eulerAngles -90 10 -150) false if isvalidNode BipNodes[3] do biped.setTransform BipNodes[3] #rotation (eulerAngles -90 10 -150) false ) else if not isThumb and isLeftHand then ( if isvalidNode BipNodes[1] do biped.setTransform BipNodes[1] #rotation (eulerAngles -90 0 0) false if isvalidNode BipNodes[2] do biped.setTransform BipNodes[2] #rotation (eulerAngles -100 0 0) false if isvalidNode BipNodes[3] do biped.setTransform BipNodes[3] #rotation (eulerAngles -105 0 0) false ) else if not isThumb and not isLeftHand then ( if isvalidNode BipNodes[1] do biped.setTransform BipNodes[1] #rotation (eulerAngles -90 0 180) false if isvalidNode BipNodes[2] do biped.setTransform BipNodes[2] #rotation (eulerAngles -100 0 180) false if isvalidNode BipNodes[3] do biped.setTransform BipNodes[3] #rotation (eulerAngles -105 0 180) false ) ) ) else ( -- 최적화 비활성화 시 기존 방식 사용 local isThumb = (findString BipNode.name "Finger0" != undefined) local isLeftHand = (findString BipNode.name " L " != undefined) or (findString BipNode.name "Left" != undefined) if isThumb and isLeftHand then ( if isvalidNode BipNodes[1] do biped.setTransform BipNodes[1] #rotation (eulerAngles -90 10 -30) false if isvalidNode BipNodes[2] do biped.setTransform BipNodes[2] #rotation (eulerAngles -90 10 -30) false if isvalidNode BipNodes[3] do biped.setTransform BipNodes[3] #rotation (eulerAngles -90 10 -30) false ) else if isThumb and not isLeftHand then ( if isvalidNode BipNodes[1] do biped.setTransform BipNodes[1] #rotation (eulerAngles -90 10 -150) false if isvalidNode BipNodes[2] do biped.setTransform BipNodes[2] #rotation (eulerAngles -90 10 -150) false if isvalidNode BipNodes[3] do biped.setTransform BipNodes[3] #rotation (eulerAngles -90 10 -150) false ) else if not isThumb and isLeftHand then ( if isvalidNode BipNodes[1] do biped.setTransform BipNodes[1] #rotation (eulerAngles -90 0 0) false if isvalidNode BipNodes[2] do biped.setTransform BipNodes[2] #rotation (eulerAngles -100 0 0) false if isvalidNode BipNodes[3] do biped.setTransform BipNodes[3] #rotation (eulerAngles -105 0 0) false ) else if not isThumb and not isLeftHand then ( if isvalidNode BipNodes[1] do biped.setTransform BipNodes[1] #rotation (eulerAngles -90 0 180) false if isvalidNode BipNodes[2] do biped.setTransform BipNodes[2] #rotation (eulerAngles -100 0 180) false if isvalidNode BipNodes[3] do biped.setTransform BipNodes[3] #rotation (eulerAngles -105 0 180) false ) ) true ) fn BipToRigProportion bipNodes rigNodes TPose:false =( /* Biped 비율 매칭 함수 - 최적화 버전 잠재적 충돌: Transform 순서 변경, Figure Mode 상태 안전장치: Figure Mode 관리자, 배치 처리, 진행률 표시 */ stepPerf "비율 매칭 시작" if bipNodes.count == 0 do ( format "❌ Biped 노드가 없음!\n" return false ) local bipctl = bipNodes[1].transform.controller if bipctl == undefined do ( format "❌ Biped 컨트롤러를 찾을 수 없음!\n" return false ) local figManager = FigureModeManager bipCtl:bipctl local progress = ProgressManager() -- 진행률 초기화 progress.init bipNodes.count -- Figure Mode 안전하게 진입 format "🔧 Figure Mode 진입 시도...\n" if not figManager.enter() do ( format "❌ Figure Mode 진입 실패!\n" format " - Biped Controller: %\n" bipctl format " - Figure Mode 상태: %\n" (try (bipctl.figuremode) catch ("접근 불가")) return false ) format "✅ Figure Mode 진입 성공\n" try ( local matchIndexes = #{1..(bipNodes.count)} format "\\n=== 비율 매칭 디버그 ===\\n" -- Unity Human Bone 참조 본들 확인 local Lthigh = getNodeByName "LeftUpperLeg" local Rthigh = getNodeByName "RightUpperLeg" if not isValidNode Lthigh do ( format "❌ LeftUpperLeg를 찾을 수 없음\\n" figManager.exit() return false ) if not isValidNode Rthigh do ( format "❌ RightUpperLeg를 찾을 수 없음\\n" figManager.exit() return false ) local p = (Lthigh.pos + Rthigh.pos) * 0.5 format "기준 위치 계산: %\\n" p for i in matchIndexes where isvalidnode bipNodes[i] do( n = bipNodes[i].name bipNode = bipNodes[i] rigNode = rigNodes[i] if isvalidNode bipNode and isvalidNode rigNode do( rigNodeEnd = rigNodes[i+1] rigTM = rigNode.transform BipTM = bipNode.transform case of( (i==1):( -- Root: 위치만 조정, 축 변환 제거 biped.setTransform bipNode #pos p false ) (findString n "Head"!= undefined): ( -- Head의 자식은 Neck이므로 이름으로 찾기 rigNodeEnd = execute ("$'Neck'") if isvalidNode rigNodeEnd do( -- Head: 크기와 위치만 조정, 축 변환 제거 dist = distance rigNode rigNodeEnd biped.setTransform bipNode #scale [dist,dist,dist] false biped.setTransform bipNode #pos rigNode.transform.row4 false ) ) (matchPattern n pattern:"*Neck*"):( -- Neck: 크기만 조정, 축 변환 제거 dist = distance rigNode rigNodeEnd biped.setTransform bipNode #scale [dist,dist,dist] false ) (matchPattern n pattern:"*Foot*" ):( if TPose then alignFootTpose rigNode bipNode else alignFoot rigNode bipNode figureMode:true ) (matchPattern n pattern:"*Toe*"):( -- Toe의 자식은 없으므로 기본 크기 사용 -- Toe: 크기와 위치만 조정, 축 변환 제거 biped.setTransform bipNode #scale [1,1*0.2,1] false biped.setTransform bipNode #pos rigNode.pos false ) (findString n "Pelvis"!= undefined ):( rigNode = $'LeftUpperLeg' rigNodeEnd = $'RightUpperLeg' dist = distance Lthigh Rthigh len = distance rigNode rigNodeEnd biped.setTransform bipNode #scale [dist,dist,dist] false ) (MatchPattern n pattern:"*Spine*"):( -- 스파인 2개 구조에서 Spine1이 마지막 스파인 if findString n "Spine1"!=undefined do ( rigNodeEnd =$'Neck' ) if isvalidNode rigNode and isvalidNode rigNodeEnd do( len = distance rigNode rigNodeEnd biped.setTransform bipNode #scale [len,len,len] false ) if matchpattern n pattern:"*Spine*" do( -- Spine: 위치만 조정, 축 변환 제거 biped.setTransform bipNode #pos rigNode.pos false ) ) (matchPattern n pattern:"*Clavicle*"):( -- 정확한 Arm 본 찾기 (children[1] 대신) rigNodeEnd = undefined if findString n "L Clavicle" != undefined or findString n "LeftShoulder" != undefined then ( rigNodeEnd = execute ("$'LeftUpperArm'") ) else if findString n "R Clavicle" != undefined or findString n "RightShoulder" != undefined then ( rigNodeEnd = execute ("$'RightUpperArm'") ) -- 백업: 이름으로 못 찾으면 children[1] 사용 if rigNodeEnd == undefined do rigNodeEnd = rigNode.children[1] len = distance rigNode rigNodeEnd x = Normalize (rigNodeEnd.pos-rigNode.pos) z = Normalize (Cross (bipNode.parent.transform.row2) x) y = Normalize (cross z x) newTM = matrix3 x y z rigTM.row4 biped.setTransform bipNode #scale [len,len,len] false SetClavicleTM bipCtl bipNode newTM ) (matchPattern n pattern:"*UpperArm*"):( if MatchPattern n pattern:"* R *" then AlignArm bipNode rigNode side:"R" FigureMode:true TPose:TPose else AlignArm bipNode rigNode side:"L" FigureMode:true TPose:TPose ) (matchPattern n pattern:"*Thigh*"):( if MatchPattern n pattern:"* R *" then AlignLeg bipNode rigNode side:"R" figureMode:true TPose:TPose else AlignLeg bipNode rigNode side:"L" figureMode:true TPose:TPose ) (MatchPattern n pattern:"*Finger*"): alignFingers bipNode rigNode figureMode:true ) ) ) ) catch ( format "❌ 비율 매칭 중 오류 발생: %\n" (getCurrentException()) false ) finally ( -- Figure Mode 안전하게 종료 figManager.exitMode() ) stepPerf "비율 매칭 완료" true ) fn getNumFingers = ( -- Unity Human Bone 표준 손가락 본의 정확한 이름들 (첫 번째 관절만) local fingerNames = #( "LeftThumbProximal", "LeftIndexProximal", "LeftMiddleProximal", "LeftRingProximal", "LeftLittleProximal", "RightThumbProximal", "RightIndexProximal", "RightMiddleProximal", "RightRingProximal", "RightLittleProximal" ) local leftFingers = 0 local rightFingers = 0 -- 왼손 손가락 체크 for i = 1 to 5 do ( if isValidNode (getNodeByName fingerNames[i]) do leftFingers += 1 ) -- 오른손 손가락 체크 for i = 6 to 10 do ( if isValidNode (getNodeByName fingerNames[i]) do rightFingers += 1 ) local maxFingers = amax #(leftFingers, rightFingers) format "손가락 본 체크: 왼손 %개, 오른손 %개\\n" leftFingers rightFingers -- 3ds Max Biped 제한 (0-5) if maxFingers > 5 then ( format "⚠️ 손가락 수 %개 → 5개로 제한\\n" maxFingers maxFingers = 5 ) format "최종 Biped fingers: %\\n" maxFingers maxFingers ) fn GetExtras root=( local out = #() if root!=undefined do( format "GetExtras: Root 노드 처리 시작 - %\\n" root.name local hrc = undefined try ( hrc = getBranchHRC root format "GetExtras: getBranchHRC 성공 - %개 레벨\\n" hrc.count ) catch ( format "❌ GetExtras: getBranchHRC 실패\\n" return #() ) local processedLevels = 0 for h=hrc.count to 1 by -1 do ( local objArr = hrc[h] local removedCount = 0 for j=objArr.count to 1 by -1 do( -- Unity Human Bone 표준 본과 선택적 본 모두 체크 local isStandardBone = (finditem MXNames objArr[j].name != 0) or (finditem OptionalNames objArr[j].name != 0) if isStandardBone do ( deleteitem objArr j -- REMOVE STANDARD FROM EXTRAS removedCount += 1 ) ) if objArr.count==0 do deleteItem hrc h processedLevels += 1 if mod processedLevels 10 == 0 do format "GetExtras: %개 레벨 처리됨\\n" processedLevels ) format "GetExtras: 처리 완료 - %개 Extra 그룹\\n" hrc.count out = hrc ) out ) fn cleanUpSkin obj =( local skn = obj.modifiers["skin"] local count = skinOps.GetNumberBones skn local IDList = #() for i=1 to count do( n = (skinOps.GetBoneName skn i 0) if findItem excludeBones n!=0 do( append IDList i ) ) if IDList.count>0 do for i=IDList.count to 1 by -1 do( skinOps.removeBone skn IDList[i] ) ) fn GetSkinPairs obj extra:#() =( local out=#() local noPairs = #() local extraV1 = for i in extra collect i.v1 local skn = obj.modifiers[#skin] local count = skinOps.GetNumberBones skn for i=1 to count do( local foundPairs = true local n = (skinOps.GetBoneName skn i 0) local dp = datapair n "" case of( (matchpattern n pattern:"*Hips"): dp.v2 = "Bip001 Pelvis" (matchpattern n pattern:"*Spine"): dp.v2 = "Bip001 Spine" (matchpattern n pattern:"*Spine1"): dp.v2 = "Bip001 Spine1" (matchpattern n pattern:"*Spine2"): dp.v2 = "Bip001 Spine2" (matchpattern n pattern:"*Neck"): dp.v2 = "Bip001 Neck" (matchpattern n pattern:"*Head"): dp.v2 = "Bip001 Head" (matchpattern n pattern:"*LeftShoulder"): dp.v2 = "Bip001 L Clavicle" (matchpattern n pattern:"*LeftArm"): dp.v2 = "Bip001 L UpperArm" (matchpattern n pattern:"*LeftForeArm"): dp.v2 = "Bip001 L Forearm" (matchpattern n pattern:"*LeftHand"): dp.v2 = "Bip001 L Hand" (matchpattern n pattern:"*LeftHandThumb1"): dp.v2 = "Bip001 L Finger0" (matchpattern n pattern:"*LeftHandThumb2"): dp.v2 = "Bip001 L Finger01" (matchpattern n pattern:"*LeftHandThumb3"): dp.v2 = "Bip001 L Finger02" (matchpattern n pattern:"*LeftHandThumb4"): dp.v2 = "Bip001 L Finger0Nub" (matchpattern n pattern:"*LeftHandIndex1"): dp.v2 = "Bip001 L Finger1" (matchpattern n pattern:"*LeftHandIndex2"): dp.v2 = "Bip001 L Finger11" (matchpattern n pattern:"*LeftHandIndex3"): dp.v2 = "Bip001 L Finger12" -- (matchpattern n pattern:"*LeftHandIndex4"): dp.v2 = "Bip001 L Finger1Nub" -- Unity Human Bone에 없음 (matchpattern n pattern:"*LeftHandMiddle1"): dp.v2 = "Bip001 L Finger2" (matchpattern n pattern:"*LeftHandMiddle2"): dp.v2 = "Bip001 L Finger21" (matchpattern n pattern:"*LeftHandMiddle3"): dp.v2 = "Bip001 L Finger22" (matchpattern n pattern:"*LeftHandMiddle4"): dp.v2 = "Bip001 L Finger2Nub" (matchpattern n pattern:"*LeftHandRing1"): dp.v2 = "Bip001 L Finger3" (matchpattern n pattern:"*LeftHandRing2"): dp.v2 = "Bip001 L Finger31" (matchpattern n pattern:"*LeftHandRing3"): dp.v2 = "Bip001 L Finger32" (matchpattern n pattern:"*LeftHandRing4"): dp.v2 = "Bip001 L Finger3Nub" (matchpattern n pattern:"*LeftHandPinky1"): dp.v2 = "Bip001 L Finger4" (matchpattern n pattern:"*LeftHandPinky2"): dp.v2 = "Bip001 L Finger41" (matchpattern n pattern:"*LeftHandPinky3"): dp.v2 = "Bip001 L Finger42" (matchpattern n pattern:"*LeftHandPinky4"): dp.v2 = "Bip001 L Finger4Nub" (matchpattern n pattern:"*RightShoulder"): dp.v2 = "Bip001 R Clavicle" (matchpattern n pattern:"*RightArm"): dp.v2 = "Bip001 R UpperArm" (matchpattern n pattern:"*RightForeArm"): dp.v2 = "Bip001 R Forearm" (matchpattern n pattern:"*RightHand"): dp.v2 = "Bip001 R Hand" (matchpattern n pattern:"*RightHandThumb1"): dp.v2 = "Bip001 R Finger0" (matchpattern n pattern:"*RightHandThumb2"): dp.v2 = "Bip001 R Finger01" (matchpattern n pattern:"*RightHandThumb3"): dp.v2 = "Bip001 R Finger02" (matchpattern n pattern:"*RightHandThumb4"): dp.v2 = "Bip001 R Finger0Nub" (matchpattern n pattern:"*RightHandIndex1"): dp.v2 = "Bip001 R Finger1" (matchpattern n pattern:"*RightHandIndex2"): dp.v2 = "Bip001 R Finger11" (matchpattern n pattern:"*RightHandIndex3"): dp.v2 = "Bip001 R Finger12" -- (matchpattern n pattern:"*RightHandIndex4"): dp.v2 = "Bip001 R Finger1Nub" -- Unity Human Bone에 없음 (matchpattern n pattern:"*RightHandMiddle1"): dp.v2 = "Bip001 R Finger2" (matchpattern n pattern:"*RightHandMiddle2"): dp.v2 = "Bip001 R Finger21" (matchpattern n pattern:"*RightHandMiddle3"): dp.v2 = "Bip001 R Finger22" (matchpattern n pattern:"*RightHandMiddle4"): dp.v2 = "Bip001 R Finger2Nub" (matchpattern n pattern:"*RightHandRing1"): dp.v2 = "Bip001 R Finger3" (matchpattern n pattern:"*RightHandRing2"): dp.v2 = "Bip001 R Finger31" (matchpattern n pattern:"*RightHandRing3"): dp.v2 = "Bip001 R Finger32" (matchpattern n pattern:"*RightHandRing4"): dp.v2 = "Bip001 R Finger3Nub" (matchpattern n pattern:"*RightHandPinky1"): dp.v2 = "Bip001 R Finger4" (matchpattern n pattern:"*RightHandPinky2"): dp.v2 = "Bip001 R Finger41" (matchpattern n pattern:"*RightHandPinky3"): dp.v2 = "Bip001 R Finger42" (matchpattern n pattern:"*RightHandPinky4"): dp.v2 = "Bip001 R Finger4Nub" (matchpattern n pattern:"*LeftUpLeg"): dp.v2 = "Bip001 L Thigh" (matchpattern n pattern:"*LeftLeg"): dp.v2 = "Bip001 L Calf" (matchpattern n pattern:"*LeftFoot"): dp.v2 = "Bip001 L Foot" (matchpattern n pattern:"*LeftToeBase"): dp.v2 = "Bip001 L Toe0" (matchpattern n pattern:"*RightUpLeg"): dp.v2 = "Bip001 R Thigh" (matchpattern n pattern:"*RightLeg"): dp.v2 = "Bip001 R Calf" (matchpattern n pattern:"*RightFoot"): dp.v2 = "Bip001 R Foot" (matchpattern n pattern:"*RightToeBase"): dp.v2 = "Bip001 R Toe0" default:(foundPairs = false ) ) local indexInExtra = findItem extraV1 n if not foundPairs and indexInExtra != 0 then( dp.v2 = extra[indexInExtra].v2 ) else if not foundPairs do append noPairs n append out dp ) out ) -- ================================== 최적화된 스킨 처리 함수들 =============================================== -- 본 ID 매핑 찾기 함수 (전역) fn findBoneIDByMapping oldBoneName bonePairs newSkin =( /* 본 이름 매핑을 통해 새 스킨에서 본 ID 찾기 */ try ( for pair in bonePairs do ( if pair.v1 == oldBoneName do ( local newBoneName = pair.v2 local numBones = skinOps.GetNumberBones newSkin for b = 1 to numBones do ( if (skinOps.GetBoneName newSkin b 0) == newBoneName do return b ) exit ) ) return 0 ) catch ( return 0 ) ) -- 메모리 기반 스킨 가중치 전송 (파일 I/O 제거) struct OptimizedSkinTransfer ( oldSkin, newSkin, bonePairs, fn findBoneIDByMappingOld oldBoneName bonePairs newSkin =( /* 본 매핑을 통해 새 스킨에서 본 ID 찾기 잠재적 충돌: 본 이름이 일치하지 않는 경우 안전장치: 이름 매핑 테이블 사용 및 fallback */ for pair in bonePairs do ( if pair.v1 == oldBoneName do ( local newBoneName = pair.v2 local numBones = skinOps.GetNumberBones newSkin for b = 1 to numBones do ( if (skinOps.GetBoneName newSkin b 0) == newBoneName do return b ) exit ) ) 0 -- 찾지 못함 ), fn transferWeights =( /* 가중치 데이터 직접 메모리 전송 잠재적 충돌: 버텍스 수 불일치, 본 구조 변경 안전장치: 각 버텍스별 유효성 검사 */ if not OPTIMIZE_SKIN_TRANSFER do return false try ( local numVerts = skinOps.getNumberVertices oldSkin local successCount = 0 for v = 1 to numVerts do ( local numBones = skinOps.getVertexWeightCount oldSkin v local weights = #() local boneIDs = #() for b = 1 to numBones do ( local weight = skinOps.getVertexWeight oldSkin v b local boneID = skinOps.getVertexWeightBoneID oldSkin v b local oldBoneName = skinOps.GetBoneName oldSkin boneID 0 -- 새 본 ID 찾기 local newBoneID = findBoneIDByMapping oldBoneName bonePairs newSkin if newBoneID > 0 and weight > 0.001 do ( -- 매우 작은 가중치 제거 append weights weight append boneIDs newBoneID ) ) -- 가중치 정규화 및 적용 if weights.count > 0 then ( local totalWeight = 0 for w in weights do totalWeight += w if totalWeight > 0.001 then ( for i = 1 to weights.count do weights[i] /= totalWeight skinOps.replaceVertexWeights newSkin v boneIDs weights successCount += 1 ) ) ) if VERBOSE_LOGGING do format " 📊 성공적으로 전송된 버텍스: %d/%d\n" successCount numVerts true ) catch ( if VERBOSE_LOGGING do format "⚠️ 메모리 기반 스킨 전송 실패\n" false ) ) ) -- 슈퍼 고속 스킨 처리 (배치 최적화) fn ultraFastSkinTransfer oldSkin newSkin bonePairs =( /* 전체 가중치 데이터를 배치로 처리하는 초고속 버전 메모리 사용량 증가하지만 10-20배 더 빠름 */ try ( local numVerts = skinOps.getNumberVertices oldSkin if numVerts <= 0 do return false -- 본 매핑 테이블 미리 생성 (빠른 조회) local boneMap = #() local numOldBones = skinOps.GetNumberBones oldSkin for i = 1 to numOldBones do ( local oldName = skinOps.GetBoneName oldSkin i 0 local newID = findBoneIDByMapping oldName bonePairs newSkin append boneMap newID ) -- 전체 정점 배치 처리 local vertexData = #() for v = 1 to numVerts do ( local numBones = skinOps.getVertexWeightCount oldSkin v local weights = #() local boneIDs = #() for b = 1 to numBones do ( local weight = skinOps.getVertexWeight oldSkin v b local oldBoneID = skinOps.getVertexWeightBoneID oldSkin v b if oldBoneID <= boneMap.count then ( local newBoneID = boneMap[oldBoneID] if newBoneID > 0 and weight > 0.001 do ( append weights weight append boneIDs newBoneID ) ) ) -- 가중치 정규화 if weights.count > 0 then ( local totalWeight = 0 for w in weights do totalWeight += w if totalWeight > 0.001 do ( for i = 1 to weights.count do weights[i] /= totalWeight append vertexData (datapair v (datapair boneIDs weights)) ) ) ) -- 배치로 모든 정점에 가중치 적용 (한번에 처리) if vertexData.count > 0 then ( -- UI 업데이트 완전 차단 local wasEditing = isSceneEditing() local wasRedraw = isSceneRedrawEnabled() if wasEditing do suspendEditing() if wasRedraw do disableSceneRedraw() -- 배치 적용 for vData in vertexData do ( skinOps.replaceVertexWeights newSkin vData.v1 vData.v2.v1 vData.v2.v2 ) -- UI 복원 if wasEditing do resumeEditing() if wasRedraw do enableSceneRedraw() format "⚡ 초고속 배치 처리 완료: %개 정점\n" vertexData.count true ) else ( false ) ) catch ( format "❌ 초고속 스킨 처리 실패: %\n" (getCurrentException()) false ) ) -- 최적화된 스킨 처리 관리자 fn processSkinnedObjectsOptimized skinObjects extrasPairs =( /* 모든 스킨 오브젝트를 최적화된 방식으로 처리 잠재적 충돌: 메모리 부족, 스킨 구조 변경 안전장치: 메모리 기반 실패 시 기존 방식으로 폴백 */ local uiOpt = SuperUIOptimizer() uiOpt.suspend() -- 슈퍼 UI 업데이트 억제 try ( local progress = ProgressManager() progress.init skinObjects.count for i in skinObjects do ( progress.update ("스킨 처리: " + i.name) local skn = i.modifiers["skin"] if skn == undefined do continue -- 페어 가져오기 (표준 본 + 엑스트라 본) local pairs = GetSkinPairs i extra:extrasPairs processSkinnedObjectStable i pairs -- 모디파이어 스택 정리 maxOps.CollapseNodeTo i 2 true ) ) catch ( format "❌ 스킨 처리 중 오류: %\n" (getCurrentException()) ) finally ( uiOpt.resume() -- UI 업데이트 재개 ) ) -- 안정적인 파일 I/O 방식 스킨 처리 (copy 버전과 동일) fn processSkinnedObjectStable skinObj pairs =( /* copy 버전과 정확히 동일한 파일 I/O 방식의 스킨 처리 안전하지만 느림 */ local skn = skinObj.modifiers["skin"] local pth = GetDir #export local n = skinObj.name+"_skin.env" local f = (pth+"\\"+n) -- 기존 스킨 데이터 저장 if skn != undefined then skinOps.saveEnvelope skn f local bipedSkin = #() for j in pairs do ( local obj1 = getNodeByName j.v1 local obj2 = getNodeByName j.v2 if isvalidNode obj1 and isValidNode obj2 do( tempName = obj1.name obj1.name = obj2.name obj2.name = tempName append bipedSkin obj2 ) ) -- 새 스킨 추가 local skn2 = skin() addmodifier skinObj skn2 select skinObj max modify mode modPanel.setCurrentObject skn2 -- 본들 추가 (copy 버전과 동일한 방식) for j in bipedSkin do( if j!= bipedSkin[bipedSkin.count] then skinOps.AddBone skn2 j 0 else skinOps.AddBone skn2 j -1 ) max create mode -- 스킨 데이터 로드 if doesFileExist f then skinOps.loadEnvelope skn2 f -- 이름 되돌리기 for j in pairs do ( local obj1 = getNodeByName j.v1 local obj2 = getNodeByName j.v2 if isValidNode obj1 and isValidNode obj2 do( tempName = obj1.name obj1.name = obj2.name obj2.name = tempName ) ) -- 임시 파일 삭제 if doesFileExist f do deleteFile f ) -- 기존 방식 스킨 처리 (폴백용) fn processSkinnedObjectClassic skinObj pairs =( /* 스킨 처리 + Body_2 특별 디버깅 */ format "🔍 DEBUG: processSkinnedObjectClassic 함수 시작 - 오브젝트: %\\n" skinObj.name format "🔍 DEBUG: 스킨 모디파이어 찾는 중...\\n" local skn = skinObj.modifiers["skin"] if skn == undefined do ( format "❌ % - 스킨 모디파이어 없음\\n" skinObj.name return false ) format "🔍 DEBUG: 스킨 모디파이어 발견: %\\n" skn local isBody2 = (findString skinObj.name "Body_2" != undefined) if isBody2 do format "🔍 Body_2 디버깅 시작\\n" -- 스킨 상태 점검 format "🔍 DEBUG: skinOps.GetNumberBones 호출 중...\\n" local totalBones = skinOps.GetNumberBones skn format "🔍 DEBUG: GetNumberBones 성공, 결과: %\\n" totalBones format "🔍 DEBUG: skinOps.getNumberVertices 호출 중...\\n" local vertCount = skinOps.getNumberVertices skn format "🔍 DEBUG: getNumberVertices 성공, 결과: %\\n" vertCount format "메시: % | 본 수: % | 버텍스 수: %\\n" skinObj.name totalBones vertCount -- Body_2 간단 분석 (속도 우선) if isBody2 do format "Body_2: %개 본, %개 버텍스\\n" totalBones vertCount -- ⚡ 본 교체 + Body_2 특별 처리 local startTime = timestamp() local replacedCount = 0 local failedBones = #() -- 한국어 접미사 패턴들 (Biped 매핑이 불가능한 엑스트라 본들) local koreanSuffixes = #("_오픈숄더", "_양갈래", "_헤드", "_스웨터", "_장갑", "_신발", "_바지", "_치마", "_모자", "_안경", "_머리띠", "_목걸이", "_귀걸이", "_팔찌") -- 더 안전한 본 교체 방식 (Biped 본 검색 개선) for j in pairs do ( -- 한국어 접미사가 있는 본은 건너뜀 (Biped에 대응 불가) local hasKoreanSuffix = false for suffix in koreanSuffixes do ( if findString j.v1 suffix != undefined then ( hasKoreanSuffix = true format "⏩ 한국어 접미사 본 스킵: %\\n" j.v1 exit ) ) if hasKoreanSuffix then continue -- Biped 본 찾기: 여러 방법 시도 local bipedBone = undefined local searchMethod = "" try ( -- 방법 1: 정확한 이름으로 검색 bipedBone = getNodeByName j.v2 if isValidNode bipedBone then ( searchMethod = "getNodeByName" ) else ( -- 방법 2: execute 사용 local executeStr = "$'" + j.v2 + "'" bipedBone = execute executeStr if isValidNode bipedBone then ( searchMethod = "execute" ) else ( -- 방법 3: 씬에서 패턴 매칭으로 검색 for obj in objects where (matchPattern obj.name pattern:("*" + j.v2 + "*")) do ( if obj.name == j.v2 then ( bipedBone = obj searchMethod = "pattern" exit ) ) ) ) ) catch () if isValidNode bipedBone then ( format "🔍 Biped 본 발견: % (방법: %)\\n" j.v2 searchMethod -- 먼저 Biped 본을 스킨에 추가 (없으면 추가) try ( skinOps.addBone skn bipedBone 0 ) catch () -- Unity Human Bone을 Biped 본으로 교체 try ( -- 본 이름으로 본 ID를 찾는 올바른 방법 local unityBoneID = 0 local numBones = skinOps.GetNumberBones skn for i = 1 to numBones do ( if (skinOps.GetBoneName skn i 0) == j.v1 then ( unityBoneID = i exit ) ) if unityBoneID > 0 then ( -- ⚡ 초고속 방법: replaceBone 재시도 (더 간단) try ( skinOps.replaceBone skn unityBoneID bipedBone replacedCount += 1 format "✓ 고속 교체: % → %\\n" j.v1 j.v2 ) catch ( -- replaceBone 실패 시에만 수동 처리 local bipedBoneID = 0 local numBones2 = skinOps.GetNumberBones skn for k = 1 to numBones2 do ( if (skinOps.GetBoneName skn k 0) == bipedBone.name then ( bipedBoneID = k exit ) ) if bipedBoneID > 0 then ( -- 간단한 본 ID 재매핑만 (가중치는 건드리지 않음) try ( skinOps.removeBone skn unityBoneID replacedCount += 1 format "✓ 본 제거만: %\\n" j.v1 ) catch () ) ) ) else ( format "⚠️ Unity 본 '%' ID 없음\\n" j.v1 ) ) catch ( append failedBones j.v1 format "❌ 교체 실패: % → %\\n" j.v1 j.v2 ) ) else ( format "❌ Biped 본 찾기 실패: '%' → '%' (모든 검색 방법 실패)\\n" j.v1 j.v2 append failedBones j.v1 ) ) local elapsed = (timestamp() - startTime) / 1000.0 format "⚡ %개 본 교체 완료 (%.2f초)\\n" replacedCount elapsed if failedBones.count > 0 do ( format "실패한 본들: %\\n" failedBones ) -- Body_2 간단 검증 (속도 우선) if isBody2 do ( local finalBones = skinOps.GetNumberBones skn local bipedCount = 0 for i = 1 to finalBones do ( local boneName = skinOps.GetBoneName skn i 0 if findString boneName "Bip001" != undefined do bipedCount += 1 ) format "Body_2 결과: %개 Biped / %개 전체 본\\n" bipedCount finalBones ) true ) -- Body_2 메시 문제 해결 전용 함수 fn fixBody2Mesh =( format "\\n🔧 Body_2 메시 문제 해결 시도...\\n" local body2 = getNodeByName "Body_2" if not isValidNode body2 do ( format "❌ Body_2 메시를 찾을 수 없습니다.\\n" return false ) local skn = body2.modifiers[#Skin] if skn == undefined do ( format "❌ Body_2에 스킨 모디파이어가 없습니다.\\n" return false ) format "✓ Body_2 발견, 스킨 모디파이어 확인됨\\n" -- Unity Human Bone들을 수동으로 Biped로 강제 교체 local unityToBiped = #( #("Hips", "Bip001 Pelvis"), #("Spine", "Bip001 Spine"), #("Chest", "Bip001 Spine1"), #("LeftUpperArm", "Bip001 L UpperArm"), #("LeftLowerArm", "Bip001 L Forearm"), #("LeftHand", "Bip001 L Hand"), #("RightUpperArm", "Bip001 R UpperArm"), #("RightLowerArm", "Bip001 R Forearm"), #("RightHand", "Bip001 R Hand"), #("LeftUpperLeg", "Bip001 L Thigh"), #("LeftLowerLeg", "Bip001 L Calf"), #("LeftFoot", "Bip001 L Foot"), #("RightUpperLeg", "Bip001 R Thigh"), #("RightLowerLeg", "Bip001 R Calf"), #("RightFoot", "Bip001 R Foot"), #("Head", "Bip001 Head"), #("Neck", "Bip001 Neck") ) local successCount = 0 for pair in unityToBiped do ( local unityName = pair[1] local bipedName = pair[2] -- Body_2용 향상된 Biped 본 검색 local bipedBone = undefined try ( -- 방법 1: 정확한 이름으로 검색 bipedBone = getNodeByName bipedName if not isValidNode bipedBone then ( -- 방법 2: execute 사용 bipedBone = execute ("$'" + bipedName + "'") ) if not isValidNode bipedBone then ( -- 방법 3: 씬에서 검색 for obj in objects where (matchPattern obj.name pattern:("*" + bipedName + "*")) do ( if obj.name == bipedName then ( bipedBone = obj exit ) ) ) ) catch () if isValidNode bipedBone then ( try ( -- 본 이름으로 본 ID 찾기 local unityID = 0 local numBones = skinOps.GetNumberBones skn for i = 1 to numBones do ( if (skinOps.GetBoneName skn i 0) == unityName then ( unityID = i exit ) ) if unityID > 0 then ( skinOps.replaceBone skn unityID bipedBone successCount += 1 format "✓ Body_2 강제 교체: % → %\\n" unityName bipedName ) ) catch ( format "⚠️ Body_2 교체 실패: % → %\\n" unityName bipedName ) ) ) format "🔧 Body_2 본 교체 완료: %개 성공\\n" successCount -- 최종 확인 local totalBones = skinOps.GetNumberBones skn local bipedCount = 0 for i = 1 to totalBones do ( local boneName = skinOps.GetBoneName skn i 0 if findString boneName "Bip001" != undefined do bipedCount += 1 ) format "✓ Body_2 최종 상태: %개 Biped 본 / %개 전체 본\\n" bipedCount totalBones true ) -- ================================== 모퍼 처리 함수들 =============================================== struct MorpherChannelData ( channelIndex, targetName, weight, channelName, isActive ) struct MorpherBackupData ( objectName, modifierName, channels ) fn BackupMorpherData obj =( /* 모퍼 모디파이어 데이터를 백업하는 함수 매개변수: - obj: 모퍼 모디파이어를 가진 오브젝트 반환값: - MorpherBackupData 구조체 */ local morphMod = obj.modifiers[#Morpher] if morphMod == undefined do return undefined local channels = #() local channelCount = try (morphMod.numChannels) catch (100) -- 기본값 100개 채널 format "📦 모퍼 데이터 백업: % (채널 수: %)\\n" obj.name channelCount for i = 1 to channelCount do ( try ( -- 채널 프로퍼티에 직접 접근 local channelProp = execute ("morphMod.morph_channel_" + i as string) if channelProp != undefined then ( local target = try (channelProp.target) catch (undefined) local weight = try (channelProp.value) catch (0.0) local channelName = try (channelProp.name) catch ("") local isActive = try (channelProp.active) catch (false) if target != undefined and isValidNode target do ( local channelData = MorpherChannelData \ channelIndex:i \ targetName:target.name \ weight:weight \ channelName:channelName \ isActive:isActive append channels channelData format " 채널 %: % (가중치: %, 타겟: %)\\n" i channelName weight target.name ) ) ) catch ( -- 채널이 비어있거나 접근할 수 없는 경우 건너뛰기 format " 채널 % 건너뛰기 (빈 채널)\\n" i ) ) MorpherBackupData objectName:obj.name modifierName:morphMod.name channels:channels ) fn RestoreMorpherData backupData pairsList =( /* 백업된 모퍼 데이터를 복원하는 함수 매개변수: - backupData: 백업된 모퍼 데이터 - pairsList: 본 이름 매핑 정보 반환값: - 성공 시 true, 실패 시 false */ if backupData == undefined do return false local obj = getNodeByName backupData.objectName if not isValidNode obj do ( format "[ERROR] 모퍼 오브젝트를 찾을 수 없음: %\\n" backupData.objectName return false ) local morphMod = obj.modifiers[#Morpher] if morphMod == undefined do ( format "[ERROR] 모퍼 모디파이어를 찾을 수 없음: %\\n" obj.name return false ) format "🔄 모퍼 데이터 복원: % (채널 수: %)\\n" obj.name backupData.channels.count for channelData in backupData.channels do ( -- 원본 타겟 이름을 새 이름으로 매핑 local newTargetName = channelData.targetName for pair in pairsList do ( if pair.v1 == channelData.targetName do ( newTargetName = pair.v2 exit() ) ) local newTarget = getNodeByName newTargetName if isValidNode newTarget then ( try ( -- 채널 프로퍼티에 직접 접근하여 설정 local channelProp = execute ("morphMod.morph_channel_" + channelData.channelIndex as string) if channelProp != undefined then ( -- 타겟 설정 try (channelProp.target = newTarget) catch (format " [WARNING] 타겟 설정 실패: 채널 %\\n" channelData.channelIndex) -- 가중치 복원 try (channelProp.value = channelData.weight) catch (format " [WARNING] 가중치 설정 실패: 채널 %\\n" channelData.channelIndex) -- 채널 활성화 상태 복원 try (channelProp.active = channelData.isActive) catch (format " [WARNING] 활성화 상태 설정 실패: 채널 %\\n" channelData.channelIndex) -- 채널 이름 복원 try (channelProp.name = channelData.channelName) catch (format " [WARNING] 채널 이름 설정 실패: 채널 %\\n" channelData.channelIndex) format " ✅ 채널 % 복원: % → % (가중치: %)\\n" channelData.channelIndex channelData.targetName newTargetName channelData.weight ) else ( format " ❌ 채널 % 프로퍼티 접근 실패\\n" channelData.channelIndex ) ) catch ( format " ⚠️ 채널 % 설정 실패: % → %\\n" channelData.channelIndex channelData.targetName newTargetName ) ) else ( format " ❌ 타겟을 찾을 수 없음: % → %\\n" channelData.targetName newTargetName ) ) true ) fn GetSkinPairs obj extra:#() =( local out=#() local noPairs = #() local extraV1 = for i in extra collect i.v1 local skn = obj.modifiers[#skin] local count = skinOps.GetNumberBones skn for i=1 to count do( local foundPairs = true local n = (skinOps.GetBoneName skn i 0) local dp = datapair n "" case of( (matchpattern n pattern:"*Hips"): dp.v2 = "Bip001 Pelvis" (matchpattern n pattern:"*Spine" and not matchpattern n pattern:"*Spine1"): dp.v2 = "Bip001 Spine" (matchpattern n pattern:"*Chest"): dp.v2 = "Bip001 Spine1" (matchpattern n pattern:"*Neck"): dp.v2 = "Bip001 Neck" (matchpattern n pattern:"*Head"): dp.v2 = "Bip001 Head" (matchpattern n pattern:"*LeftShoulder"): dp.v2 = "Bip001 L Clavicle" (matchpattern n pattern:"*LeftUpperArm"): dp.v2 = "Bip001 L UpperArm" (matchpattern n pattern:"*LeftLowerArm"): dp.v2 = "Bip001 L Forearm" (matchpattern n pattern:"*LeftHand"): dp.v2 = "Bip001 L Hand" (matchpattern n pattern:"*LeftThumbProximal"): dp.v2 = "Bip001 L Finger0" (matchpattern n pattern:"*LeftThumbIntermediate"): dp.v2 = "Bip001 L Finger01" (matchpattern n pattern:"*LeftThumbDistal"): dp.v2 = "Bip001 L Finger02" -- LeftHandThumb4 제거됨 (matchpattern n pattern:"*LeftIndexProximal"): dp.v2 = "Bip001 L Finger1" (matchpattern n pattern:"*LeftIndexIntermediate"): dp.v2 = "Bip001 L Finger11" (matchpattern n pattern:"*LeftIndexDistal"): dp.v2 = "Bip001 L Finger12" -- (matchpattern n pattern:"*LeftIndexDistal"): dp.v2 = "Bip001 L Finger1Nub" -- Unity Human Bone에 Index4 없음 (matchpattern n pattern:"*LeftMiddleProximal"): dp.v2 = "Bip001 L Finger2" (matchpattern n pattern:"*LeftMiddleIntermediate"): dp.v2 = "Bip001 L Finger21" (matchpattern n pattern:"*LeftMiddleDistal"): dp.v2 = "Bip001 L Finger22" -- LeftHandMiddle4 제거됨 (matchpattern n pattern:"*LeftRingProximal"): dp.v2 = "Bip001 L Finger3" (matchpattern n pattern:"*LeftRingIntermediate"): dp.v2 = "Bip001 L Finger31" (matchpattern n pattern:"*LeftRingDistal"): dp.v2 = "Bip001 L Finger32" -- LeftHandRing4 제거됨 (matchpattern n pattern:"*LeftLittleProximal"): dp.v2 = "Bip001 L Finger4" (matchpattern n pattern:"*LeftLittleIntermediate"): dp.v2 = "Bip001 L Finger41" (matchpattern n pattern:"*LeftLittleDistal"): dp.v2 = "Bip001 L Finger42" -- LeftHandPinky4 제거됨 (matchpattern n pattern:"*RightShoulder"): dp.v2 = "Bip001 R Clavicle" (matchpattern n pattern:"*RightUpperArm"): dp.v2 = "Bip001 R UpperArm" (matchpattern n pattern:"*RightLowerArm"): dp.v2 = "Bip001 R Forearm" (matchpattern n pattern:"*RightHand"): dp.v2 = "Bip001 R Hand" (matchpattern n pattern:"*RightThumbProximal"): dp.v2 = "Bip001 R Finger0" (matchpattern n pattern:"*RightThumbIntermediate"): dp.v2 = "Bip001 R Finger01" (matchpattern n pattern:"*RightThumbDistal"): dp.v2 = "Bip001 R Finger02" -- RightHandThumb4 제거됨 (matchpattern n pattern:"*RightIndexProximal"): dp.v2 = "Bip001 R Finger1" (matchpattern n pattern:"*RightIndexIntermediate"): dp.v2 = "Bip001 R Finger11" (matchpattern n pattern:"*RightIndexDistal"): dp.v2 = "Bip001 R Finger12" -- (matchpattern n pattern:"*RightIndexDistal"): dp.v2 = "Bip001 R Finger1Nub" -- Unity Human Bone에 Index4 없음 (matchpattern n pattern:"*RightMiddleProximal"): dp.v2 = "Bip001 R Finger2" (matchpattern n pattern:"*RightMiddleIntermediate"): dp.v2 = "Bip001 R Finger21" (matchpattern n pattern:"*RightMiddleDistal"): dp.v2 = "Bip001 R Finger22" -- RightHandMiddle4 제거됨 (matchpattern n pattern:"*RightRingProximal"): dp.v2 = "Bip001 R Finger3" (matchpattern n pattern:"*RightRingIntermediate"): dp.v2 = "Bip001 R Finger31" (matchpattern n pattern:"*RightRingDistal"): dp.v2 = "Bip001 R Finger32" -- RightHandRing4 제거됨 (matchpattern n pattern:"*RightLittleProximal"): dp.v2 = "Bip001 R Finger4" (matchpattern n pattern:"*RightLittleIntermediate"): dp.v2 = "Bip001 R Finger41" (matchpattern n pattern:"*RightLittleDistal"): dp.v2 = "Bip001 R Finger42" -- RightHandPinky4 제거됨 (matchpattern n pattern:"*LeftUpperLeg"): dp.v2 = "Bip001 L Thigh" (matchpattern n pattern:"*LeftLowerLeg"): dp.v2 = "Bip001 L Calf" (matchpattern n pattern:"*LeftFoot"): dp.v2 = "Bip001 L Foot" (matchpattern n pattern:"*LeftToes"): dp.v2 = "Bip001 L Toe0" (matchpattern n pattern:"*RightUpperLeg"): dp.v2 = "Bip001 R Thigh" (matchpattern n pattern:"*RightLowerLeg"): dp.v2 = "Bip001 R Calf" (matchpattern n pattern:"*RightFoot"): dp.v2 = "Bip001 R Foot" (matchpattern n pattern:"*RightToes"): dp.v2 = "Bip001 R Toe0" default:(foundPairs = false ) ) local indexInExtra = findItem extraV1 n if not foundPairs and indexInExtra != 0 then( dp.v2 = extra[indexInExtra].v2 foundPairs = true ) append out dp if not foundPairs do append noPairs n ) out ) -- ================================== 메인 변환 함수 =============================================== -- ================================== 단계별 변환 데이터 =============================================== -- 단계 간 공유 데이터 구조체 struct StepData ( charName = "", alwaysDeform = false, bipObj = undefined, extras = undefined, extrasPairs = undefined, skinObjects = undefined, morpherModifiers = undefined, result = false ) -- 전역 단계 데이터 global STEP_DATA = undefined -- ================================== 1단계: Biped 생성 및 기본 설정 =============================================== fn ConvertUnityHumanToBiped_Step1 charName:"Character001" alwaysDeform:false =( /* 1단계: Biped 생성 및 기본 설정 (빠른 단계) 포함 작업: - 전처리 및 검증 - Biped 생성 및 구조 수정 - 비율 매칭 (BipToRigProportion) - Extra Bones 처리 매개변수: - charName: 캐릭터 이름 (기본값: "Character001") - alwaysDeform: 스킨 Always Deform 설정 (기본값: false) 반환값: - 성공 시 true, 실패 시 false */ -- 단계 데이터 초기화 STEP_DATA = StepData charName:charName alwaysDeform:alwaysDeform -- 디버깅: 씬에 있는 모든 본 이름 출력 format "\n=== 씬에 있는 모든 객체 디버깅 ===\n" local allObjects = objects as array local boneObjects = #() for obj in allObjects do ( if (classOf obj == BoneGeometry) or (findString obj.name "Hips" != undefined) or (findString obj.name "Spine" != undefined) or (findString obj.name "Arm" != undefined) or (findString obj.name "Leg" != undefined) or (findString obj.name "Hand" != undefined) or (findString obj.name "Foot" != undefined) or (findString obj.name "Head" != undefined) then ( append boneObjects obj format "발견된 본: % (클래스: %)\n" obj.name (classOf obj) ) ) format "총 %개의 본 관련 객체 발견\n\n" boneObjects.count -- 성능 측정 시작 startPerf -- 슈퍼 UI 최적화 관리자 초기화 local uiOpt = SuperUIOptimizer() uiOpt.suspend() -- 변환 중 슈퍼 UI 업데이트 억제 if charName=="" do( uiOpt.resume() messagebox "please insert Character Name" return false ) try ( -- 이름 정리 및 검증 stepPerf "전처리 시작" CleanNames() local root = getNodeByName "Hips" if not isValidNode root do ( uiOpt.resume() format "[ERROR] Hips not found!\n" return false ) -- Unity Human Bone 유효성 검증 (간소화) format "Unity Human Bone 검증: Hips 발견됨 - 검증 통과\n" -- SanityCheck() 함수는 Unity Human Bone에서 불필요하므로 생략 stepPerf "전처리 완료" -- 데이터 수집 시작 stepPerf "데이터 수집 시작" format "Root 노드 확인: % (유효함)\\n" root.name local hrc = getHierarchy #(root) #() format "Unity Human Bone 계층 구조 수집: %개 노드\\n" hrc.count -- 추가 본들 수집 및 메인에서 제외 (디버깅 추가) format "GetExtras 함수 호출 시작...\\n" local extras = undefined try ( extras = GetExtras root format "GetExtras 성공: %개 그룹\\n" extras.count -- 실제로 존재하는 Unity Human Bone만 확인 local requiredBones = #("Hips", "Spine", "LeftUpperArm", "RightUpperArm", "LeftUpperLeg", "RightUpperLeg") local foundRequired = 0 for boneName in requiredBones do ( local bone = getNodeByName boneName if isValidNode bone then ( foundRequired += 1 format "필수 본 확인: % ✓\\n" boneName ) else ( format "필수 본 없음: % ✗\\n" boneName ) ) format "필수 본 %개/%개 발견됨\\n" foundRequired requiredBones.count ) catch ( format "❌ GetExtras 함수 실패: %\\n" (getCurrentException()) uiOpt.resume() return false ) for i in extras do( for j in i do( deleteItem hrc (findItem hrc j) ) ) -- 스킨 오브젝트들 수집 local skinObjects = for i in objects where i.modifiers[#Skin]!=undefined collect i format "스킨 오브젝트 발견: %개\\n" skinObjects.count if alwaysDeform do for i in skinObjects do ( cleanUpSkin i i.modifiers[#Skin].always_deform =false i.modifiers[#Skin].always_deform =true ) -- 모퍼 오브젝트들 수집 (간소화) local morpherObjects = for i in objects where i.modifiers[#Morpher]!=undefined collect i local morpherModifiers = #() -- 제거된 모퍼 모디파이어들을 저장 format "모퍼 오브젝트 발견: %개\\n" morpherObjects.count for obj in morpherObjects do ( -- 모퍼 모디파이어를 임시로 제거하고 저장 local morphMod = obj.modifiers[#Morpher] append morpherModifiers (datapair obj morphMod) deleteModifier obj morphMod ) stepPerf "데이터 수집 완료" -- 믹사모 리그 구조 분석 stepPerf "리그 분석 시작" local mxNodes = getRigStructures() format "믹사모 리그 노드 분석 완료\\n" local hipsObj = getNodeByName "Hips" if not isValidNode hipsObj do ( uiOpt.resume() format "❌ Unity Human Bone 'Hips' 오브젝트를 찾을 수 없습니다!\\n" return false ) -- 전체 씬에서 가장 높은 본을 찾아 높이 계산 local allBones = #(hipsObj) local headObj = getNodeByName "Head" if isValidNode headObj do append allBones headObj local maxZ = hipsObj.pos.z local minZ = hipsObj.pos.z for bone in allBones do ( if bone.pos.z > maxZ do maxZ = bone.pos.z if bone.pos.z < minZ do minZ = bone.pos.z ) local height = maxZ - minZ + 20 -- 여유분 추가 if height < 120 do height = 180 -- 최소 높이 보장 format "계산된 높이: %\\n" height local numfingers = getNumFingers() -- 바이패드 생성 stepPerf "Biped 생성 시작" format "\\n=== BIPED 생성 디버그 ===\\n" format "spineSegments: %\\n" spineSegments format "height: %\\n" height format "numfingers: %\\n" numfingers local bipObj = undefined try ( bipObj = createBip height spine:spineSegments fingers:numfingers format "✅ Biped 생성 성공\\n" ) catch ( uiOpt.resume() format "❌ Biped 생성 실패: %\\n" (getCurrentException()) return false ) if not isValidNode bipObj do ( uiOpt.resume() format "❌ 생성된 Biped이 유효하지 않습니다!\\n" return false ) -- 생성 직후 구조 확인 format "\\n=== 생성 직후 Biped 구조 ===\\n" local pelvis = biped.getNode bipObj #pelvis link:1 local spine1 = biped.getNode bipObj #spine link:1 local spine2 = try (biped.getNode bipObj #spine link:2) catch (undefined) local lthigh = biped.getNode bipObj #lleg link:1 local lclavicle = biped.getNode bipObj #larm link:1 local neck = biped.getNode bipObj #neck link:1 format "Pelvis: % (부모: %)\\n" pelvis.name (if pelvis.parent != undefined then pelvis.parent.name else "없음") format "Spine1: % (부모: %)\\n" spine1.name (if spine1.parent != undefined then spine1.parent.name else "없음") if spine2 != undefined then format "Spine2: % (부모: %)\\n" spine2.name (if spine2.parent != undefined then spine2.parent.name else "없음") format "LThigh: % (부모: %)\\n" lthigh.name (if lthigh.parent != undefined then lthigh.parent.name else "없음") format "LClavicle: % (부모: %)\\n" lclavicle.name (if lclavicle.parent != undefined then lclavicle.parent.name else "없음") format "Neck: % (부모: %)\\n" neck.name (if neck.parent != undefined then neck.parent.name else "없음") -- 🔧 잘못된 구조 수정 format "\\n=== Biped 구조 수정 중 ===\\n" -- Figure Mode 활성화 (구조 수정 가능) bipObj.transform.controller.figureMode = true -- 다리 본들을 Pelvis로 이동 (현재 Spine에서 → Pelvis로) local rthigh = biped.getNode bipObj #rleg link:1 if lthigh.parent != pelvis then ( format "다리를 Pelvis로 이동: % → %\\n" lthigh.parent.name pelvis.name lthigh.parent = pelvis rthigh.parent = pelvis ) -- 어깨 본들을 최상위 Spine으로 이동 (현재 Neck에서 → Spine1로) local rclavicle = biped.getNode bipObj #rarm link:1 local topSpine = if spine2 != undefined then spine2 else spine1 -- 최상위 Spine if lclavicle.parent != topSpine then ( format "어깨를 최상위 Spine으로 이동: % → %\\n" lclavicle.parent.name topSpine.name lclavicle.parent = topSpine rclavicle.parent = topSpine ) format "구조 수정 완료!\\n" -- 구조 수정 후 확인 format "\\n=== 구조 수정 후 확인 ===\\n" format "LThigh: % (부모: %)\\n" lthigh.name (if lthigh.parent != undefined then lthigh.parent.name else "없음") format "RThigh: % (부모: %)\\n" rthigh.name (if rthigh.parent != undefined then rthigh.parent.name else "없음") format "LClavicle: % (부모: %)\\n" lclavicle.name (if lclavicle.parent != undefined then lclavicle.parent.name else "없음") format "RClavicle: % (부모: %)\\n" rclavicle.name (if rclavicle.parent != undefined then rclavicle.parent.name else "없음") local bipedNodes = GetBipStructure bipObj spinesegs:spineSegments format "Biped 노드 구조 분석 완료: %개 노드\\n" bipedNodes.count -- 비율 매칭 (안전하게) stepPerf "비율 매칭 시작" try ( BipToRigProportion bipedNodes mxNodes TPose:true format "✅ 비율 매칭 성공\\n" stepPerf "비율 매칭 완료" ) catch ( uiOpt.resume() format "❌ 비율 매칭 실패: %\\n" (getCurrentException()) return false ) -- 비율 매칭 후 구조 재확인 format "\\n=== 비율 매칭 후 Biped 구조 ===\\n" local pelvis_after = biped.getNode bipObj #pelvis link:1 local spine1_after = biped.getNode bipObj #spine link:1 local lthigh_after = biped.getNode bipObj #lleg link:1 local lclavicle_after = biped.getNode bipObj #larm link:1 local neck_after = biped.getNode bipObj #neck link:1 format "Pelvis: % (부모: %)\\n" pelvis_after.name (if pelvis_after.parent != undefined then pelvis_after.parent.name else "없음") format "Spine1: % (부모: %)\\n" spine1_after.name (if spine1_after.parent != undefined then spine1_after.parent.name else "없음") format "LThigh: % (부모: %)\\n" lthigh_after.name (if lthigh_after.parent != undefined then lthigh_after.parent.name else "없음") format "LClavicle: % (부모: %)\\n" lclavicle_after.name (if lclavicle_after.parent != undefined then lclavicle_after.parent.name else "없음") format "Neck: % (부모: %)\\n" neck_after.name (if neck_after.parent != undefined then neck_after.parent.name else "없음") -- 1단계에서는 Extra Bones 수집만 (처리는 2단계에서) local totalExtraCount = 0 for ex in extras do totalExtraCount += ex.count format "전체 Extra Bones 개수: %개 (2단계에서 처리 예정)\\n" totalExtraCount format "================================\\n" -- 1단계 완료 - 단계 데이터 저장 (extrasPairs는 2단계에서 생성) STEP_DATA.bipObj = bipObj STEP_DATA.extras = extras STEP_DATA.extrasPairs = #() -- 2단계에서 채워질 예정 STEP_DATA.skinObjects = skinObjects STEP_DATA.morpherModifiers = morpherModifiers STEP_DATA.result = true -- 중간 메모리 정리 (가벼운 정리) local memOpt = MemoryOptimizer() memOpt.quickCleanup() -- UI 복원 및 성능 결과 출력 uiOpt.resume() stepPerf "1단계 완료" format "\\n🎉 1단계 완료! 🎉\\n" format "✅ Biped 생성 및 구조 설정 완료\\n" format "✅ 비율 매칭 완료\\n" format "✅ Extra Bones % 개 수집 완료 (2단계에서 처리)\\n" totalExtraCount format "\\n▶️ 이제 [2단계: Extra Bones + 스킨 처리] 버튼을 클릭하세요\\n" return true ) catch ( -- 오류 발생 시 UI 복원 if uiOpt != undefined do uiOpt.resume() format "\\n❌ 1단계 변환 중 오류 발생!\\n" format "오류 내용: %\\n" (getCurrentException()) STEP_DATA = undefined return false ) ) -- ================================== 2단계: Extra Bones + 스킨 처리 및 정리 =============================================== fn ConvertUnityHumanToBiped_Step2 =( /* 2단계: Extra Bones 처리 + 스킨 처리 및 정리 작업 (오래 걸리는 단계) 포함 작업: - Extra Bones 연결 및 처리 (Biped에 부모 연결) - 스킨 웨이트 전송 (시간 소요) - 모퍼 모디파이어 복원 - 원본 Unity Human Bone 본 삭제 - 이름 정리 및 최종 마무리 반환값: - 성공 시 true, 실패 시 false 주의: 1단계를 먼저 실행해야 합니다. */ -- 1단계 완료 여부 검증 if STEP_DATA == undefined or not STEP_DATA.result do ( messagebox "먼저 [1단계: Biped 생성]을 완료해주세요!" return false ) -- 단계 데이터에서 변수 복원 local charName = STEP_DATA.charName local alwaysDeform = STEP_DATA.alwaysDeform local bipObj = STEP_DATA.bipObj local extras = STEP_DATA.extras local extrasPairs = STEP_DATA.extrasPairs local skinObjects = STEP_DATA.skinObjects local morpherModifiers = STEP_DATA.morpherModifiers -- 슈퍼 UI 최적화 관리자 초기화 local uiOpt = SuperUIOptimizer() uiOpt.suspend() -- 변환 중 슈퍼 UI 업데이트 억제 try ( stepPerf "2단계 시작" format "\\n=== 2단계: 스킨 처리 및 정리 시작 ===\\n" -- 스킨 팝업 자동 처리 함수 (강화 버전) fn ShowPopup =( local hwnd = (DialogMonitorOPS.GetWindowHandle() ) if hwnd!=0 then( local windowTitle = uiAccessor.getWindowText hwnd local children = uiAccessor.getChildWindows hwnd -- 다이얼로그 타입별 처리 case of ( -- Load Envelopes 다이얼로그 (findString windowTitle "Load Envelopes" != undefined): ( format "Load Envelopes 다이얼로그 감지 - 자동 OK 처리\\n" for c in children do( local buttonText = uiAccessor.getWindowText c if buttonText == "OK" do ( UIAccessor.PressButton c format "OK 버튼 자동 클릭 완료\\n" return true ) ) ) -- Match by Name 다이얼로그 (findString windowTitle "Match by Name" != undefined): ( format "Match by Name 다이얼로그 감지 - 자동 처리\\n" for c in children do( if (uiAccessor.getWindowText c) =="Match by Name" do UIAccessor.PressButton c ) ) -- 기타 다이얼로그들 default: ( -- 일반적인 OK 버튼 찾기 for c in children do( local buttonText = uiAccessor.getWindowText c if buttonText == "OK" do ( format "일반 OK 버튼 감지 - 자동 클릭: %\\n" windowTitle UIAccessor.PressButton c return true ) ) ) ) -- 기본 버튼 처리 uiAccessor.pressDefaultButton() true ) else false ) -- Extra Bones 처리 (2단계에서 실행) stepPerf "Extra Bones 처리 시작" extrasPairs = #() -- 이미 위에서 선언된 변수 재사용 try ( -- ========== 원본 엑스트라 본 유지 방식 ================ local totalExtraCount = 0 for ex in extras do totalExtraCount += ex.count format "\\n=== Extra Bones 처리 시작 ===\\n" format "전체 Extra Bones 개수: %개 (원본 본 그대로 유지)\\n" totalExtraCount if totalExtraCount > 0 then ( bipObj.transform.controller.figureMode = true local idx = 1 for ex in extras do( for i=1 to ex.count do ( local originalBone = ex[i] if not isValidNode originalBone do continue -- Unity Human Bone 이름은 이미 깔끔하므로 그대로 사용 local cleanName = originalBone.name append extrasPairs (datapair originalBone.name cleanName) -- MXPairs는 표준 매핑만 유지 (Extra 본은 추가하지 않음) local currentParent = originalBone.parent format " [%] Extra 본: % (현재 부모: %)" idx originalBone.name (if isValidNode currentParent then currentParent.name else "없음") -- Copy 버전과 동일한 방식: 첫 번째 본만 적절한 Biped 부모에 연결 if i==1 then ( local par = originalBone.parent for pair in MXPairs where isValidNode par and par.name==pair.v1 do ( local bipPar = getNodebyName pair.v2 if isValidNode bipPar do ( -- 원본 본을 Biped 부모에 연결 try ( originalBone.parent = bipPar format " (부모: %)" bipPar.name ) catch ( format " (부모 연결 실패: %)" bipPar.name ) exit ) ) if originalBone.parent == par do format " (부모 연결 안됨)" ) format "\\n" idx += 1 ) ) bipObj.transform.controller.figureMode = false format "\\n=== 원본 엑스트라 본 유지 완료 ===\\n" format "- 각도/위치 완벽 보존\\n" format "- 애니메이션 데이터 손실 없음\\n" format "- 스키닝 품질 유지\\n" -- Extra Bones 처리 결과 format "\\n📊 Extra Bones 처리 결과 요약:\\n" format "✅ 총 %개 엑스트라 본 처리 완료\\n" extrasPairs.count format "✅ 모든 엑스트라 본이 원본 위치/각도 유지\\n" format "✅ 위치 변화: 0 (원본 본 그대로 사용)\\n" format "✅ 각도 변화: 0 (원본 각도 그대로 사용)\\n" format "✅ 애니메이션 데이터: 완벽 보존\\n" format "✅ 스키닝 품질: 원본 유지\\n" format "✅ 표준 본: Biped로 대체됨\\n" format "✅ 엑스트라 본: 원본 유지됨\\n" ) else ( format "Extra Bones가 없습니다.\\n" ) -- 단계 데이터 업데이트 STEP_DATA.extrasPairs = extrasPairs stepPerf "Extra Bones 처리 완료" ) catch ( -- Figure Mode 안전하게 해제 try ( bipObj.transform.controller.figureMode = false ) catch () uiOpt.resume() format "❌ Extra Bones 처리 실패: %\\n" (getCurrentException()) return false ) DialogMonitorOPS.Enabled = true DialogMonitorOPS.Interactive =false DialogMonitorOPS.RegisterNotification ShowPopup id:#UnityHumanToBiped -- 최적화된 스킨 처리 stepPerf "스킨 처리 시작" format "\\n=== 스킨 오브젝트 처리 (최적화 버전) ===\\n" if skinObjects.count > 0 then ( -- 최적화된 스킨 처리 함수 사용 processSkinnedObjectsOptimized skinObjects extrasPairs ) else ( format "처리할 스킨 오브젝트가 없습니다.\\n" ) stepPerf "스킨 처리 완료" format "\\n✅ 모든 스킨 오브젝트 처리 완료 (최적화)\\n" -- 대화상자 모니터 비활성화 DialogMonitorOPS.Enabled = false DialogMonitorOPS.UnRegisterNotification id:#UnityHumanToBiped -- 모퍼 모디파이어 복원 (간소화) if morpherModifiers.count > 0 do ( format "\\n🎭 모퍼 모디파이어 복원 시작...\\n" -- 모퍼 모디파이어를 다시 추가 for modPair in morpherModifiers do ( local obj = modPair.v1 local morphMod = modPair.v2 addModifier obj morphMod ) format "✅ 모퍼 모디파이어 복원 완료\\n" ) -- 하이브리드 방식: 표준 본은 삭제, 엑스트라 본은 유지 format "\\n=== 믹사모 표준 본 삭제 ===\\n" -- 믹사모 루트 본 다시 찾기 local root = getNodeByName "Hips" if not isValidNode root do ( format "❌ Hips를 찾을 수 없어 표준 본 삭제를 건너뜁니다.\\n" root = undefined ) -- Unity Human Bone 완전 제거 (더 확실한 방법) local standardBones = #() local foundBones = #() -- 모든 Unity Human Bone 이름으로 직접 검색 format "Unity Human Bone 검색 중...\\n" local allUnityBoneNames = MXNames + OptionalNames -- 모든 Unity Human Bone 이름 결합 for boneName in allUnityBoneNames do ( local bone = getNodeByName boneName if isValidNode bone then ( appendIfUnique standardBones bone appendIfUnique foundBones boneName format "- 삭제 예정: %\\n" boneName ) ) -- 추가로 계층 구조에서도 검색 (누락 방지) if isValidNode root then ( local hierarchyBones = getHierarchy #(root) #() for bone in hierarchyBones do ( if isValidNode bone and (findItem MXNames bone.name != 0 or findItem OptionalNames bone.name != 0) then ( if findItem foundBones bone.name == 0 do ( -- 중복 방지 appendIfUnique standardBones bone appendIfUnique foundBones bone.name format "- 계층에서 추가 발견: %\\n" bone.name ) ) ) ) format "총 %개 Unity Human Bone 발견\\n" standardBones.count -- Unity Human Bone 삭제 (더 안전하게) if standardBones.count > 0 then ( -- 스킨 의존성이 있는지 마지막 확인 local safeBones = #() for bone in standardBones do ( local hasReferences = false for obj in objects where obj.modifiers[#Skin] != undefined do ( local skn = obj.modifiers[#Skin] -- 본 이름으로 본 ID 찾기 local boneID = 0 local numBones = skinOps.GetNumberBones skn for i = 1 to numBones do ( if (skinOps.GetBoneName skn i 0) == bone.name then ( boneID = i exit ) ) if boneID > 0 do ( hasReferences = true exit ) ) if hasReferences then ( format "⚠️ 스킨 참조 있음, 삭제 건너뜀: %\\n" bone.name ) else ( append safeBones bone ) ) -- 안전한 본들만 삭제 if safeBones.count > 0 then ( try ( delete safeBones format "✅ %개 Unity Human Bone 삭제 완료\\n" safeBones.count ) catch ( format "❌ Unity Human Bone 삭제 중 오류: %\\n" (getCurrentException()) -- 개별 삭제 시도 local successCount = 0 for bone in safeBones do ( try ( delete bone successCount += 1 format "✓ 개별 삭제: %\\n" bone.name ) catch ( format "❌ 개별 삭제 실패: %\\n" bone.name ) ) format "→ %개 본 개별 삭제 성공\\n" successCount ) ) else ( format "삭제 가능한 Unity Human Bone이 없습니다. (모두 스킨 참조 중)\\n" ) ) else ( format "삭제할 Unity Human Bone을 찾을 수 없습니다.\\n" ) -- Unity Human Bone 이름 확인 (이미 표준 이름) format "\\n=== Unity Human Bone 이름 확인 ===\\n" local validCount = 0 for i in extrasPairs do( try ( local theObj = getNodeByName i.v1 if isValidNode theObj then ( format "- % (유효)\\n" theObj.name validCount += 1 ) else ( format "- % → 찾을 수 없음\\n" i.v1 ) ) catch ( format "- % → 접근 오류: %\\n" i.v1 (getCurrentException()) ) ) format "✅ %개 Unity Human Bone 확인 완료\\n" validCount -- 레이어 설정 (안전하게) format "\\n=== 레이어 설정 ===\\n" try ( local rootLayer = LayerManager.newLayerFromName "CHARACTERS" if rootLayer == undefined do rootLayer = LayerManager.getLayerFromName "CHARACTERS" local charLayer = LayerManager.newLayerFromName "CHARACTER" if charLayer == undefined do charLayer = LayerManager.getLayerFromName "CHARACTER" if charLayer != undefined do charLayer.setParent rootLayer local bipLayer = LayerManager.newLayerFromName "BIPED" if bipLayer == undefined do bipLayer = LayerManager.getLayerFromName "BIPED" if bipLayer != undefined and charLayer != undefined do bipLayer.setParent charLayer local GeoLayer = LayerManager.newLayerFromName "GEOMETRY" if GeoLayer == undefined do GeoLayer = LayerManager.getLayerFromName "GEOMETRY" if GeoLayer != undefined and charLayer != undefined do ( GeoLayer.setParent charLayer GeoLayer.isfrozen = true ) format "✅ 레이어 설정 완료\\n" ) catch ( format "❌ 레이어 설정 중 오류: %\\n" (getCurrentException()) format "레이어 설정을 건너뜁니다.\\n" ) -- 오브젝트 관리 (안전하게) format "\\n=== 오브젝트 관리 ===\\n" -- 스킨 오브젝트 처리 try ( if skinObjects != undefined and skinObjects.count > 0 then ( for i in skinObjects do ( if isValidNode i do ( try ( i.showFrozenInGray = false -- 메시와 머티리얼 이름은 원래대로 유지 if GeoLayer != undefined do GeoLayer.addnode i ) catch ( format "스킨 오브젝트 % 처리 중 오류: %\\n" i.name (getCurrentException()) ) ) ) format "✅ %개 스킨 오브젝트 처리 완료\\n" skinObjects.count ) else ( format "처리할 스킨 오브젝트가 없습니다.\\n" ) ) catch ( format "❌ 스킨 오브젝트 관리 중 오류: %\\n" (getCurrentException()) ) -- Biped 오브젝트 처리 try ( if isValidNode bipObj then ( local allBip = getHierarchy bipObj #() for i in allBip do( if isValidNode i do ( try ( if bipLayer != undefined do bipLayer.addNode i -- Biped 본 이름은 표준 이름 그대로 유지 (Bip001 Pelvis, etc.) i.renderable = false i.boxMode = true ) catch ( format "Biped 오브젝트 % 처리 중 오류: %\\n" i.name (getCurrentException()) ) ) ) format "✅ %개 Biped 오브젝트 처리 완료\\n" allBip.count ) else ( format "❌ Biped 오브젝트를 찾을 수 없습니다.\\n" ) ) catch ( format "❌ Biped 오브젝트 관리 중 오류: %\\n" (getCurrentException()) ) -- 2단계 성공 반환 stepPerf "2단계 완료" -- 메모리 정리 및 성능 복원 (3ds Max 느려짐 방지) local memOpt = MemoryOptimizer() memOpt.deepCleanup() -- UI 업데이트 재개 및 최종 화면 갱신 uiOpt.resume() -- 성능 결과 출력 totalPerf format "\\n🎉 2단계 완료! 🎉\\n" format "✅ 스킨 처리 완료\\n" format "✅ 모퍼 복원 완료\\n" format "✅ 정리 작업 완료\\n" format "\\n🎉 전체 변환 완료! 🎉\\n" format "캐릭터: %\\n" charName format "✅ Biped 로컬 축: 표준 유지 (축 뒤틀림 없음)\\n" format "✅ T-Pose 자세: 기존 알고리즘으로 매칭\\n" format "✅ 애니메이션 호환성: 완벽\\n" format "레이어: CHARACTERS > %\\n" (toUpper charName) -- 단계 데이터 정리 STEP_DATA = undefined redrawViews() return true ) catch ( -- 에러 처리: UI 복원 및 에러 메시지 if uiOpt != undefined do uiOpt.resume() format "\\n❌ 2단계 변환 중 오류 발생!\\n" format "오류 내용: %\\n" (getCurrentException()) STEP_DATA = undefined return false ) ) -- ================================== 단계별 변환 UI =============================================== -- 단계별 변환 UI rollout UnityHumanBipedConverter_UI "Unity Human to Biped 변환기 (단계별)" width:400 height:650 ( -- 제목 및 상태 표시 group "📋 변환 상태" ( label lblStatus "⏳ 준비 대기 중..." style_sunkenedge:true height:25 label lblProgress "" height:20 ) -- 설정 group "⚙️ 설정" ( label lblNote "기본 이름 사용: Bip001, BIPED 레이어, GEOMETRY 레이어" style_sunkenedge:true height:25 checkbox chkAlwaysDeform "Always Deform 적용" checked:false ) -- 1단계 버튼 group "🚀 1단계: Biped 생성 및 기본 설정 (빠름)" ( label lbl1Info "• 전처리 및 검증\n• Biped 생성 및 구조 수정\n• 비율 매칭\n• Extra Bones 수집" height:60 style_sunkenedge:true button btnStep1 "1단계 실행" width:350 height:35 color:green ) -- 2단계 버튼 group "⏳ 2단계: Extra Bones + 스킨 처리 (시간 소요)" ( label lbl2Info "• Extra Bones 연결 및 처리\n• 스킨 웨이트 전송 (오래 걸림)\n• 모퍼 모디파이어 복원\n• 원본 본 삭제 및 최종 정리" height:70 style_sunkenedge:true button btnStep2 "2단계 실행" width:350 height:35 color:orange enabled:false ) -- 전체 실행 버튼 group "⚡ 통합 실행" ( button btnFullConvert "전체 변환 (1단계 + 2단계)" width:350 height:30 color:blue ) -- 정보 표시 group "ℹ️ 도움말" ( label lblHelp "1. Unity Human Bone 이름의 아바타를 임포트하세요\n2. 단계별로 실행하거나 전체 실행하세요\n• Unity Human Bone → 3ds Max Biped 변환\n• 필요한 본: Hips, Spine, LeftUpperArm, RightUpperArm 등" height:60 style_sunkenedge:true ) -- 초기화 on UnityHumanBipedConverter_UI open do ( -- Unity Human Bone 오브젝트 확인 local humanBoneObjects = #() try ( local hipsObj = getNodeByName "Hips" if isValidNode hipsObj do append humanBoneObjects hipsObj local spineObj = getNodeByName "Spine" if isValidNode spineObj do append humanBoneObjects spineObj local leftArmObj = getNodeByName "LeftUpperArm" if isValidNode leftArmObj do append humanBoneObjects leftArmObj local rightArmObj = getNodeByName "RightUpperArm" if isValidNode rightArmObj do append humanBoneObjects rightArmObj ) catch () if humanBoneObjects.count > 0 then ( lblStatus.text = "✅ Unity Human Bone 오브젝트 " + (humanBoneObjects.count as string) + "개 발견 - 변환 준비 완료" ) else ( lblStatus.text = "❌ Unity Human Bone 오브젝트를 찾을 수 없음 - 올바른 본 이름 필요" btnStep1.enabled = false btnFullConvert.enabled = false ) ) -- 1단계 실행 on btnStep1 pressed do ( lblStatus.text = "⏳ 1단계 실행 중..." btnStep1.enabled = false try ( local result = ConvertUnityHumanToBiped_Step1 charName:"Character" alwaysDeform:chkAlwaysDeform.checked if result then ( lblStatus.text = "✅ 1단계 완료 - 2단계 실행 가능" btnStep2.enabled = true lblProgress.text = "✅ Biped 생성 및 구조 설정 완료 (Extra Bones는 2단계에서 처리)" ) else ( lblStatus.text = "❌ 1단계 실패" btnStep1.enabled = true ) ) catch ( lblStatus.text = "❌ 1단계 오류 발생" btnStep1.enabled = true messagebox ("1단계 오류: " + (getCurrentException() as string)) ) ) -- 2단계 실행 on btnStep2 pressed do ( lblStatus.text = "⏳ 2단계 실행 중... (시간 소요)" btnStep2.enabled = false try ( local result = ConvertUnityHumanToBiped_Step2() if result then ( lblStatus.text = "🎉 전체 변환 완료!" lblProgress.text = "🎉 모든 변환 작업 완료!" btnStep1.enabled = true btnStep2.enabled = false ) else ( lblStatus.text = "❌ 2단계 실패" btnStep2.enabled = true ) ) catch ( lblStatus.text = "❌ 2단계 오류 발생" btnStep2.enabled = true messagebox ("2단계 오류: " + (getCurrentException() as string)) ) ) -- 전체 변환 on btnFullConvert pressed do ( lblStatus.text = "⏳ 전체 변환 실행 중..." btnFullConvert.enabled = false btnStep1.enabled = false try ( -- 1단계 실행 lblProgress.text = "⏳ 1단계 실행 중..." local result1 = ConvertUnityHumanToBiped_Step1 charName:"Character" alwaysDeform:chkAlwaysDeform.checked if result1 then ( lblProgress.text = "✅ 1단계 완료 - ⏳ 2단계 실행 중..." -- 2단계 실행 local result2 = ConvertUnityHumanToBiped_Step2() if result2 then ( lblStatus.text = "🎉 전체 변환 완료!" lblProgress.text = "🎉 모든 변환 작업 완료!" ) else ( lblStatus.text = "❌ 2단계 실패" ) ) else ( lblStatus.text = "❌ 1단계 실패" ) btnFullConvert.enabled = true btnStep1.enabled = true ) catch ( lblStatus.text = "❌ 변환 중 오류 발생" btnFullConvert.enabled = true btnStep1.enabled = true messagebox ("변환 오류: " + (getCurrentException() as string)) ) ) ) -- UI 표시 createDialog UnityHumanBipedConverter_UI -- 스크립트 로딩 완료 메시지 format "\n⚡ UnityHumanToBiped Converter (슈퍼 최적화 버전) 로딩 완료! ⚡\n" format "• 단계별 변환 UI가 열렸습니다\n" format "• Unity Human Bone 이름 → 3ds Max Biped 변환\n" format "• 기본 이름 사용: Bip001, BIPED 레이어, GEOMETRY 레이어\n" format "• 슈퍼 UI 최적화 + 메모리 정리로 3ds Max 느려짐 방지\n"