219 lines
9.7 KiB
C#
219 lines
9.7 KiB
C#
/*
|
||
* CharacterVATConverter
|
||
*
|
||
* 一键把现有 SpriteRenderer 主角 (Wizard) 转成 VAT 3D 主角:
|
||
* 1) 实例化 Wizard.prefab 作为模板
|
||
* 2) 在根节点挂上 R02_VAT.prefab 作为 "View" 子物体
|
||
* 3) 添加 RendererAdapter (Mode=Mesh, fetchRoot=View)
|
||
* 4) 把 RendererAdapter 拖到 CharacterBehavior.rendererAdapter 槽
|
||
* 5) 保存为 Wizard_R02.prefab
|
||
* 6) (可选) 把 CharactersDatabase 的 WIZARD 的 prefab 指向新 Wizard_R02.prefab
|
||
*
|
||
* 用途:验证 RendererAdapter 在 OctoberStudio 主角生命周期里能正常 wire 起来。
|
||
*/
|
||
|
||
using OctoberStudio.Rendering;
|
||
using UnityEditor;
|
||
using UnityEngine;
|
||
|
||
namespace OctoberStudio.EditorTools
|
||
{
|
||
public static class CharacterVATConverter
|
||
{
|
||
private const string TemplatePath = "Assets/Common/Prefabs/Characters/Wizard.prefab";
|
||
private const string VATSourcePath = "Assets/_VAT_Baking/Output/R02_S_body01_world/R02_S_body01_world_VAT.prefab";
|
||
private const string OutputPath = "Assets/Common/Prefabs/Characters/Wizard_R02.prefab";
|
||
private const string DatabasePath = "Assets/Common/Scriptables/Characters Database.asset";
|
||
|
||
[MenuItem("Tools/OctoberStudio/VAT/1. Make Wizard_R02 Variant", priority = 1)]
|
||
public static void MakeVariant()
|
||
{
|
||
var ok = MakeWizardR02();
|
||
// 不弹 modal dialog (会阻塞 MCP 主线程通信),改用 console log。
|
||
if (ok) Debug.Log($"[VAT Convert] Created: {OutputPath}\n下一步: Tools/OctoberStudio/VAT/2. Apply Wizard_R02 to Database");
|
||
}
|
||
|
||
[MenuItem("Tools/OctoberStudio/VAT/2. Apply Wizard_R02 to Database", priority = 2)]
|
||
public static void ApplyToDatabase()
|
||
{
|
||
var ok = ApplyR02ToDatabase();
|
||
if (ok) Debug.Log("[VAT Convert] CharactersDatabase: WIZARD prefab → Wizard_R02.prefab. 跑场景查看效果。");
|
||
}
|
||
|
||
// Legacy: 用于校正旧版"单层"R02_VAT.prefab(无 Root+Mesh 结构)。
|
||
// 新版 baker 已把 Y=180 烤入 Mesh 子节点,无需调用此菜单。
|
||
[MenuItem("Tools/OctoberStudio/VAT/[Legacy] Calibrate Wizard_R02 (Y=180 + auto pivot)", priority = 100)]
|
||
public static void QuickCalibrateWizardR02()
|
||
{
|
||
CalibratePrefab(OutputPath, new Vector3(0f, 180f, 0f), 0.5f, autoCenterPivot: true);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 直接在 prefab asset 上做朝向 + 缩放 + pivot 校正,无需中转场景。
|
||
/// </summary>
|
||
public static void CalibratePrefab(string prefabPath, Vector3 euler, float uniformScale, bool autoCenterPivot)
|
||
{
|
||
var prefabRoot = PrefabUtility.LoadPrefabContents(prefabPath);
|
||
if (prefabRoot == null)
|
||
{
|
||
Debug.LogError($"[VAT] Cannot load prefab contents: {prefabPath}");
|
||
return;
|
||
}
|
||
try
|
||
{
|
||
var view = prefabRoot.transform.Find("View");
|
||
if (view == null)
|
||
{
|
||
Debug.LogError("[VAT] View transform not found");
|
||
return;
|
||
}
|
||
|
||
view.localRotation = Quaternion.Euler(euler);
|
||
view.localScale = Vector3.one * uniformScale;
|
||
|
||
if (autoCenterPivot)
|
||
{
|
||
var mf = view.GetComponentInChildren<MeshFilter>();
|
||
if (mf != null && mf.sharedMesh != null)
|
||
{
|
||
var meshCenter = mf.sharedMesh.bounds.center; // model-space
|
||
var scaled = Vector3.Scale(meshCenter, view.localScale);
|
||
var offset = view.localRotation * scaled; // view-parent space
|
||
view.localPosition = -offset;
|
||
}
|
||
}
|
||
|
||
PrefabUtility.SaveAsPrefabAsset(prefabRoot, prefabPath);
|
||
Debug.Log($"[VAT] Calibrated {prefabPath}: euler={euler}, scale={uniformScale}, pos={view.localPosition}");
|
||
}
|
||
finally
|
||
{
|
||
PrefabUtility.UnloadPrefabContents(prefabRoot);
|
||
}
|
||
AssetDatabase.SaveAssets();
|
||
AssetDatabase.Refresh();
|
||
}
|
||
|
||
[MenuItem("Tools/OctoberStudio/VAT/3. Restore Wizard (Original Sprite)", priority = 3)]
|
||
public static void RestoreOriginal()
|
||
{
|
||
var db = AssetDatabase.LoadAssetAtPath<CharactersDatabase>(DatabasePath);
|
||
if (db == null) { Debug.LogError("Database not found"); return; }
|
||
var template = AssetDatabase.LoadAssetAtPath<GameObject>(TemplatePath);
|
||
if (template == null) { Debug.LogError("Wizard.prefab not found"); return; }
|
||
|
||
var so = new SerializedObject(db);
|
||
var characters = so.FindProperty("characters");
|
||
if (characters.arraySize > 0)
|
||
{
|
||
characters.GetArrayElementAtIndex(0).FindPropertyRelative("prefab").objectReferenceValue = template;
|
||
so.ApplyModifiedProperties();
|
||
AssetDatabase.SaveAssets();
|
||
Debug.Log("[VAT] Restored original Wizard.prefab");
|
||
}
|
||
}
|
||
|
||
// ========================= IMPL =========================
|
||
|
||
public static bool MakeWizardR02()
|
||
{
|
||
var template = AssetDatabase.LoadAssetAtPath<GameObject>(TemplatePath);
|
||
var vatSrc = AssetDatabase.LoadAssetAtPath<GameObject>(VATSourcePath);
|
||
|
||
if (template == null)
|
||
{
|
||
Debug.LogError($"[VAT] Template prefab not found: {TemplatePath}");
|
||
return false;
|
||
}
|
||
if (vatSrc == null)
|
||
{
|
||
Debug.LogError($"[VAT] VAT prefab not found: {VATSourcePath}");
|
||
return false;
|
||
}
|
||
|
||
// 1) Instantiate Wizard as base
|
||
var instance = (GameObject)PrefabUtility.InstantiatePrefab(template);
|
||
instance.name = "Wizard_R02_tmp";
|
||
|
||
try
|
||
{
|
||
// 2) Instantiate R02 VAT as child "View"
|
||
// R02_VAT 自身是 Root + Mesh 两层结构, Mesh 节点上已经烤入 Y=180 (面相机),
|
||
// 所以这里 View 的 localRotation 保持 identity 即可,无需再做朝向校正
|
||
var view = (GameObject)PrefabUtility.InstantiatePrefab(vatSrc, instance.transform);
|
||
view.name = "View";
|
||
view.transform.localPosition = Vector3.zero;
|
||
view.transform.localRotation = Quaternion.identity;
|
||
// VAT 模型默认尺寸是真实角色级 (~1.7m);OctoberStudio 单位较小,先给个 0.5 缩放避免太大
|
||
// 用户可以在 prefab 里再调
|
||
view.transform.localScale = Vector3.one * 0.5f;
|
||
|
||
// 3) Add RendererAdapter on root, mode=Mesh, fetchRoot=View
|
||
var adapter = instance.AddComponent<RendererAdapter>();
|
||
{
|
||
var soAdapter = new SerializedObject(adapter);
|
||
var modeProp = soAdapter.FindProperty("mode");
|
||
if (modeProp != null) modeProp.enumValueIndex = (int)RendererMode.Mesh;
|
||
var rootProp = soAdapter.FindProperty("fetchRoot");
|
||
if (rootProp != null) rootProp.objectReferenceValue = view.transform;
|
||
soAdapter.ApplyModifiedProperties();
|
||
adapter.FetchComponents();
|
||
adapter.ApplyMode();
|
||
}
|
||
|
||
// 4) Wire adapter into CharacterBehavior.rendererAdapter
|
||
var character = instance.GetComponent<CharacterBehavior>();
|
||
if (character != null)
|
||
{
|
||
var soChar = new SerializedObject(character);
|
||
var fieldProp = soChar.FindProperty("rendererAdapter");
|
||
if (fieldProp != null) fieldProp.objectReferenceValue = adapter;
|
||
soChar.ApplyModifiedProperties();
|
||
}
|
||
else
|
||
{
|
||
Debug.LogWarning("[VAT] CharacterBehavior not found on Wizard root");
|
||
}
|
||
|
||
// 5) Save as new prefab (规则的 prefab,不是 variant,独立可改)
|
||
var savedAsset = PrefabUtility.SaveAsPrefabAsset(instance, OutputPath, out var success);
|
||
if (!success || savedAsset == null)
|
||
{
|
||
Debug.LogError("[VAT] SaveAsPrefabAsset failed");
|
||
return false;
|
||
}
|
||
Debug.Log($"[VAT] Saved: {OutputPath}");
|
||
}
|
||
finally
|
||
{
|
||
Object.DestroyImmediate(instance);
|
||
}
|
||
|
||
AssetDatabase.SaveAssets();
|
||
AssetDatabase.Refresh();
|
||
return true;
|
||
}
|
||
|
||
public static bool ApplyR02ToDatabase()
|
||
{
|
||
var db = AssetDatabase.LoadAssetAtPath<CharactersDatabase>(DatabasePath);
|
||
if (db == null) { Debug.LogError("Database not found"); return false; }
|
||
var newPrefab = AssetDatabase.LoadAssetAtPath<GameObject>(OutputPath);
|
||
if (newPrefab == null) { Debug.LogError($"Output prefab not found: {OutputPath}"); return false; }
|
||
|
||
var so = new SerializedObject(db);
|
||
var characters = so.FindProperty("characters");
|
||
if (characters == null || characters.arraySize == 0)
|
||
{
|
||
Debug.LogError("Database has no characters");
|
||
return false;
|
||
}
|
||
characters.GetArrayElementAtIndex(0).FindPropertyRelative("prefab").objectReferenceValue = newPrefab;
|
||
so.ApplyModifiedProperties();
|
||
AssetDatabase.SaveAssets();
|
||
Debug.Log("[VAT] CharactersDatabase: WIZARD prefab → Wizard_R02.prefab");
|
||
return true;
|
||
}
|
||
}
|
||
}
|