2025-04-25 21:14:54 +09:00

635 lines
28 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
}
}
}