635 lines
28 KiB
C#
635 lines
28 KiB
C#
using UnityEngine;
|
||
using UnityEditor;
|
||
using VRM;
|
||
using System.Collections.Generic;
|
||
using System.Linq;
|
||
using System.Reflection;
|
||
using System;
|
||
|
||
namespace KindScript
|
||
{
|
||
public class VRMFaceAssetMaker : EditorWindow
|
||
{
|
||
private BlendShapeAvatar blendShapeAvatar;
|
||
private List<SkinnedMeshRenderer> skinnedMeshRenderers = new List<SkinnedMeshRenderer>();
|
||
private Vector2 scrollPosition;
|
||
private bool createVRMPresets = true;
|
||
private bool createARKitPresets = true;
|
||
|
||
// 언어 설정
|
||
private enum Language { English, Japanese, Korean }
|
||
private Language currentLanguage = Language.Korean;
|
||
private static readonly Dictionary<string, Dictionary<Language, string>> Translations = new Dictionary<string, Dictionary<Language, string>>
|
||
{
|
||
// 타이틀
|
||
{"title", new Dictionary<Language, string> {
|
||
{Language.English, "VRM BlendShape Settings"},
|
||
{Language.Japanese, "VRM BlendShape 設定"},
|
||
{Language.Korean, "VRM BlendShape 설정"}
|
||
}},
|
||
// BlendShape Avatar 관련
|
||
{"avatar_section", new Dictionary<Language, string> {
|
||
{Language.English, "BlendShape Avatar"},
|
||
{Language.Japanese, "BlendShape アバター"},
|
||
{Language.Korean, "BlendShape 아바타"}
|
||
}},
|
||
{"create_new", new Dictionary<Language, string> {
|
||
{Language.English, "Create New"},
|
||
{Language.Japanese, "新規作成"},
|
||
{Language.Korean, "새로 만들기"}
|
||
}},
|
||
// Renderer 관련
|
||
{"renderer_section", new Dictionary<Language, string> {
|
||
{Language.English, "Skinned Mesh Renderers"},
|
||
{Language.Japanese, "スキンメッシュレンダラー"},
|
||
{Language.Korean, "스킨드 메시 렌더러"}
|
||
}},
|
||
{"renderer_label", new Dictionary<Language, string> {
|
||
{Language.English, "Renderer"},
|
||
{Language.Japanese, "レンダラー"},
|
||
{Language.Korean, "렌더러"}
|
||
}},
|
||
{"add_renderer", new Dictionary<Language, string> {
|
||
{Language.English, "+ Add Renderer"},
|
||
{Language.Japanese, "+ レンダラー追加"},
|
||
{Language.Korean, "+ 렌더러 추가"}
|
||
}},
|
||
{"remove", new Dictionary<Language, string> {
|
||
{Language.English, "Remove"},
|
||
{Language.Japanese, "削除"},
|
||
{Language.Korean, "제거"}
|
||
}},
|
||
// 프리셋 관련
|
||
{"preset_section", new Dictionary<Language, string> {
|
||
{Language.English, "Select Presets to Create"},
|
||
{Language.Japanese, "作成するプリセットを選択"},
|
||
{Language.Korean, "생성할 프리셋 선택"}
|
||
}},
|
||
{"vrm_preset", new Dictionary<Language, string> {
|
||
{Language.English, "VRM Basic Presets"},
|
||
{Language.Japanese, "VRM 基本プリセット"},
|
||
{Language.Korean, "VRM 기본 프리셋"}
|
||
}},
|
||
{"vrm_preset_desc", new Dictionary<Language, string> {
|
||
{Language.English, "Basic expressions (A,I,U,E,O)\nEmotions (Joy, Angry, Sorrow)\nEye movements"},
|
||
{Language.Japanese, "基本表情 (あ,い,う,え,お)\n感情 (喜び,怒り,悲しみ)\n視線移動"},
|
||
{Language.Korean, "기본 표정 (아,이,우,에,오)\n감정 표현 (기쁨, 화남, 슬픔)\n시선 이동"}
|
||
}},
|
||
{"arkit_preset", new Dictionary<Language, string> {
|
||
{Language.English, "ARKit Presets"},
|
||
{Language.Japanese, "ARKit プリセット"},
|
||
{Language.Korean, "ARKit 프리셋"}
|
||
}},
|
||
{"arkit_preset_desc", new Dictionary<Language, string> {
|
||
{Language.English, "52 ARKit BlendShapes\nEyes, Eyebrows, Mouth, Cheeks etc."},
|
||
{Language.Japanese, "52個のARKit BlendShape\n目、眉、口、頬など"},
|
||
{Language.Korean, "52개의 ARKit 블렌드쉐입\n눈, 눈썹, 입, 볼 등"}
|
||
}},
|
||
{"create_button", new Dictionary<Language, string> {
|
||
{Language.English, "Create BlendShape Clips"},
|
||
{Language.Japanese, "BlendShape クリップを作成"},
|
||
{Language.Korean, "BlendShape 클립 생성"}
|
||
}},
|
||
// 에러 메시지
|
||
{"error_no_avatar", new Dictionary<Language, string> {
|
||
{Language.English, "Please select a BlendShape Avatar."},
|
||
{Language.Japanese, "BlendShape アバターを選択してください。"},
|
||
{Language.Korean, "BlendShape 아바타를 선택해주세요."}
|
||
}},
|
||
{"error_no_renderer", new Dictionary<Language, string> {
|
||
{Language.English, "Please set all Skinned Mesh Renderers."},
|
||
{Language.Japanese, "すべてのスキンメッシュレンダラーを設定してください。"},
|
||
{Language.Korean, "모든 스킨드 메시 렌더러를 설정해주세요."}
|
||
}},
|
||
// 로그 메시지
|
||
{"log_clip_exists", new Dictionary<Language, string> {
|
||
{Language.English, "Clip already exists: "},
|
||
{Language.Japanese, "クリップが既に存在します:"},
|
||
{Language.Korean, "클립이 이미 존재합니다: "}
|
||
}},
|
||
{"log_clip_created", new Dictionary<Language, string> {
|
||
{Language.English, "Clip created: "},
|
||
{Language.Japanese, "クリップを作成しました:"},
|
||
{Language.Korean, "클립이 생성되었습니다: "}
|
||
}},
|
||
{"log_blendshape_not_found", new Dictionary<Language, string> {
|
||
{Language.English, "BlendShape not found in any mesh: "},
|
||
{Language.Japanese, "どのメッシュにもBlendShapeが見つかりません:"},
|
||
{Language.Korean, "어떤 메시에서도 블렌드쉐입을 찾을 수 없습니다: "}
|
||
}},
|
||
// 확인 대화상자 메시지
|
||
{"confirm_create", new Dictionary<Language, string> {
|
||
{Language.English, "Are you sure you want to create BlendShape clips?"},
|
||
{Language.Japanese, "BlendShapeクリップを作成してもよろしいですか?"},
|
||
{Language.Korean, "BlendShape 클립을 생성하시겠습니까?"}
|
||
}},
|
||
{"confirm_yes", new Dictionary<Language, string> {
|
||
{Language.English, "Create"},
|
||
{Language.Japanese, "作成"},
|
||
{Language.Korean, "생성"}
|
||
}},
|
||
{"confirm_no", new Dictionary<Language, string> {
|
||
{Language.English, "Cancel"},
|
||
{Language.Japanese, "キャンセル"},
|
||
{Language.Korean, "취소"}
|
||
}},
|
||
{"creation_complete", new Dictionary<Language, string> {
|
||
{Language.English, "BlendShape clips have been created successfully!\n\nCreated Clips: {0}"},
|
||
{Language.Japanese, "BlendShapeクリップの作成が完了しました!\n\n作成されたクリップ:{0}"},
|
||
{Language.Korean, "BlendShape 클립이 성공적으로 생성되었습니다!\n\n생성된 클립: {0}"}
|
||
}},
|
||
{"creation_title", new Dictionary<Language, string> {
|
||
{Language.English, "Creation Complete"},
|
||
{Language.Japanese, "作成完了"},
|
||
{Language.Korean, "생성 완료"}
|
||
}},
|
||
{"guide_docs", new Dictionary<Language, string> {
|
||
{Language.English, "Documentation"},
|
||
{Language.Japanese, "ドキュメント"},
|
||
{Language.Korean, "문서"}
|
||
}},
|
||
{"guide_video", new Dictionary<Language, string> {
|
||
{Language.English, "Video Tutorial"},
|
||
{Language.Japanese, "ビデオチュートリアル"},
|
||
{Language.Korean, "비디오 튜토리얼"}
|
||
}}
|
||
};
|
||
|
||
// 가이드 URL 추가
|
||
private static readonly Dictionary<Language, (string docs, string video)> GuideUrls = new Dictionary<Language, (string, string)>
|
||
{
|
||
{ Language.English, (
|
||
"https://vrm.dev/en/docs/univrm/blendshape/univrm_blendshape/",
|
||
"https://www.youtube.com/watch?v=example_en")
|
||
},
|
||
{ Language.Japanese, (
|
||
"https://vrm.dev/docs/univrm/blendshape/univrm_blendshape/",
|
||
"https://www.youtube.com/watch?v=example_jp")
|
||
},
|
||
{ Language.Korean, (
|
||
"https://vrm.dev/ko/docs/univrm/blendshape/univrm_blendshape/",
|
||
"https://www.youtube.com/watch?v=example_kr")
|
||
}
|
||
};
|
||
|
||
private string GetText(string key)
|
||
{
|
||
if (Translations.ContainsKey(key))
|
||
{
|
||
return Translations[key][currentLanguage];
|
||
}
|
||
return $"Missing:{key}";
|
||
}
|
||
|
||
[MenuItem("Tools/VRM/Create BlendShape Assets")]
|
||
static void ShowWindow()
|
||
{
|
||
var window = GetWindow<VRMFaceAssetMaker>("VRM BlendShape Maker");
|
||
// 최소 창 크기 설정 (너비, 높이)
|
||
window.minSize = new Vector2(500, 450);
|
||
}
|
||
|
||
void OnGUI()
|
||
{
|
||
// 최소 창 크기 설정
|
||
float minWindowWidth = 300;
|
||
float currentWidth = EditorGUIUtility.currentViewWidth;
|
||
float usableWidth = Mathf.Max(minWindowWidth, currentWidth);
|
||
|
||
// 스크롤 시작
|
||
scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition);
|
||
|
||
// 전체 여백 설정
|
||
EditorGUILayout.BeginVertical(new GUIStyle { padding = new RectOffset(10, 10, 10, 10) });
|
||
|
||
// 언어 선택 드롭다운과 가이드 버튼을 같은 행에 배치
|
||
EditorGUILayout.BeginHorizontal();
|
||
|
||
// 언어 선택 부분을 텍스트 길이에 맞게 동적으로 설정
|
||
EditorGUILayout.BeginHorizontal();
|
||
var languageLabelContent = new GUIContent("Language");
|
||
float languageLabelWidth = EditorStyles.label.CalcSize(languageLabelContent).x + 5;
|
||
EditorGUILayout.LabelField(languageLabelContent, GUILayout.Width(languageLabelWidth));
|
||
|
||
// 현재 선택된 언어의 텍스트 길이 계산
|
||
var enumContent = new GUIContent(currentLanguage.ToString());
|
||
float enumWidth = EditorStyles.popup.CalcSize(enumContent).x + 30; // 드롭다운 화살표를 위한 여유 공간
|
||
currentLanguage = (Language)EditorGUILayout.EnumPopup(currentLanguage, GUILayout.Width(enumWidth));
|
||
EditorGUILayout.EndHorizontal();
|
||
|
||
GUILayout.FlexibleSpace();
|
||
|
||
// 가이드 버튼들
|
||
var buttonStyle = new GUIStyle(GUI.skin.button);
|
||
buttonStyle.padding = new RectOffset(4, 4, 4, 4);
|
||
buttonStyle.fixedHeight = EditorGUIUtility.singleLineHeight + 5;
|
||
|
||
var urls = GuideUrls[currentLanguage];
|
||
|
||
// 문서 버튼의 내용과 크기 계산
|
||
var docsContent = new GUIContent(GetText("guide_docs"), EditorGUIUtility.IconContent("_Help").image);
|
||
var docsSize = buttonStyle.CalcSize(docsContent);
|
||
|
||
// 비디오 버튼의 내용과 크기 계산 - 아이콘 변경
|
||
var videoContent = new GUIContent(GetText("guide_video"), EditorGUIUtility.IconContent("d_PlayButton").image);
|
||
var videoSize = buttonStyle.CalcSize(videoContent);
|
||
|
||
// 두 버튼 중 더 큰 너비를 사용
|
||
float guideButtonWidth = Mathf.Max(docsSize.x, videoSize.x);
|
||
|
||
// 문서 버튼
|
||
if (GUILayout.Button(docsContent, buttonStyle, GUILayout.Width(guideButtonWidth)))
|
||
{
|
||
Application.OpenURL(urls.docs);
|
||
}
|
||
|
||
GUILayout.Space(5);
|
||
|
||
// 비디오 버튼
|
||
if (GUILayout.Button(videoContent, buttonStyle, GUILayout.Width(guideButtonWidth)))
|
||
{
|
||
Application.OpenURL(urls.video);
|
||
}
|
||
|
||
EditorGUILayout.EndHorizontal();
|
||
|
||
EditorGUILayout.Space(15); // 상단 여백 증가
|
||
|
||
// 타이틀 섹션
|
||
GUILayout.Label(GetText("title"), EditorStyles.boldLabel);
|
||
EditorGUILayout.Space(10); // 타이틀 아래 여백 증가
|
||
|
||
// BlendShape Avatar 섹션
|
||
using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox))
|
||
{
|
||
EditorGUILayout.LabelField(GetText("avatar_section"), EditorStyles.boldLabel);
|
||
EditorGUILayout.Space(5); // 섹션 내부 여백 증가
|
||
|
||
using (new EditorGUILayout.HorizontalScope())
|
||
{
|
||
// Avatar 라벨을 고정 너비로 설정
|
||
EditorGUILayout.LabelField("Avatar", GUILayout.Width(45));
|
||
|
||
// Avatar 필드가 남은 공간을 차지하되, 버튼을 위한 공간 확보
|
||
blendShapeAvatar = EditorGUILayout.ObjectField(
|
||
GUIContent.none,
|
||
blendShapeAvatar,
|
||
typeof(BlendShapeAvatar),
|
||
true,
|
||
GUILayout.ExpandWidth(true)) as BlendShapeAvatar;
|
||
|
||
// 버튼과 Avatar 필드 사이의 여백을 최소화
|
||
GUILayout.Space(2);
|
||
|
||
// 버튼은 고정된 크기 유지
|
||
if (blendShapeAvatar == null)
|
||
{
|
||
if (GUILayout.Button(GetText("create_new"), GUILayout.Width(80)))
|
||
{
|
||
CreateNewBlendShapeAvatar();
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
EditorGUILayout.Space(15); // 섹션 간 여백 증가
|
||
|
||
// Skinned Mesh Renderers 섹션
|
||
using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox))
|
||
{
|
||
EditorGUILayout.LabelField(GetText("renderer_section"), EditorStyles.boldLabel);
|
||
EditorGUILayout.Space(2);
|
||
|
||
for (int i = 0; i < skinnedMeshRenderers.Count; i++)
|
||
{
|
||
using (new EditorGUILayout.HorizontalScope())
|
||
{
|
||
// 렌더러 라벨의 내용 미리 계산
|
||
var labelContent = new GUIContent($"{GetText("renderer_label")} {i + 1}");
|
||
// 라벨의 필요한 너비 계산 (여유 공간 10 추가)
|
||
float labelWidth = EditorStyles.label.CalcSize(labelContent).x + 10;
|
||
|
||
// 라벨을 계산된 너비로 표시
|
||
EditorGUILayout.LabelField(labelContent, GUILayout.Width(labelWidth));
|
||
|
||
// Object 필드가 남은 공간을 차지하되 최소 너비 보장
|
||
skinnedMeshRenderers[i] = EditorGUILayout.ObjectField(
|
||
GUIContent.none,
|
||
skinnedMeshRenderers[i],
|
||
typeof(SkinnedMeshRenderer),
|
||
true,
|
||
GUILayout.MinWidth(150),
|
||
GUILayout.ExpandWidth(true)
|
||
) as SkinnedMeshRenderer;
|
||
|
||
// 제거 버튼의 텍스트 너비 계산
|
||
var removeContent = new GUIContent(GetText("remove"));
|
||
float removeWidth = GUI.skin.button.CalcSize(removeContent).x + 10;
|
||
|
||
// 약간의 간격 추가
|
||
GUILayout.Space(2);
|
||
|
||
// 제거 버튼을 텍스트 크기에 맞춰 표시
|
||
if (GUILayout.Button(removeContent, GUILayout.Width(removeWidth)))
|
||
{
|
||
skinnedMeshRenderers.RemoveAt(i);
|
||
i--;
|
||
}
|
||
}
|
||
}
|
||
|
||
EditorGUILayout.Space(2);
|
||
|
||
// 렌더러 추가 버튼
|
||
using (new EditorGUILayout.HorizontalScope())
|
||
{
|
||
float addRendererButtonWidth = Mathf.Clamp(usableWidth - 40, 150, 300);
|
||
GUILayout.FlexibleSpace();
|
||
if (GUILayout.Button(GetText("add_renderer"), GUILayout.Width(addRendererButtonWidth)))
|
||
{
|
||
skinnedMeshRenderers.Add(null);
|
||
}
|
||
GUILayout.FlexibleSpace();
|
||
}
|
||
}
|
||
|
||
EditorGUILayout.Space(15); // 섹션 간 여백 증가
|
||
|
||
// 프리셋 섹션은 항상 수직으로 표시
|
||
using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox))
|
||
{
|
||
EditorGUILayout.LabelField(GetText("preset_section"), EditorStyles.boldLabel);
|
||
EditorGUILayout.Space(2);
|
||
|
||
// VRM 프리셋 박스
|
||
using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox))
|
||
{
|
||
createVRMPresets = EditorGUILayout.ToggleLeft(
|
||
new GUIContent(GetText("vrm_preset")),
|
||
createVRMPresets);
|
||
|
||
if (createVRMPresets)
|
||
{
|
||
EditorGUI.indentLevel++;
|
||
var content = new GUIContent(GetText("vrm_preset_desc"));
|
||
float width = usableWidth - 40;
|
||
var height = EditorStyles.miniLabel.CalcHeight(content, width);
|
||
EditorGUILayout.LabelField(content, EditorStyles.miniLabel, GUILayout.Height(height));
|
||
EditorGUI.indentLevel--;
|
||
}
|
||
}
|
||
|
||
// ARKit 프리셋 박스
|
||
using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox))
|
||
{
|
||
createARKitPresets = EditorGUILayout.ToggleLeft(
|
||
new GUIContent(GetText("arkit_preset")),
|
||
createARKitPresets);
|
||
|
||
if (createARKitPresets)
|
||
{
|
||
EditorGUI.indentLevel++;
|
||
var content = new GUIContent(GetText("arkit_preset_desc"));
|
||
float width = usableWidth - 40;
|
||
var height = EditorStyles.miniLabel.CalcHeight(content, width);
|
||
EditorGUILayout.LabelField(content, EditorStyles.miniLabel, GUILayout.Height(height));
|
||
EditorGUI.indentLevel--;
|
||
}
|
||
}
|
||
}
|
||
|
||
EditorGUILayout.Space(15);
|
||
|
||
// 생성 버튼
|
||
using (new EditorGUILayout.HorizontalScope())
|
||
{
|
||
GUILayout.FlexibleSpace();
|
||
using (new EditorGUI.DisabledGroupScope(!IsValid()))
|
||
{
|
||
float createButtonWidth = Mathf.Clamp(usableWidth - 40, 150, 300);
|
||
if (GUILayout.Button(GetText("create_button"), GUILayout.Width(createButtonWidth), GUILayout.Height(30)))
|
||
{
|
||
CreateBlendShapeClips();
|
||
}
|
||
}
|
||
GUILayout.FlexibleSpace();
|
||
}
|
||
|
||
EditorGUILayout.EndVertical();
|
||
|
||
// 스크롤 종료
|
||
EditorGUILayout.EndScrollView();
|
||
}
|
||
|
||
private bool IsValid()
|
||
{
|
||
return blendShapeAvatar != null &&
|
||
skinnedMeshRenderers.Count > 0 &&
|
||
!skinnedMeshRenderers.Any(smr => smr == null) &&
|
||
(createVRMPresets || createARKitPresets);
|
||
}
|
||
|
||
void CreateBlendShapeClips()
|
||
{
|
||
if (blendShapeAvatar == null)
|
||
{
|
||
EditorUtility.DisplayDialog("Error", GetText("error_no_avatar"), "OK");
|
||
return;
|
||
}
|
||
|
||
if (skinnedMeshRenderers.Count == 0 || skinnedMeshRenderers.Any(smr => smr == null))
|
||
{
|
||
EditorUtility.DisplayDialog("Error", GetText("error_no_renderer"), "OK");
|
||
return;
|
||
}
|
||
|
||
// 확인 대화상자 표시
|
||
bool proceed = EditorUtility.DisplayDialog(
|
||
GetText("title"),
|
||
GetText("confirm_create"),
|
||
GetText("confirm_yes"),
|
||
GetText("confirm_no")
|
||
);
|
||
|
||
if (!proceed) return;
|
||
|
||
// 생성 통계
|
||
int createdClips = 0;
|
||
|
||
try
|
||
{
|
||
EditorUtility.DisplayProgressBar(GetText("title"), GetText("create_button"), 0f);
|
||
|
||
string avatarPath = AssetDatabase.GetAssetPath(blendShapeAvatar);
|
||
string saveFolder = System.IO.Path.GetDirectoryName(avatarPath);
|
||
|
||
// VRM 프리셋 생성
|
||
if (createVRMPresets)
|
||
{
|
||
EditorUtility.DisplayProgressBar(GetText("title"), "VRM Presets", 0.3f);
|
||
var vrmPresets = new[] {
|
||
(BlendShapePreset.A, "A"), (BlendShapePreset.I, "I"),
|
||
(BlendShapePreset.U, "U"), (BlendShapePreset.E, "E"),
|
||
(BlendShapePreset.O, "O"), (BlendShapePreset.Blink, "Blink"),
|
||
(BlendShapePreset.Joy, "Joy"), (BlendShapePreset.Angry, "Angry"),
|
||
(BlendShapePreset.Sorrow, "Sorrow"), (BlendShapePreset.Fun, "Fun"),
|
||
(BlendShapePreset.LookUp, "LookUp"), (BlendShapePreset.LookDown, "LookDown"),
|
||
(BlendShapePreset.LookLeft, "LookLeft"), (BlendShapePreset.LookRight, "LookRight"),
|
||
(BlendShapePreset.Blink_L, "Blink_L"), (BlendShapePreset.Blink_R, "Blink_R")
|
||
};
|
||
|
||
foreach (var (preset, name) in vrmPresets)
|
||
{
|
||
bool created = CreatePresetClip(preset, name);
|
||
if (created) createdClips++;
|
||
}
|
||
}
|
||
|
||
// ARKit 프리셋 생성
|
||
if (createARKitPresets)
|
||
{
|
||
EditorUtility.DisplayProgressBar(GetText("title"), "ARKit Presets", 0.6f);
|
||
var arkitPresets = new[] {
|
||
"browInnerUp", "browDownLeft", "browDownRight",
|
||
"browOuterUpLeft", "browOuterUpRight", "eyeLookUpLeft",
|
||
"eyeLookUpRight", "eyeLookDownLeft", "eyeLookDownRight",
|
||
"eyeLookInLeft", "eyeLookInRight", "eyeLookOutLeft",
|
||
"eyeLookOutRight", "eyeBlinkLeft", "eyeBlinkRight",
|
||
"eyeSquintLeft", "eyeSquintRight", "eyeWideLeft",
|
||
"eyeWideRight", "cheekPuff", "cheekSquintLeft",
|
||
"cheekSquintRight", "noseSneerLeft", "noseSneerRight",
|
||
"jawOpen", "jawForward", "jawLeft", "jawRight",
|
||
"mouthFunnel", "mouthPucker", "mouthLeft", "mouthRight",
|
||
"mouthRollUpper", "mouthRollLower", "mouthShrugUpper",
|
||
"mouthShrugLower", "mouthClose", "mouthSmileLeft",
|
||
"mouthSmileRight", "mouthFrownLeft", "mouthFrownRight",
|
||
"mouthDimpleLeft", "mouthDimpleRight", "mouthUpperUpLeft",
|
||
"mouthUpperUpRight", "mouthLowerDownLeft", "mouthLowerDownRight",
|
||
"mouthPressLeft", "mouthPressRight", "mouthStretchLeft",
|
||
"mouthStretchRight", "tongueOut"
|
||
};
|
||
|
||
foreach (var name in arkitPresets)
|
||
{
|
||
bool created = CreatePresetClip(BlendShapePreset.Unknown, name);
|
||
if (created) createdClips++;
|
||
}
|
||
}
|
||
|
||
EditorUtility.SetDirty(blendShapeAvatar);
|
||
AssetDatabase.SaveAssets();
|
||
AssetDatabase.Refresh();
|
||
|
||
// 완료 메시지 표시 - 스킵된 클립 정보 제거
|
||
EditorUtility.DisplayDialog(
|
||
GetText("creation_title"),
|
||
string.Format(GetText("creation_complete"), createdClips),
|
||
"OK"
|
||
);
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
Debug.LogError($"Error creating BlendShape clips: {e.Message}\n{e.StackTrace}");
|
||
EditorUtility.DisplayDialog("Error", e.Message, "OK");
|
||
}
|
||
finally
|
||
{
|
||
EditorUtility.ClearProgressBar();
|
||
}
|
||
}
|
||
|
||
private bool CreatePresetClip(BlendShapePreset preset, string blendShapeName)
|
||
{
|
||
string avatarPath = AssetDatabase.GetAssetPath(blendShapeAvatar);
|
||
string saveFolder = System.IO.Path.GetDirectoryName(avatarPath);
|
||
var path = $"{saveFolder}/{blendShapeName.ToLower()}.asset";
|
||
|
||
// 이미 존재하는 클립 확인
|
||
var existingClip = AssetDatabase.LoadAssetAtPath<BlendShapeClip>(path);
|
||
if (existingClip != null)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
// 새로운 BlendShapeClip 생성
|
||
var clip = ScriptableObject.CreateInstance<BlendShapeClip>();
|
||
clip.BlendShapeName = blendShapeName;
|
||
clip.Preset = preset;
|
||
|
||
var bindings = new List<BlendShapeBinding>();
|
||
|
||
foreach (var skinnedMeshRenderer in skinnedMeshRenderers)
|
||
{
|
||
// SkinnedMeshRenderer에서 블렌드쉐입 인덱스 찾기
|
||
int blendShapeIndex = -1;
|
||
Mesh mesh = skinnedMeshRenderer.sharedMesh;
|
||
|
||
// 블렌드쉐입 이름 비교 시 대소문자 구분 없이 포함 여부 확인
|
||
for (int i = 0; i < mesh.blendShapeCount; i++)
|
||
{
|
||
string shapeName = mesh.GetBlendShapeName(i);
|
||
if (shapeName.ToLower().Contains(blendShapeName.ToLower()))
|
||
{
|
||
blendShapeIndex = i;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (blendShapeIndex != -1)
|
||
{
|
||
Transform avatarRoot = skinnedMeshRenderer.transform.root;
|
||
string relativePath = "";
|
||
|
||
Transform current = skinnedMeshRenderer.transform;
|
||
while (current != avatarRoot && current != null)
|
||
{
|
||
if (string.IsNullOrEmpty(relativePath))
|
||
relativePath = current.name;
|
||
else
|
||
relativePath = current.name + "/" + relativePath;
|
||
current = current.parent;
|
||
}
|
||
|
||
float weight = preset != BlendShapePreset.Unknown ? 0f : 100f;
|
||
|
||
bindings.Add(new BlendShapeBinding
|
||
{
|
||
RelativePath = relativePath,
|
||
Index = blendShapeIndex,
|
||
Weight = weight
|
||
});
|
||
}
|
||
}
|
||
|
||
if (bindings.Count > 0)
|
||
{
|
||
clip.Values = bindings.ToArray();
|
||
AssetDatabase.CreateAsset(clip, path);
|
||
|
||
var clips = new List<BlendShapeClip>();
|
||
if (blendShapeAvatar.Clips != null)
|
||
{
|
||
clips.AddRange(blendShapeAvatar.Clips);
|
||
}
|
||
clips.Add(clip);
|
||
blendShapeAvatar.Clips = clips;
|
||
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
private void CreateNewBlendShapeAvatar()
|
||
{
|
||
// 새로운 BlendShapeAvatar 생성
|
||
var avatar = ScriptableObject.CreateInstance<BlendShapeAvatar>();
|
||
|
||
// ProjectWindowUtil을 사용하여 현재 프로젝트 창 위치에 에셋 생성
|
||
ProjectWindowUtil.CreateAsset(avatar, "BlendShapeAvatar.asset");
|
||
|
||
// 생성된 에셋을 현재 선택된 BlendShapeAvatar로 설정
|
||
blendShapeAvatar = avatar;
|
||
}
|
||
}
|
||
} |