Land Explorer in HomeCamp with boot loading, journal UI, and CDN publish tooling.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -162,120 +162,6 @@ public static class BuildCharacterAnimationLab
|
||||
EditorSceneManager.CloseScene(scene,true);
|
||||
}
|
||||
}
|
||||
public static string BuildLmGirl()
|
||||
{
|
||||
if (EditorApplication.isPlaying) throw new InvalidOperationException("Edit Mode required.");
|
||||
const string targetRoot=Root+"/LmGirl";
|
||||
const string generated=targetRoot+"/Generated";
|
||||
if (SceneManager.GetSceneByPath(targetRoot+"/LmGirlAnimationLab.unity").isLoaded)
|
||||
throw new InvalidOperationException("Open a different scene before rebuilding LmGirl; Unity cannot overwrite another loaded scene.");
|
||||
Folder(generated);
|
||||
var data=JsonConvert.DeserializeObject<Data>(File.ReadAllText(Path.Combine(Repo,"Tools/output/lmgirl_tpose_studio/rig_mixamo41/unity_review_data.json")));
|
||||
var meshes=data.variants.ToDictionary(v=>v.name,v=>MakeMesh(v,data.bones,generated));
|
||||
var website=JsonConvert.DeserializeObject<Data>(File.ReadAllText(Path.Combine(Repo,"Tools/output/lmgirl_tpose_studio/rig_mixamo41/website/unity_review_data.json")));
|
||||
meshes["mixamo"]=MakeMesh(website.variants[0],website.bones,generated);
|
||||
var gait=JsonConvert.DeserializeObject<Gait>(File.ReadAllText(Path.Combine(Repo,"Tools/output/lmgirl_tpose_studio/rig_mixamo41/gait_correction.json")));
|
||||
var walk41=MakeClip(gait.motions["tokenrig"],data.bones,generated);
|
||||
var walk33=MakeClip(gait.motions["mixamo"],website.bones,generated);
|
||||
foreach (string suffix in new[]{"base","mix"})
|
||||
{
|
||||
string path=generated+"/LmGirlTpose_"+suffix+".png";
|
||||
File.Copy(Path.Combine(Repo,"Tools/output/lmgirl_tpose_studio/xpbr/LmGirlTpose_"+suffix+".png"),path,true);
|
||||
AssetDatabase.ImportAsset(path,ImportAssetOptions.ForceSynchronousImport);
|
||||
var importer=(TextureImporter)AssetImporter.GetAtPath(path);
|
||||
importer.sRGBTexture=suffix=="base";importer.maxTextureSize=1024;
|
||||
importer.alphaIsTransparency=false;importer.SaveAndReimport();
|
||||
}
|
||||
string matPath=generated+"/LmGirlTextured.mat";
|
||||
var material=AssetDatabase.LoadAssetAtPath<Material>(matPath);
|
||||
if (!material)
|
||||
{
|
||||
material=new Material(AssetDatabase.LoadAssetAtPath<Material>(Generated+"/CharacterTextured.mat"));
|
||||
AssetDatabase.CreateAsset(material,matPath);
|
||||
}
|
||||
material.SetTexture("_MainTex",AssetDatabase.LoadAssetAtPath<Texture2D>(generated+"/LmGirlTpose_base.png"));
|
||||
material.SetTexture("_MixTex",AssetDatabase.LoadAssetAtPath<Texture2D>(generated+"/LmGirlTpose_mix.png"));
|
||||
EditorUtility.SetDirty(material);
|
||||
var uiPath=JsonUIPrefabBuilder.BuildFromJsonFile(Path.Combine(Repo,"Doc/UIPrefabCreater/UI_LmGirlAnimationLab.json"));
|
||||
if (string.IsNullOrEmpty(uiPath)) throw new InvalidOperationException("LmGirl UI conversion failed.");
|
||||
var previous=SceneManager.GetActiveScene();
|
||||
var scene=EditorSceneManager.NewScene(NewSceneSetup.EmptyScene,NewSceneMode.Additive);
|
||||
try
|
||||
{
|
||||
SceneManager.SetActiveScene(scene);
|
||||
var root=(GameObject)PrefabUtility.InstantiatePrefab(AssetDatabase.LoadAssetAtPath<GameObject>(Generated+"/CharacterAnimationLab.prefab"));
|
||||
PrefabUtility.UnpackPrefabInstance(root,PrefabUnpackMode.Completely,InteractionMode.AutomatedAction);
|
||||
root.name="LmGirlAnimationLab";
|
||||
var viewer=root.GetComponent<CharacterAnimationLab>();
|
||||
string[] variants={"mixamo","raw","post","local"};
|
||||
string[] labels={"Mixamo 网站 · 33 骨","TokenRig 原始 · 41 骨","体素后处理 · 41 骨","局部约束 · 41 骨"};
|
||||
var slots=viewer.actors.Select(a=>a.rig.transform.parent).ToList();
|
||||
foreach(var actor in viewer.actors)UnityEngine.Object.DestroyImmediate(actor.rig);
|
||||
slots.Add(UnityEngine.Object.Instantiate(slots[2],root.transform));
|
||||
viewer.actors=new CharacterAnimationLab.Actor[4];
|
||||
Color[] colors={new Color(.49f,.78f,.85f),new Color(.94f,.72f,.43f),new Color(.68f,.62f,.85f),new Color(.62f,.82f,.51f)};
|
||||
Quaternion Rotation(float[] q)=>new Quaternion(q[0],q[1],q[2],q[3]);
|
||||
for (int i=0;i<variants.Length;i++)
|
||||
{
|
||||
var actorData=i==0?website:data;
|
||||
var slot=slots[i];slot.localPosition=new Vector3((i-1.5f)*.83f,0,0);
|
||||
slot.name="Stage_LmGirl_"+variants[i];
|
||||
slot.GetComponentInChildren<Renderer>().sharedMaterial=Material("LmGirlPedestal"+i,colors[i],generated);
|
||||
var rig=new GameObject("Rig");rig.transform.SetParent(slot,false);rig.transform.localScale=Vector3.one*(1.55f/actorData.height);
|
||||
var bones=new Transform[actorData.bones.Length];
|
||||
for (int b=0;b<bones.Length;b++)
|
||||
{
|
||||
var go=new GameObject(actorData.bones[b].name);bones[b]=go.transform;
|
||||
go.transform.SetParent(actorData.bones[b].parent<0?rig.transform:bones[actorData.bones[b].parent],false);
|
||||
SetTRS(go.transform,actorData.bones[b].rest);
|
||||
}
|
||||
var meshGo=new GameObject("CharacterMesh");meshGo.transform.SetParent(rig.transform,false);
|
||||
var skin=meshGo.AddComponent<SkinnedMeshRenderer>();skin.sharedMesh=meshes[variants[i]];skin.bones=bones;skin.rootBone=bones[0];
|
||||
skin.sharedMaterial=material;skin.quality=SkinQuality.Auto;skin.updateWhenOffscreen=true;
|
||||
skin.localBounds=new Bounds(new Vector3(0,.75f,0),new Vector3(3,3,3));
|
||||
viewer.actors[i]=new CharacterAnimationLab.Actor{label=labels[i],rig=rig,hips=bones[0],
|
||||
rightHand=bones.First(b=>b.name=="mixamorig:RightHand"),renderer=skin,
|
||||
walkingClip=i==0?walk33:walk41,
|
||||
translationScale=actorData.translationScale,translationOffsets=actorData.translationOffsets.Select(x=>V(x)).ToArray(),
|
||||
translationDeltaRotations=actorData.translationDeltaRotations?.Select(Rotation).ToArray()??Array.Empty<Quaternion>(),
|
||||
bindRotationCorrections=actorData.bindRotationCorrections?.Select(Rotation).ToArray()??Array.Empty<Quaternion>(),
|
||||
rotationParents=actorData.rotationParents??Array.Empty<int>()};
|
||||
}
|
||||
UnityEngine.Object.DestroyImmediate(viewer.ui.gameObject);
|
||||
var ui=(GameObject)PrefabUtility.InstantiatePrefab(AssetDatabase.LoadAssetAtPath<GameObject>(uiPath),root.transform);
|
||||
viewer.ui=ui.transform;ui.GetComponentInChildren<AnimationLabSeekBar>().viewer=viewer;
|
||||
viewer.texturedMaterial=material;viewer.motion=0;viewer.time=10f/30;viewer.focus=-1;viewer.detail=0;viewer.yaw=0;viewer.textured=true;viewer.playing=true;
|
||||
viewer.Sample();
|
||||
RenderSettings.ambientMode=AmbientMode.Flat;RenderSettings.ambientLight=new Color(.65f,.69f,.73f);RenderSettings.fog=false;
|
||||
if (!EditorSceneManager.SaveScene(scene,targetRoot+"/LmGirlAnimationLab.unity"))
|
||||
throw new IOException("LmGirl scene was not saved.");
|
||||
PrefabUtility.SaveAsPrefabAsset(root,generated+"/LmGirlAnimationLab.prefab");
|
||||
// Keep the rejected generic-cleaning experiments inspectable as
|
||||
// persistent prefabs without promoting them to the default lineup.
|
||||
for(int b=0;b<data.bones.Length;b++)SetTRS(viewer.actors[1].renderer.bones[b],data.bones[b].rest);
|
||||
for(int b=0;b<website.bones.Length;b++)SetTRS(viewer.actors[0].renderer.bones[b],website.bones[b].rest);
|
||||
PrefabUtility.SaveAsPrefabAsset(viewer.actors[0].rig,generated+"/LmGirl_mixamo.prefab");
|
||||
foreach (string variant in new[]{"raw","post","clean","clean_eps","local"})
|
||||
{
|
||||
var actor=viewer.actors[1];actor.renderer.sharedMesh=meshes[variant];
|
||||
PrefabUtility.SaveAsPrefabAsset(actor.rig,generated+"/LmGirl_"+variant+".prefab");
|
||||
}
|
||||
AssetDatabase.SaveAssets();
|
||||
return JsonConvert.SerializeObject(new{scene=targetRoot+"/LmGirlAnimationLab.unity",visibleVariants=4,websiteBones=website.bones.Length,tokenRigBones=data.bones.Length,triangles=4999,sourceClips=viewer.clips.Select(c=>AssetDatabase.GetAssetPath(c)),walkingClips=new[]{AssetDatabase.GetAssetPath(walk33),AssetDatabase.GetAssetPath(walk41)},translationScale=data.translationScale});
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (previous.IsValid()&&previous.isLoaded)SceneManager.SetActiveScene(previous);
|
||||
EditorSceneManager.CloseScene(scene,true);
|
||||
}
|
||||
}
|
||||
public static string OpenLmGirl()
|
||||
{
|
||||
if (EditorSceneManager.GetSceneManagerSetup().Any(s=>SceneManager.GetSceneByPath(s.path).isDirty))throw new InvalidOperationException("Existing scene has unsaved changes.");
|
||||
EditorSceneManager.OpenScene(Root+"/LmGirl/LmGirlAnimationLab.unity",OpenSceneMode.Single);
|
||||
Selection.activeGameObject=UnityEngine.Object.FindObjectOfType<CharacterAnimationLab>().gameObject;
|
||||
return SceneManager.GetActiveScene().path;
|
||||
}
|
||||
public static string RefineLayout()
|
||||
{
|
||||
if (EditorApplication.isPlaying) throw new InvalidOperationException("Edit Mode required.");
|
||||
|
||||
@@ -340,8 +340,8 @@ public static class BuildHomeCamp
|
||||
}
|
||||
Add<Tonemapping>().mode.value = TonemappingMode.Neutral;
|
||||
var color = Add<ColorAdjustments>();
|
||||
// Noon frame + another ~20% brightness (EV += log2(1.2)); mild contrast keeps shade soft-PBR.
|
||||
color.postExposure.value = .74f; color.contrast.value = 10; color.saturation.value = 5; color.hueShift.value = 0;
|
||||
// Keep the sunny palette while retaining roof and paving highlights in the home screen.
|
||||
color.postExposure.value = .30f; color.contrast.value = 10; color.saturation.value = 5; color.hueShift.value = 0;
|
||||
color.colorFilter.value = Color.white;
|
||||
var white = Add<WhiteBalance>(); white.temperature.value = 6; white.tint.value = 0;
|
||||
var bloom = Add<Bloom>(); bloom.threshold.value = 1.15f; bloom.intensity.value = .12f; bloom.scatter.value = .55f;
|
||||
@@ -415,6 +415,7 @@ public static class BuildHomeCamp
|
||||
camera.allowHDR = true; camera.allowMSAA = true;
|
||||
var data = camera.GetUniversalAdditionalCameraData();
|
||||
data.renderPostProcessing = true; data.volumeLayerMask = 1 << Layer;
|
||||
data.renderShadows = true;
|
||||
return camera;
|
||||
}
|
||||
static Light Light(Scene scene, Transform parent)
|
||||
@@ -425,10 +426,10 @@ public static class BuildHomeCamp
|
||||
// Key: warm sun. Fill comes from a separate baked skylight so Subtractive maps do not crush backfaces.
|
||||
// Fully Baked sun: cast shadows live in the lightmap RGB. Mixed/Subtractive still needs
|
||||
// realtime shadow maps, which offscreen camera.Render does not produce — so review looked shadowless.
|
||||
// Noon key: high elevation, +30% intensity vs prior look-dev; shade deepened in bake/shaders.
|
||||
light.color = new Color(1, .98f, .94f); light.intensity = 2.15f; light.shadows = LightShadows.Soft;
|
||||
light.lightmapBakeType = LightmapBakeType.Baked; light.shadowAngle = 4;
|
||||
light.shadowStrength = .94f; light.shadowBias = .05f; light.shadowNormalBias = .3f;
|
||||
// Noon key: warm sun with realtime soft shadows for buildings, explorer and companions.
|
||||
light.color = new Color(1, .98f, .94f); light.intensity = 1.95f; light.shadows = LightShadows.Soft;
|
||||
light.lightmapBakeType = LightmapBakeType.Realtime; light.shadowAngle = 4;
|
||||
light.shadowStrength = .85f; light.shadowBias = .05f; light.shadowNormalBias = .3f;
|
||||
light.cullingMask = 1 << Layer; go.transform.rotation = Quaternion.Euler(68, 50, 0);
|
||||
BakeFillLight(scene, parent);
|
||||
return light;
|
||||
@@ -471,12 +472,13 @@ public static class BuildHomeCamp
|
||||
renderer.sharedMaterial = AssetDatabase.LoadAssetAtPath<Material>(Root + "/backdrop.mat");
|
||||
renderer.shadowCastingMode = ShadowCastingMode.Off; renderer.receiveShadows = false;
|
||||
const float distance = 75;
|
||||
var halfHeight = distance * Mathf.Tan(camera.fieldOfView * Mathf.Deg2Rad * .5f);
|
||||
var width = halfHeight * 2 * 1080f / 1920;
|
||||
var height = width / 1.5f;
|
||||
go.transform.position = camera.transform.position + camera.transform.forward * distance + camera.transform.up * (halfHeight - height * .5f);
|
||||
// Wide enough for 3:4 tablets and tall enough for 9:22 ultra-long displays with margin.
|
||||
const float width = 72f;
|
||||
const float height = 56f;
|
||||
const float topHeight = 52f;
|
||||
go.transform.position = camera.transform.position + camera.transform.forward * distance + camera.transform.up * (topHeight - height * .5f);
|
||||
go.transform.rotation = camera.transform.rotation;
|
||||
go.transform.localScale = new Vector3(width * 1.04f, height * 1.04f, 1);
|
||||
go.transform.localScale = new Vector3(width, height, 1);
|
||||
SetLayer(go);
|
||||
}
|
||||
|
||||
@@ -529,6 +531,66 @@ public static class BuildHomeCamp
|
||||
foreach (var house in new[] { "MainHouse", "HouseLeft", "HouseRight" })
|
||||
root.transform.Find(house).position += Vector3.down * .08f;
|
||||
Put("fountain", "Fountain", 0, .8f, 4.4f, true);
|
||||
// Place protagonist Explorer in the central plaza (scaled 4x for isometric camp readability)
|
||||
var explorerPrefab = AssetDatabase.LoadAssetAtPath<GameObject>("Assets/Bundles/Characters/Human/Explorer/Explorer.prefab");
|
||||
if (explorerPrefab != null)
|
||||
{
|
||||
var explorerGo = (GameObject)PrefabUtility.InstantiatePrefab(explorerPrefab, root.transform);
|
||||
explorerGo.name = "Explorer";
|
||||
float expY = GroundY(ground, 0, -3.5f);
|
||||
explorerGo.transform.position = new Vector3(0, expY, -3.5f);
|
||||
explorerGo.transform.localScale = Vector3.one * 4.0f;
|
||||
// Face towards camera / player by default
|
||||
explorerGo.transform.rotation = Quaternion.Euler(0, 180f, 0);
|
||||
SetLayer(explorerGo);
|
||||
placements.Add("Explorer protagonist groundY=" + expY.ToString("F3"));
|
||||
}
|
||||
// Place wandering companions in the camp (scaled 4x for isometric readability)
|
||||
var petDefs = new[]
|
||||
{
|
||||
(path: "Assets/Bundles/Characters/Animal/FireCat/FireCat_Companion.prefab", name: "Pet_FireCat", x: -3.8f, z: 1.2f, r: 2.8f, s: 3.40f),
|
||||
(path: "Assets/Bundles/Characters/Animal/RockBallStudio/RockBallStudio_Companion.prefab", name: "Pet_RockBall", x: 2.6f, z: -1.8f, r: 2.8f, s: 3.00f),
|
||||
(path: "Assets/Bundles/Characters/Animal/BudBud/BudBud_Companion.prefab", name: "Pet_BudBud", x: -1.6f, z: 4.2f, r: 2.6f, s: 3.20f)
|
||||
};
|
||||
|
||||
var petWandererType = AppDomain.CurrentDomain.GetAssemblies()
|
||||
.Select(a => a.GetType("HotUpdate.Gameplay.CampPetWanderer"))
|
||||
.FirstOrDefault(t => t != null);
|
||||
|
||||
foreach (var petDef in petDefs)
|
||||
{
|
||||
var petPrefab = AssetDatabase.LoadAssetAtPath<GameObject>(petDef.path);
|
||||
if (petPrefab == null) continue;
|
||||
var petGo = (GameObject)PrefabUtility.InstantiatePrefab(petPrefab, root.transform);
|
||||
petGo.name = petDef.name;
|
||||
float petY = GroundY(ground, petDef.x, petDef.z);
|
||||
petGo.transform.position = new Vector3(petDef.x, petY, petDef.z);
|
||||
petGo.transform.localScale = Vector3.one * petDef.s;
|
||||
// Default facing towards camera for -Z models
|
||||
petGo.transform.rotation = Quaternion.Euler(0, 0, 0);
|
||||
SetLayer(petGo);
|
||||
|
||||
var col = petGo.GetComponent<CapsuleCollider>();
|
||||
if (col == null)
|
||||
{
|
||||
col = petGo.AddComponent<CapsuleCollider>();
|
||||
col.center = new Vector3(0, 0.45f, 0);
|
||||
col.radius = 0.4f;
|
||||
col.height = 1.0f;
|
||||
}
|
||||
|
||||
if (petWandererType != null)
|
||||
{
|
||||
var wanderer = petGo.GetComponent(petWandererType);
|
||||
if (wanderer == null) wanderer = petGo.AddComponent(petWandererType);
|
||||
var centerField = petWandererType.GetField("wanderCenter");
|
||||
if (centerField != null) centerField.SetValue(wanderer, new Vector3(petDef.x, petY, petDef.z));
|
||||
var radiusField = petWandererType.GetField("wanderRadius");
|
||||
if (radiusField != null) radiusField.SetValue(wanderer, petDef.r);
|
||||
}
|
||||
|
||||
placements.Add($"{petDef.name} groundY={petY:F3}");
|
||||
}
|
||||
// Keep canopy trees behind the roofs so they frame the backdrop without dunking HouseLeft in shadow.
|
||||
// Nearer canopy slots are placed after the viewer vector, as DGX meshes when ready.
|
||||
// TreeRearLeft / TreeRearRight stay cards: they sit behind the roofs and another 2000 tris each is not worth it.
|
||||
|
||||
@@ -6,7 +6,7 @@ using UnityEngine;
|
||||
using UnityEngine.Rendering;
|
||||
|
||||
/// <summary>
|
||||
/// 历史 LmGirl / LmCat 观察场景,保留旧 -Z 资产及相机方向;项目新资源采用 +Z 正面。
|
||||
/// 历史 Explorer / LmCat 观察场景,保留旧 -Z 资产及相机方向;项目新资源采用 +Z 正面。
|
||||
/// unity command run_script --file AgentScripts/BuildModelView.cs --entry BuildModelView.Run
|
||||
/// </summary>
|
||||
public static class BuildModelView
|
||||
@@ -14,7 +14,7 @@ public static class BuildModelView
|
||||
const string SceneDir = "Assets/Bundles/Scenes/Test/ModelView";
|
||||
const string ScenePath = SceneDir + "/ModelView.unity";
|
||||
const string GroundMatPath = SceneDir + "/Ground.mat";
|
||||
const string GirlPath = "Assets/Bundles/Characters/Human/LmGirl/LmGirl.prefab";
|
||||
const string GirlPath = "Assets/Bundles/Characters/Human/Explorer/Explorer.prefab";
|
||||
const string CatPath = "Assets/Bundles/Characters/Animal/LmCat/LmCat.prefab";
|
||||
|
||||
public static string Run()
|
||||
@@ -54,7 +54,7 @@ public static class BuildModelView
|
||||
var houses = PlaceHouses();
|
||||
|
||||
var girlGo = (GameObject)PrefabUtility.InstantiatePrefab(girl);
|
||||
girlGo.name = "LmGirl";
|
||||
girlGo.name = "Explorer";
|
||||
girlGo.transform.SetPositionAndRotation(new Vector3(-1.6f, 0f, -36f), Quaternion.identity);
|
||||
|
||||
var catGo = (GameObject)PrefabUtility.InstantiatePrefab(cat);
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
#if UNITY_EDITOR
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
public static class CheckPetFacing
|
||||
{
|
||||
public static string Run()
|
||||
{
|
||||
var paths = new[]
|
||||
{
|
||||
"Assets/Bundles/Characters/Animal/FireCat/FireCat_Companion.prefab",
|
||||
"Assets/Bundles/Characters/Animal/RockBallStudio/RockBallStudio_Companion.prefab",
|
||||
"Assets/Bundles/Characters/Animal/BudBud/BudBud_Companion.prefab"
|
||||
};
|
||||
var sb = new System.Text.StringBuilder();
|
||||
foreach (var path in paths)
|
||||
{
|
||||
var go = AssetDatabase.LoadAssetAtPath<GameObject>(path);
|
||||
if (go == null) continue;
|
||||
var smr = go.GetComponentInChildren<SkinnedMeshRenderer>();
|
||||
var anim = go.GetComponentInChildren<Animator>();
|
||||
sb.AppendLine($"=== {go.name} ===");
|
||||
if (smr != null)
|
||||
{
|
||||
var bounds = smr.localBounds;
|
||||
sb.AppendLine($" SMR: {smr.name}, localBounds center={bounds.center}, size={bounds.size}");
|
||||
if (smr.rootBone != null)
|
||||
{
|
||||
sb.AppendLine($" RootBone: {smr.rootBone.name}, localRot={smr.rootBone.localEulerAngles}, forward={smr.rootBone.forward}");
|
||||
}
|
||||
}
|
||||
if (anim != null)
|
||||
{
|
||||
sb.AppendLine($" Animator on: {anim.gameObject.name}, rot={anim.transform.localEulerAngles}");
|
||||
}
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -1,178 +0,0 @@
|
||||
#if UNITY_EDITOR
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
using UnityEditor;
|
||||
using UnityEditor.SceneManagement;
|
||||
using UnityEditor.Animations;
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
using UnityEngine.Animations;
|
||||
using UnityEngine.Playables;
|
||||
|
||||
public static class DiagnoseSharedHumanoidWalk
|
||||
{
|
||||
const string Boy="Assets/Bundles/Characters/Human/test/Boy_mixamo/CampGuideStudio.fbx";
|
||||
const string Girl="Assets/Bundles/Characters/Human/test/girl/LmGirlTpose.fbx";
|
||||
const string Walk="Assets/Bundles/Characters/Human/Common/Walking.fbx";
|
||||
static string Output=>Path.Combine(Directory.GetParent(Application.dataPath).Parent.FullName,"Tools/output/lmgirl_tpose_studio/shared_walk_audit");
|
||||
static float[] V(Vector3 v)=>new[]{v.x,v.y,v.z};
|
||||
static string P(UnityEngine.Object o)=>o?AssetDatabase.GetAssetPath(o):null;
|
||||
static readonly HumanBodyBones[] BoneIds={HumanBodyBones.Hips,HumanBodyBones.LeftUpperLeg,HumanBodyBones.RightUpperLeg,HumanBodyBones.LeftLowerLeg,HumanBodyBones.RightLowerLeg,HumanBodyBones.LeftFoot,HumanBodyBones.RightFoot,HumanBodyBones.LeftToes,HumanBodyBones.RightToes};
|
||||
static object[] Pose(Animator a)=>BoneIds.Select(id=>{var t=a.GetBoneTransform(id);return (object)new{bone=id.ToString(),name=t?t.name:null,position=t?V(a.transform.InverseTransformPoint(t.position)):null,rotation=t?V(t.localEulerAngles):null};}).ToArray();
|
||||
public static string Inspect()
|
||||
{
|
||||
var scene=EditorSceneManager.NewPreviewScene();
|
||||
try
|
||||
{
|
||||
var assets=new List<object>();
|
||||
foreach(var path in new[]{Boy,Girl,Walk})
|
||||
{
|
||||
var importer=(ModelImporter)AssetImporter.GetAtPath(path);var hd=importer.humanDescription;
|
||||
var avatar=AssetDatabase.LoadAllAssetsAtPath(path).OfType<Avatar>().FirstOrDefault();
|
||||
var go=(GameObject)PrefabUtility.InstantiatePrefab(AssetDatabase.LoadAssetAtPath<GameObject>(path),scene);
|
||||
var animator=go.GetComponent<Animator>();if(animator)animator.runtimeAnimatorController=null;
|
||||
var ahd=avatar?avatar.humanDescription:new HumanDescription();
|
||||
assets.Add(new{path,importer.animationType,importer.avatarSetup,sourceAvatar=P(importer.sourceAvatar),avatar=avatar?avatar.name:null,valid=avatar&&avatar.isValid,
|
||||
importerHuman=hd.human.Select(b=>new{b.humanName,b.boneName}).ToArray(),importerSkeleton=hd.skeleton.Select(b=>new{b.name,p=V(b.position),r=V(b.rotation.eulerAngles),s=V(b.scale)}).ToArray(),
|
||||
avatarHuman=ahd.human==null?null:ahd.human.Select(b=>new{b.humanName,b.boneName}).ToArray(),avatarSkeleton=ahd.skeleton==null?null:ahd.skeleton.Select(b=>new{b.name,p=V(b.position),r=V(b.rotation.eulerAngles),s=V(b.scale)}).ToArray(),
|
||||
pose=animator&&animator.isHuman?Pose(animator):null});
|
||||
UnityEngine.Object.DestroyImmediate(go);
|
||||
}
|
||||
Directory.CreateDirectory(Output);var json=JsonConvert.SerializeObject(assets,Formatting.Indented);File.WriteAllText(Path.Combine(Output,"avatars.json"),json);return "Saved "+Output+"/avatars.json";
|
||||
}
|
||||
finally{EditorSceneManager.ClosePreviewScene(scene);}
|
||||
}
|
||||
public static string Compare()
|
||||
{
|
||||
var scene=EditorSceneManager.NewPreviewScene();var results=new List<object>();
|
||||
try
|
||||
{
|
||||
foreach(var path in new[]{Boy,Girl})foreach(bool footIK in new[]{false,true})
|
||||
{
|
||||
var go=(GameObject)PrefabUtility.InstantiatePrefab(AssetDatabase.LoadAssetAtPath<GameObject>(path),scene);
|
||||
var a=go.GetComponent<Animator>();a.runtimeAnimatorController=null;a.applyRootMotion=false;a.cullingMode=AnimatorCullingMode.AlwaysAnimate;
|
||||
var clip=AssetDatabase.LoadAllAssetsAtPath(Walk).OfType<AnimationClip>().First(c=>!c.name.StartsWith("__preview__"));
|
||||
var graph=PlayableGraph.Create("SharedWalkAudit");graph.SetTimeUpdateMode(DirectorUpdateMode.Manual);
|
||||
try
|
||||
{
|
||||
var playable=AnimationClipPlayable.Create(graph,clip);playable.SetApplyFootIK(footIK);playable.SetApplyPlayableIK(false);
|
||||
var output=AnimationPlayableOutput.Create(graph,"Animation",a);output.SetSourcePlayable(playable);graph.Play();
|
||||
var frames=new List<object>();float minGap=float.MaxValue,maxGap=float.MinValue;int crossed=0;
|
||||
for(int i=0;i<125;i++)
|
||||
{
|
||||
playable.SetTime(i/120.0);graph.Evaluate(0);
|
||||
var left=go.transform.InverseTransformPoint(a.GetBoneTransform(HumanBodyBones.LeftFoot).position);var right=go.transform.InverseTransformPoint(a.GetBoneTransform(HumanBodyBones.RightFoot).position);
|
||||
float gap=right.x-left.x;minGap=Mathf.Min(minGap,gap);maxGap=Mathf.Max(maxGap,gap);if(gap<0)crossed++;
|
||||
frames.Add(new{t=i/120.0,gap,pose=Pose(a)});
|
||||
}
|
||||
results.Add(new{path,footIK,minGap,maxGap,crossed,frames});
|
||||
}
|
||||
finally{graph.Destroy();UnityEngine.Object.DestroyImmediate(go);}
|
||||
}
|
||||
Directory.CreateDirectory(Output);var json=JsonConvert.SerializeObject(results,Formatting.Indented);File.WriteAllText(Path.Combine(Output,"baseline.json"),json);
|
||||
return "Saved "+Output+"/baseline.json";
|
||||
}
|
||||
finally{EditorSceneManager.ClosePreviewScene(scene);}
|
||||
}
|
||||
public static string VerifyController()
|
||||
{
|
||||
const string controllerPath="Assets/Bundles/Characters/Human/Common/Man.controller";
|
||||
var controller=AssetDatabase.LoadAssetAtPath<AnimatorController>(controllerPath);
|
||||
var scene=EditorSceneManager.NewPreviewScene();var results=new List<object>();
|
||||
var units=new List<GameObject>();var controllers=new List<AnimatorController>();
|
||||
var material=new Material(Shader.Find("Universal Render Pipeline/Lit"));material.color=new Color(.65f,.7f,.76f);
|
||||
try
|
||||
{
|
||||
int column=0;
|
||||
foreach(var path in new[]{Boy,Girl})foreach(bool footIK in new[]{false,true})
|
||||
{
|
||||
// A transient controller contains only the actual walk state and clip.
|
||||
var c=new AnimatorController();controllers.Add(c);c.AddLayer("Base Layer");
|
||||
var state=c.layers[0].stateMachine.AddState("walk");state.motion=controller.layers[0].stateMachine.states.Single(s=>s.state.name=="walk").state.motion;state.iKOnFeet=footIK;c.layers[0].stateMachine.defaultState=state;
|
||||
var go=(GameObject)PrefabUtility.InstantiatePrefab(AssetDatabase.LoadAssetAtPath<GameObject>(path),scene);units.Add(go);
|
||||
go.transform.position=new Vector3((1.5f-column++)*.85f,0,0);
|
||||
var a=go.GetComponent<Animator>();a.runtimeAnimatorController=c;a.applyRootMotion=false;a.cullingMode=AnimatorCullingMode.AlwaysAnimate;
|
||||
foreach(var r in go.GetComponentsInChildren<Renderer>())r.sharedMaterial=material;
|
||||
a.Rebind();a.Update(0);
|
||||
float minGap=float.MaxValue,maxGap=float.MinValue;int crossed=0,minFrame=0;
|
||||
for(int i=0;i<125;i++)
|
||||
{
|
||||
a.Play("Base Layer.walk",0,(i/120f)/state.motion.averageDuration);a.Update(0);
|
||||
var left=go.transform.InverseTransformPoint(a.GetBoneTransform(HumanBodyBones.LeftFoot).position);var right=go.transform.InverseTransformPoint(a.GetBoneTransform(HumanBodyBones.RightFoot).position);
|
||||
float gap=right.x-left.x;if(gap<minGap){minGap=gap;minFrame=i;}maxGap=Mathf.Max(maxGap,gap);if(gap<0)crossed++;
|
||||
}
|
||||
results.Add(new{path,footIK,minGap,maxGap,crossed,minTime=minFrame/120f});
|
||||
// Show all variants at the same phase, near the observed crossing.
|
||||
a.Play("Base Layer.walk",0,(32/120f)/state.motion.averageDuration);a.Update(0);
|
||||
var hips=a.GetBoneTransform(HumanBodyBones.Hips);var hp=hips.position;hp.x=go.transform.position.x;hp.z=0;hips.position=hp;
|
||||
}
|
||||
var light=new GameObject("AuditLight").AddComponent<Light>();SceneManager.MoveGameObjectToScene(light.gameObject,scene);light.type=LightType.Directional;light.intensity=1.5f;light.transform.rotation=Quaternion.Euler(35,135,0);
|
||||
var camera=new GameObject("AuditCamera").AddComponent<Camera>();SceneManager.MoveGameObjectToScene(camera.gameObject,scene);camera.scene=scene;camera.orthographic=true;camera.orthographicSize=.8f;
|
||||
camera.transform.position=new Vector3(0,.65f,5);camera.transform.LookAt(new Vector3(0,.65f,0));camera.clearFlags=CameraClearFlags.SolidColor;camera.backgroundColor=new Color(.19f,.22f,.26f);camera.nearClipPlane=.01f;camera.farClipPlane=20;
|
||||
var rt=new RenderTexture(1600,720,24);var tex=new Texture2D(1600,720,TextureFormat.RGB24,false);var previous=RenderTexture.active;
|
||||
Directory.CreateDirectory(Output);
|
||||
try{camera.targetTexture=rt;camera.Render();RenderTexture.active=rt;tex.ReadPixels(new Rect(0,0,1600,720),0,0);tex.Apply();File.WriteAllBytes(Path.Combine(Output,"foot_ik_comparison.png"),tex.EncodeToPNG());}
|
||||
finally{RenderTexture.active=previous;camera.targetTexture=null;rt.Release();UnityEngine.Object.DestroyImmediate(rt);UnityEngine.Object.DestroyImmediate(tex);}
|
||||
var json=JsonConvert.SerializeObject(new{order=new[]{"Boy FootIK off","Boy FootIK on","Girl FootIK off","Girl FootIK on"},controllerPath,controllerDirty=EditorUtility.IsDirty(controller),stateDirty=EditorUtility.IsDirty(controller.layers[0].stateMachine.states.Single(s=>s.state.name=="walk").state),results},Formatting.Indented);
|
||||
File.WriteAllText(Path.Combine(Output,"controller_verification.json"),json);return json;
|
||||
}
|
||||
finally
|
||||
{
|
||||
foreach(var go in units)UnityEngine.Object.DestroyImmediate(go);
|
||||
foreach(var c in controllers){foreach(var l in c.layers){foreach(var s in l.stateMachine.states)UnityEngine.Object.DestroyImmediate(s.state);UnityEngine.Object.DestroyImmediate(l.stateMachine);}UnityEngine.Object.DestroyImmediate(c);}
|
||||
EditorSceneManager.ClosePreviewScene(scene);UnityEngine.Object.DestroyImmediate(material);
|
||||
}
|
||||
}
|
||||
public static string FixFootIK()
|
||||
{
|
||||
const string controllerPath="Assets/Bundles/Characters/Human/Common/Man.controller";
|
||||
var controller=AssetDatabase.LoadAssetAtPath<AnimatorController>(controllerPath);
|
||||
var state=controller.layers[0].stateMachine.states.Single(s=>s.state.name=="walk").state;
|
||||
if(EditorUtility.IsDirty(controller)||EditorUtility.IsDirty(state))throw new Exception("Controller has unsaved edits; refusing to save unrelated edits.");
|
||||
var scenesBefore=Enumerable.Range(0,SceneManager.sceneCount).Select(i=>{var s=SceneManager.GetSceneAt(i);return new{s.path,s.isDirty};}).ToArray();
|
||||
bool previous=state.iKOnFeet;
|
||||
state.iKOnFeet=true;EditorUtility.SetDirty(state);AssetDatabase.SaveAssetIfDirty(state);
|
||||
var importer=(ModelImporter)AssetImporter.GetAtPath(Walk);
|
||||
if(!state.iKOnFeet || P(state.motion)!=Walk || P(importer.sourceAvatar)!=Boy)throw new Exception("Unexpected controller/source setup");
|
||||
var json=JsonConvert.SerializeObject(new{controllerPath,state=state.name,previousFootIK=previous,footIK=state.iKOnFeet,walk=P(state.motion),sourceAvatar=P(importer.sourceAvatar),
|
||||
controllerDirty=EditorUtility.IsDirty(controller),stateDirty=EditorUtility.IsDirty(state),
|
||||
actors=UnityEngine.Object.FindObjectsOfType<Animator>(true).Where(a=>a.runtimeAnimatorController==controller).Select(a=>new{a.name,avatar=P(a.avatar),a.applyRootMotion}).ToArray(),
|
||||
scenesBefore,scenesAfter=Enumerable.Range(0,SceneManager.sceneCount).Select(i=>{var s=SceneManager.GetSceneAt(i);return new{s.path,s.isDirty};}).ToArray()},Formatting.Indented);
|
||||
File.WriteAllText(Path.Combine(Output,"fix.json"),json);return json;
|
||||
}
|
||||
public static string VerifyApplied()
|
||||
{
|
||||
var controller=AssetDatabase.LoadAssetAtPath<AnimatorController>("Assets/Bundles/Characters/Human/Common/Man.controller");
|
||||
var state=controller.layers[0].stateMachine.states.Single(s=>s.state.name=="walk").state;
|
||||
if(!state.iKOnFeet)throw new Exception("Foot IK disabled");
|
||||
var scene=EditorSceneManager.NewPreviewScene();var results=new List<object>();
|
||||
try
|
||||
{
|
||||
foreach(var path in new[]{Boy,Girl})
|
||||
{
|
||||
var go=(GameObject)PrefabUtility.InstantiatePrefab(AssetDatabase.LoadAssetAtPath<GameObject>(path),scene);
|
||||
try
|
||||
{
|
||||
var a=go.GetComponent<Animator>();a.runtimeAnimatorController=controller;a.applyRootMotion=false;a.cullingMode=AnimatorCullingMode.AlwaysAnimate;a.Rebind();a.Update(0);
|
||||
float minGap=float.MaxValue,maxGap=float.MinValue;int crossed=0;
|
||||
for(int i=0;i<125;i++)
|
||||
{
|
||||
a.Play("Base Layer.walk",0,(i/120f)/state.motion.averageDuration);a.Update(0);
|
||||
var left=go.transform.InverseTransformPoint(a.GetBoneTransform(HumanBodyBones.LeftFoot).position);var right=go.transform.InverseTransformPoint(a.GetBoneTransform(HumanBodyBones.RightFoot).position);
|
||||
float gap=right.x-left.x;minGap=Mathf.Min(minGap,gap);maxGap=Mathf.Max(maxGap,gap);if(gap<0)crossed++;
|
||||
}
|
||||
if(crossed>0)throw new Exception("Crossing after fix: "+path);
|
||||
results.Add(new{path,avatar=P(a.avatar),samples=125,minGap,maxGap,crossed});
|
||||
}
|
||||
finally{UnityEngine.Object.DestroyImmediate(go);}
|
||||
}
|
||||
var json=JsonConvert.SerializeObject(new{controller=P(controller),state=state.name,footIK=state.iKOnFeet,clip=P(state.motion),results},Formatting.Indented);
|
||||
File.WriteAllText(Path.Combine(Output,"validation.json"),json);return json;
|
||||
}
|
||||
finally{EditorSceneManager.ClosePreviewScene(scene);}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -1,123 +0,0 @@
|
||||
#if UNITY_EDITOR
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using System.Security.Cryptography;
|
||||
using Newtonsoft.Json;
|
||||
using UnityEditor;
|
||||
using UnityEditor.Animations;
|
||||
using UnityEditor.SceneManagement;
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
|
||||
public static class ImportLmGirlTokenRigFbx
|
||||
{
|
||||
const string FolderPath="Assets/Bundles/Characters/Human/test/girl_tokenrig";
|
||||
const string ModelPath=FolderPath+"/LmGirlTokenRig.fbx";
|
||||
const string MaterialPath=FolderPath+"/LmGirlTokenRig.mat";
|
||||
static string Repo=>Directory.GetParent(Application.dataPath).Parent.FullName;
|
||||
static string Output=>Path.Combine(Repo,"Tools/output/lmgirl_tpose_studio/rig_mixamo41/fbx");
|
||||
static float[] V(Vector3 v)=>new[]{v.x,v.y,v.z};
|
||||
static void Folder(string p){if(AssetDatabase.IsValidFolder(p))return;Folder(Path.GetDirectoryName(p).Replace('\\','/'));AssetDatabase.CreateFolder(Path.GetDirectoryName(p).Replace('\\','/'),Path.GetFileName(p));}
|
||||
static string Hash(string p){using(var sha=SHA256.Create())return BitConverter.ToString(sha.ComputeHash(File.ReadAllBytes(p))).Replace("-","").ToLowerInvariant();}
|
||||
static void CopyNew(string source,string target)
|
||||
{
|
||||
var full=Path.Combine(Directory.GetParent(Application.dataPath).FullName,target);
|
||||
if(File.Exists(full)){if(Hash(full)!=Hash(source))throw new Exception("Existing destination differs: "+target);}
|
||||
else File.Copy(source,full);
|
||||
AssetDatabase.ImportAsset(target,ImportAssetOptions.ForceSynchronousImport);
|
||||
}
|
||||
public static string Import()
|
||||
{
|
||||
if(EditorApplication.isPlaying)throw new Exception("Edit Mode required");
|
||||
Folder(FolderPath);
|
||||
foreach(var suffix in new[]{"base","mix"})
|
||||
{
|
||||
string path=FolderPath+"/LmGirlTokenRig_"+suffix+".png";
|
||||
CopyNew(Path.Combine(Repo,"Tools/output/lmgirl_tpose_studio/xpbr/LmGirlTpose_"+suffix+".png"),path);
|
||||
var ti=(TextureImporter)AssetImporter.GetAtPath(path);ti.sRGBTexture=suffix=="base";ti.alphaIsTransparency=false;ti.maxTextureSize=1024;ti.SaveAndReimport();
|
||||
}
|
||||
var material=AssetDatabase.LoadAssetAtPath<Material>(MaterialPath);
|
||||
if(!material)
|
||||
{
|
||||
var reference=AssetDatabase.LoadAssetAtPath<Material>("Assets/Review/CharacterAnimationLab/LmGirl/Generated/LmGirlTextured.mat");
|
||||
if(!reference)throw new Exception("Missing approved XPbr reference material");
|
||||
material=new Material(reference);material.name="LmGirlTokenRig";AssetDatabase.CreateAsset(material,MaterialPath);
|
||||
}
|
||||
material.SetTexture("_MainTex",AssetDatabase.LoadAssetAtPath<Texture2D>(FolderPath+"/LmGirlTokenRig_base.png"));
|
||||
material.SetTexture("_MixTex",AssetDatabase.LoadAssetAtPath<Texture2D>(FolderPath+"/LmGirlTokenRig_mix.png"));
|
||||
EditorUtility.SetDirty(material);AssetDatabase.SaveAssetIfDirty(material);
|
||||
CopyNew(Path.Combine(Output,"LmGirlTokenRig.fbx"),ModelPath);
|
||||
var importer=(ModelImporter)AssetImporter.GetAtPath(ModelPath);
|
||||
importer.animationType=ModelImporterAnimationType.Human;importer.avatarSetup=ModelImporterAvatarSetup.CreateFromThisModel;
|
||||
importer.importAnimation=false;importer.globalScale=1;importer.useFileScale=true;
|
||||
importer.importCameras=false;importer.importLights=false;
|
||||
importer.meshCompression=ModelImporterMeshCompression.Off;
|
||||
importer.skinWeights=ModelImporterSkinWeights.Custom;importer.maxBonesPerVertex=4;importer.minBoneWeight=0;
|
||||
importer.materialImportMode=ModelImporterMaterialImportMode.ImportStandard;
|
||||
importer.AddRemap(new AssetImporter.SourceAssetIdentifier(typeof(Material),"LmGirlTokenRig_Body"),material);
|
||||
importer.SaveAndReimport();
|
||||
var model=AssetDatabase.LoadAssetAtPath<GameObject>(ModelPath);
|
||||
var avatar=AssetDatabase.LoadAllAssetsAtPath(ModelPath).OfType<Avatar>().FirstOrDefault();
|
||||
if(!avatar || !avatar.isValid || !avatar.isHuman)throw new Exception("Invalid Humanoid Avatar");
|
||||
if(!model.GetComponentsInChildren<SkinnedMeshRenderer>().All(s=>s.sharedMaterial==material))throw new Exception("Material remap failed");
|
||||
return "Imported "+ModelPath+" with valid Humanoid Avatar and project XPbr material";
|
||||
}
|
||||
public static string Verify()
|
||||
{
|
||||
var scene=EditorSceneManager.NewPreviewScene();var units=new List<GameObject>();
|
||||
var controller=AssetDatabase.LoadAssetAtPath<AnimatorController>("Assets/Bundles/Characters/Human/Common/Man.controller");
|
||||
var walk=controller.layers[0].stateMachine.states.Single(s=>s.state.name=="walk").state;
|
||||
var records=new List<object>();
|
||||
try
|
||||
{
|
||||
int column=0;
|
||||
foreach(var path in new[]{"Assets/Bundles/Characters/Human/test/girl/LmGirlTpose.fbx",ModelPath})
|
||||
{
|
||||
var go=(GameObject)PrefabUtility.InstantiatePrefab(AssetDatabase.LoadAssetAtPath<GameObject>(path),scene);units.Add(go);
|
||||
go.transform.position=new Vector3((.5f-column++)*.95f,0,0);
|
||||
var a=go.GetComponent<Animator>();var skin=go.GetComponentInChildren<SkinnedMeshRenderer>();
|
||||
if(!a||!a.isHuman||!a.avatar.isValid)throw new Exception("Invalid Animator: "+path);
|
||||
var baked=new Mesh();skin.BakeMesh(baked);
|
||||
var vertices=baked.vertices.Select(p=>go.transform.InverseTransformPoint(skin.transform.TransformPoint(p))).ToArray();
|
||||
var lo=vertices.Aggregate(Vector3.positiveInfinity,Vector3.Min);var hi=vertices.Aggregate(Vector3.negativeInfinity,Vector3.Max);
|
||||
var toes=new[]{HumanBodyBones.LeftToes,HumanBodyBones.RightToes}.Select(b=>go.transform.InverseTransformPoint(a.GetBoneTransform(b).position)).ToArray();
|
||||
var ankles=new[]{HumanBodyBones.LeftFoot,HumanBodyBones.RightFoot}.Select(b=>go.transform.InverseTransformPoint(a.GetBoneTransform(b).position)).ToArray();
|
||||
if(hi.y-lo.y<1 || hi.y-lo.y>1.4f || Mathf.Abs(lo.y)>.001f)throw new Exception("Wrong height/floor: "+path);
|
||||
if(toes[0].z<=ankles[0].z||toes[1].z<=ankles[1].z)throw new Exception("Not facing +Z: "+path);
|
||||
var counts=skin.sharedMesh.GetBonesPerVertex();int maxInfluences=0;foreach(var n in counts)maxInfluences=Math.Max(maxInfluences,n);counts.Dispose();
|
||||
var rest=new{boundsMin=V(lo),boundsMax=V(hi),rootRotation=V(go.transform.localEulerAngles),rootScale=V(go.transform.localScale),toes=toes.Select(V).ToArray(),ankles=ankles.Select(V).ToArray()};
|
||||
UnityEngine.Object.DestroyImmediate(baked);
|
||||
a.runtimeAnimatorController=controller;a.applyRootMotion=false;a.cullingMode=AnimatorCullingMode.AlwaysAnimate;a.Rebind();a.Update(0);
|
||||
float minGap=float.MaxValue,maxGap=float.MinValue;int crossed=0;
|
||||
for(int i=0;i<125;i++)
|
||||
{
|
||||
a.Play("Base Layer.walk",0,(i/120f)/walk.motion.averageDuration);a.Update(0);
|
||||
var left=go.transform.InverseTransformPoint(a.GetBoneTransform(HumanBodyBones.LeftFoot).position);var right=go.transform.InverseTransformPoint(a.GetBoneTransform(HumanBodyBones.RightFoot).position);
|
||||
float gap=right.x-left.x;minGap=Mathf.Min(minGap,gap);maxGap=Mathf.Max(maxGap,gap);if(gap<0)crossed++;
|
||||
}
|
||||
if(crossed>0)throw new Exception("Crossed ankles: "+path);
|
||||
records.Add(new{path,avatar=a.avatar.name,a.avatar.isHuman,a.avatar.isValid,bones=skin.bones.Length,vertices=skin.sharedMesh.vertexCount,triangles=skin.sharedMesh.triangles.Length/3,
|
||||
uv=skin.sharedMesh.uv.Length,maxInfluences,material=AssetDatabase.GetAssetPath(skin.sharedMaterial),shader=skin.sharedMaterial.shader.name,rest,
|
||||
walk=new{clip=AssetDatabase.GetAssetPath(walk.motion),footIK=walk.iKOnFeet,samples=125,minGap,maxGap,crossed}});
|
||||
a.Play("Base Layer.walk",0,.2666667f/walk.motion.averageDuration);a.Update(0);
|
||||
var hips=a.GetBoneTransform(HumanBodyBones.Hips);var hp=hips.position;hp.x=go.transform.position.x;hp.z=0;hips.position=hp;
|
||||
}
|
||||
Render(scene);
|
||||
var json=JsonConvert.SerializeObject(new{records,scenes=Enumerable.Range(0,SceneManager.sceneCount).Select(i=>{var s=SceneManager.GetSceneAt(i);return new{s.path,s.isDirty};}).ToArray()},Formatting.Indented);
|
||||
File.WriteAllText(Path.Combine(Output,"unity_validation.json"),json);return json;
|
||||
}
|
||||
finally{EditorSceneManager.ClosePreviewScene(scene);}
|
||||
}
|
||||
static void Render(Scene scene)
|
||||
{
|
||||
var light=new GameObject("ExportAuditLight").AddComponent<Light>();SceneManager.MoveGameObjectToScene(light.gameObject,scene);light.type=LightType.Directional;light.intensity=1.3f;light.transform.rotation=Quaternion.Euler(35,140,0);
|
||||
var camera=new GameObject("ExportAuditCamera").AddComponent<Camera>();SceneManager.MoveGameObjectToScene(camera.gameObject,scene);camera.scene=scene;camera.orthographic=true;camera.orthographicSize=.73f;
|
||||
camera.transform.position=new Vector3(0,.61f,5);camera.transform.LookAt(new Vector3(0,.61f,0));camera.clearFlags=CameraClearFlags.SolidColor;camera.backgroundColor=new Color(.22f,.25f,.29f);camera.nearClipPlane=.01f;camera.farClipPlane=20;
|
||||
var rt=new RenderTexture(1080,800,24);var tex=new Texture2D(1080,800,TextureFormat.RGB24,false);var previous=RenderTexture.active;
|
||||
try{camera.targetTexture=rt;camera.Render();RenderTexture.active=rt;tex.ReadPixels(new Rect(0,0,1080,800),0,0);tex.Apply();File.WriteAllBytes(Path.Combine(Output,"unity_walking.png"),tex.EncodeToPNG());}
|
||||
finally{RenderTexture.active=previous;camera.targetTexture=null;rt.Release();UnityEngine.Object.DestroyImmediate(rt);UnityEngine.Object.DestroyImmediate(tex);}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,41 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Rendering;
|
||||
using UnityEngine.Rendering.Universal;
|
||||
using UnityEngine.SceneManagement;
|
||||
|
||||
// Read-only diagnosis of the actual running camp, without rendering an extra camera frame.
|
||||
public static class InspectCampLighting
|
||||
{
|
||||
public static string Run()
|
||||
{
|
||||
var pipeline = GraphicsSettings.currentRenderPipeline as UniversalRenderPipelineAsset;
|
||||
var cameras = UnityEngine.Object.FindObjectsOfType<Camera>(true);
|
||||
var lights = UnityEngine.Object.FindObjectsOfType<Light>(true);
|
||||
var renderers = UnityEngine.Object.FindObjectsOfType<Renderer>(true).Where(r => r.gameObject.layer == 29).ToArray();
|
||||
var report = new {
|
||||
time = DateTime.UtcNow, activeScene = SceneManager.GetActiveScene().path,
|
||||
scenes = Enumerable.Range(0,SceneManager.sceneCount).Select(i=>new { SceneManager.GetSceneAt(i).path, SceneManager.GetSceneAt(i).isLoaded }).ToArray(),
|
||||
maps = LightmapSettings.lightmaps.Length, ambient = RenderSettings.ambientMode.ToString(),
|
||||
sky = RenderSettings.ambientSkyColor.ToString(), equator = RenderSettings.ambientEquatorColor.ToString(),
|
||||
sun = RenderSettings.sun != null ? RenderSettings.sun.name : null,
|
||||
pipeline = pipeline != null ? pipeline.name : null, mainShadows = pipeline != null && pipeline.supportsMainLightShadows,
|
||||
shadowDistance = pipeline != null ? pipeline.shadowDistance : 0,
|
||||
cameras = cameras.Select(c => new { c.name, c.enabled, active = c.gameObject.activeInHierarchy,
|
||||
c.cullingMask, target = c.targetTexture != null ? c.targetTexture.name : null,
|
||||
shadows = c.GetComponent<UniversalAdditionalCameraData>()?.renderShadows }).ToArray(),
|
||||
lights = lights.Select(l=>new { l.name, l.enabled, active = l.gameObject.activeInHierarchy, l.intensity, l.cullingMask,
|
||||
bake=l.lightmapBakeType.ToString(), shadows=l.shadows.ToString(), l.shadowStrength }).ToArray(),
|
||||
rendererCount=renderers.Length, lightmapped=renderers.Count(r=>r.lightmapIndex>=0 && r.lightmapIndex<LightmapSettings.lightmaps.Length),
|
||||
renderers=renderers.Take(12).Select(r=>new {r.name, r.lightmapIndex, shadows=r.shadowCastingMode.ToString(), r.receiveShadows,
|
||||
shader=r.sharedMaterial!=null?r.sharedMaterial.shader.name:null}).ToArray()
|
||||
};
|
||||
string json = JsonConvert.SerializeObject(report,Formatting.Indented);
|
||||
string folder=Path.GetFullPath(Path.Combine(Application.dataPath,"../../Tools/output/camp_lighting"));
|
||||
Directory.CreateDirectory(folder); File.WriteAllText(Path.Combine(folder,"runtime_diagnosis.json"),json);
|
||||
return json;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Collections;
|
||||
using Newtonsoft.Json;
|
||||
using UnityEngine;
|
||||
|
||||
public static class InspectHomeCampAot
|
||||
{
|
||||
public static string BuildAndCheck()
|
||||
{
|
||||
var built = AOT.Editor.YooAssetTools.BuildAndStageHotUpdate.BuildAndStage();
|
||||
if (!built.StartsWith("ok", StringComparison.Ordinal)) throw new Exception(built);
|
||||
return built + " | " + CheckHotUpdate();
|
||||
}
|
||||
|
||||
public static string CheckHotUpdate()
|
||||
{
|
||||
var cecil = AppDomain.CurrentDomain.GetAssemblies().First(a => a.GetName().Name == "Mono.Cecil");
|
||||
var definition = cecil.GetType("Mono.Cecil.AssemblyDefinition");
|
||||
var stripped = Path.GetFullPath(Path.Combine(Application.dataPath, "../Library/Bee/artifacts/WebGL/ManagedStripped/YooAsset.dll"));
|
||||
object original = definition.GetMethod("ReadAssembly", new[] { typeof(string) }).Invoke(null, new object[] { stripped });
|
||||
object update = definition.GetMethod("ReadAssembly", new[] { typeof(string) }).Invoke(null, new object[] { Path.Combine(Application.dataPath, "Bundles/Raw/HotUpdate.Runtime.bytes") });
|
||||
var calls = new System.Collections.Generic.List<string>();
|
||||
using ((IDisposable)original)
|
||||
using ((IDisposable)update)
|
||||
{
|
||||
var oldModule = Property(original, "MainModule");
|
||||
var module = Property(update, "MainModule");
|
||||
var references = (IEnumerable)module.GetType().GetMethod("GetMemberReferences", Type.EmptyTypes).Invoke(module, null);
|
||||
foreach (object reference in references)
|
||||
{
|
||||
var declaring = Property(reference, "DeclaringType");
|
||||
string typeName = (string)Property(declaring, "FullName");
|
||||
if (typeName != "YooAsset.SceneHandle" && typeName != "YooAsset.HandleBase") continue;
|
||||
string fullName = (string)Property(reference, "FullName");
|
||||
object type = oldModule.GetType().GetMethod("GetType", new[] { typeof(string) }).Invoke(oldModule, new object[] { typeName });
|
||||
var retained = ((IEnumerable)Property(type, "Methods")).Cast<object>().Any(m => (string)Property(m, "FullName") == fullName);
|
||||
if (!retained) throw new Exception("Hot update uses a method absent from the shipped shell: " + fullName);
|
||||
calls.Add(fullName);
|
||||
}
|
||||
}
|
||||
var report = JsonConvert.SerializeObject(new { passed=true, baseline=stripped, retainedCalls=calls });
|
||||
var output = Path.GetFullPath(Path.Combine(Application.dataPath, "../../Tools/output/homecamp_aot_compatibility"));
|
||||
Directory.CreateDirectory(output); File.WriteAllText(Path.Combine(output, "aot_check.json"), report);
|
||||
return report;
|
||||
}
|
||||
public static string Run()
|
||||
{
|
||||
var directory = Path.GetFullPath(Path.Combine(Application.dataPath, "../Library/Bee/artifacts/WebGL/ManagedStripped"));
|
||||
var cecil = AppDomain.CurrentDomain.GetAssemblies().First(a => a.GetName().Name == "Mono.Cecil");
|
||||
var definition = cecil.GetType("Mono.Cecil.AssemblyDefinition");
|
||||
var output = new System.Collections.Generic.List<object>();
|
||||
foreach (var file in new[] { "YooAsset.dll", "UnityEngine.CoreModule.dll" })
|
||||
{
|
||||
object assembly = definition.GetMethod("ReadAssembly", new[] { typeof(string) }).Invoke(null, new object[] { Path.Combine(directory, file) });
|
||||
using ((IDisposable)assembly)
|
||||
{
|
||||
object module = Property(assembly, "MainModule");
|
||||
foreach (var name in new[] { "YooAsset.SceneHandle", "YooAsset.HandleBase", "UnityEngine.SceneManagement.SceneManager" })
|
||||
{
|
||||
object type = module.GetType().GetMethod("GetType", new[] { typeof(string) }).Invoke(module, new object[] { name });
|
||||
if (type != null) output.Add(new { file, written = File.GetLastWriteTimeUtc(Path.Combine(directory, file)), type = name,
|
||||
methods = ((IEnumerable)Property(type, "Methods")).Cast<object>().Select(m => Property(m, "FullName")).ToArray() });
|
||||
}
|
||||
}
|
||||
}
|
||||
return JsonConvert.SerializeObject(output);
|
||||
}
|
||||
static object Property(object value, string name) => value.GetType().GetProperty(name).GetValue(value);
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
#if UNITY_EDITOR
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
public static class InspectLmGirlMaterials
|
||||
{
|
||||
static string PathOf(UnityEngine.Object obj)=>obj?AssetDatabase.GetAssetPath(obj):null;
|
||||
static object MaterialInfo(Material material)=>material?new{name=material.name,path=PathOf(material),shader=material.shader?material.shader.name:null,
|
||||
textures=material.GetTexturePropertyNames().Select(p=>new{property=p,texture=PathOf(material.GetTexture(p))}).ToArray()}:null;
|
||||
static object Renderers(GameObject obj)=>obj.GetComponentsInChildren<Renderer>(true).Select(r=>new{r.name,
|
||||
mesh=r is SkinnedMeshRenderer skin&&skin.sharedMesh?new{name=skin.sharedMesh.name,vertices=skin.sharedMesh.vertexCount,uv=skin.sharedMesh.HasVertexAttribute(UnityEngine.Rendering.VertexAttribute.TexCoord0)}:null,
|
||||
materials=r.sharedMaterials.Select(MaterialInfo).ToArray()}).ToArray();
|
||||
public static string Inspect()
|
||||
{
|
||||
const string path="Assets/Bundles/Characters/Human/test/girl/LmGirlTpose.fbx";
|
||||
var importer=(ModelImporter)AssetImporter.GetAtPath(path);
|
||||
var model=AssetDatabase.LoadAssetAtPath<GameObject>(path);
|
||||
var prefab=AssetDatabase.LoadAssetAtPath<GameObject>("Assets/Bundles/Characters/Human/test/girl/Girl.prefab");
|
||||
var report=new{importer=new{importer.materialImportMode,importer.materialLocation,importer.materialSearch,
|
||||
remaps=importer.GetExternalObjectMap().Select(p=>new{p.Key.name,path=PathOf(p.Value)}).ToArray()},
|
||||
fbx=Renderers(model),prefab=prefab?Renderers(prefab):null,
|
||||
embeddedMaterials=AssetDatabase.LoadAllAssetsAtPath(path).OfType<Material>().Select(MaterialInfo).ToArray(),
|
||||
availableReviewMaterial=MaterialInfo(AssetDatabase.LoadAssetAtPath<Material>("Assets/Review/CharacterAnimationLab/LmGirl/Generated/LmGirlTextured.mat")),
|
||||
scene=UnityEngine.Object.FindObjectsOfType<Animator>(true).Where(a=>a.name.IndexOf("LmGirl",StringComparison.OrdinalIgnoreCase)>=0).Select(a=>new{a.name,renderers=Renderers(a.gameObject)}).ToArray()};
|
||||
var json=JsonConvert.SerializeObject(report,Formatting.Indented);
|
||||
var directory=System.IO.Path.Combine(Directory.GetParent(Application.dataPath).Parent.FullName,"Tools/output/lmgirl_tpose_studio/material_audit");
|
||||
Directory.CreateDirectory(directory);File.WriteAllText(System.IO.Path.Combine(directory,"unity_materials.json"),json);return json;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,132 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using AIGame.EditorTools;
|
||||
using AOT.Editor.YooAssetTools;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UnityEngine.EventSystems;
|
||||
using System.Collections.Generic;
|
||||
|
||||
public static class IntegrateHomeCamp
|
||||
{
|
||||
public static string Status() => "frame=" + Time.frameCount + " paused=" + EditorApplication.isPaused + " background=" + Application.runInBackground;
|
||||
public static string ExplorerStatus()
|
||||
{
|
||||
var presenter = UnityEngine.Object.FindObjectOfType<HotUpdate.UI.HomeExplorerPresenter>();
|
||||
var stage = GameObject.Find("HomeExplorerStage");
|
||||
if (!stage) return "No explorer stage";
|
||||
var animator = stage.GetComponentInChildren<Animator>(true);
|
||||
return "ready=" + (presenter && presenter.Ready) + " animator=" + (animator != null) + " controller=" + (animator && animator.runtimeAnimatorController ? animator.runtimeAnimatorController.name : "null") + " avatar=" + (animator && animator.avatar ? animator.avatar.name : "null");
|
||||
}
|
||||
public static string EnablePreviewFrames() { Application.runInBackground = true; return Status(); }
|
||||
public static async System.Threading.Tasks.Task<string> CheckArtManifest()
|
||||
{
|
||||
if (!EditorApplication.isPlaying) throw new InvalidOperationException("Play Mode required");
|
||||
if (!YooAsset.YooAssets.IsInitialized) YooAsset.YooAssets.Initialize();
|
||||
if (!YooAsset.YooAssets.TryGetPackage("Art", out var package)) package = YooAsset.YooAssets.CreatePackage("Art");
|
||||
var build = YooAsset.EditorSimulateBuildInvoker.Build("Art", (int)YooAsset.EBundleType.VirtualAssetBundle);
|
||||
var init = package.InitializePackageAsync(new YooAsset.EditorSimulateModeOptions {
|
||||
EditorFileSystemParameters = YooAsset.FileSystemParameters.CreateDefaultEditorFileSystemParameters(build.PackageRootDirectory)
|
||||
});
|
||||
await init;
|
||||
if (init.Status != YooAsset.EOperationStatus.Succeeded) throw new InvalidOperationException(init.Error);
|
||||
var version = package.RequestPackageVersionAsync(); await version;
|
||||
if (version.Status != YooAsset.EOperationStatus.Succeeded) throw new InvalidOperationException(version.Error);
|
||||
var manifest = package.LoadPackageManifestAsync(new YooAsset.LoadPackageManifestOptions(version.PackageVersion, 60)); await manifest;
|
||||
if (manifest.Status != YooAsset.EOperationStatus.Succeeded) throw new InvalidOperationException(manifest.Error);
|
||||
if (!package.IsLocationValid("HomeCamp")) throw new InvalidOperationException("HomeCamp missing from Art manifest");
|
||||
return "PASS simulated Art manifest resolves HomeCamp";
|
||||
}
|
||||
public static async System.Threading.Tasks.Task<string> CheckArtSceneLoad()
|
||||
{
|
||||
var manager = HotUpdate.UI.UiManager.Instance;
|
||||
var oldHud = manager.Get("UI_MainHUD");
|
||||
manager.Close("UI_MainHUD", true);
|
||||
for (int i = 0; i < 100 && UnityEngine.SceneManagement.SceneManager.GetSceneByPath("Assets/Bundles/Scenes/Town/HomeCamp/HomeCamp.unity").isLoaded; i++)
|
||||
await System.Threading.Tasks.Task.Delay(50);
|
||||
GameObject hud = null;
|
||||
manager.StartCoroutine(manager.OpenAsync("UI_MainHUD", loaded => hud = loaded));
|
||||
for (int i = 0; i < 200 && !hud; i++) await System.Threading.Tasks.Task.Delay(50);
|
||||
if (!hud) throw new InvalidOperationException("HUD reload failed");
|
||||
var view = hud.GetComponent<HotUpdate.UI.MainHudView>() ?? hud.AddComponent<HotUpdate.UI.MainHudView>();
|
||||
view.Bind();
|
||||
for (int i = 0; i < 200 && (!hud || !hud.GetComponentInChildren<HotUpdate.UI.HomeCampPresenter>(true) || !hud.GetComponentInChildren<HotUpdate.UI.HomeCampPresenter>(true).Ready); i++)
|
||||
await System.Threading.Tasks.Task.Delay(50);
|
||||
var presenter = hud.GetComponentInChildren<HotUpdate.UI.HomeCampPresenter>(true);
|
||||
var field = typeof(HotUpdate.UI.HomeCampPresenter).GetField("_handle", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
|
||||
if (presenter == null || !presenter.Ready || !(field.GetValue(presenter) is YooAsset.SceneHandle handle) || handle.Status != YooAsset.EOperationStatus.Succeeded)
|
||||
throw new InvalidOperationException("YooAsset scene handle did not load HomeCamp");
|
||||
return "PASS YooAsset additive scene load and HUD render";
|
||||
}
|
||||
public static string CheckPreview()
|
||||
{
|
||||
var hud = HotUpdate.UI.UiManager.Instance?.Get("UI_MainHUD");
|
||||
if (!hud) throw new InvalidOperationException("HUD not ready");
|
||||
var camp = hud.GetComponentInChildren<HotUpdate.UI.HomeCampPresenter>(true);
|
||||
if (!camp || !camp.Ready) throw new InvalidOperationException("Camp preview not ready");
|
||||
var image = camp.GetComponent<RawImage>();
|
||||
if (!image.enabled || image.raycastTarget || !(image.texture is RenderTexture rt))
|
||||
throw new InvalidOperationException("Camp render target or hit testing invalid");
|
||||
var camera = GameObject.Find("HomeCampCamera")?.GetComponent<Camera>();
|
||||
if (!camera || camera.targetTexture != rt || !camera.enabled)
|
||||
throw new InvalidOperationException("Camp camera is not rendering into HUD");
|
||||
var explorer = hud.transform.Find("ExplorerStage");
|
||||
if (!explorer || explorer.gameObject.activeSelf || hud.transform.Find("Btn_Explorer").gameObject.activeSelf)
|
||||
throw new InvalidOperationException("Legacy explorer overlay should be hidden over the 3D camp");
|
||||
hud.SetActive(false);
|
||||
if (camera.enabled && camera.gameObject.activeInHierarchy) throw new InvalidOperationException("Hidden HUD still renders camp");
|
||||
hud.SetActive(true);
|
||||
if (!camera.enabled || !camera.gameObject.activeInHierarchy) throw new InvalidOperationException("Camp did not resume");
|
||||
return "PASS camp render " + rt.width + "x" + rt.height + "; FOV=" + camera.fieldOfView + "; legacy overlay hidden; hide/resume passed";
|
||||
}
|
||||
public static string Capture()
|
||||
{
|
||||
var hud = HotUpdate.UI.UiManager.Instance.Get("UI_MainHUD");
|
||||
var camera = GameObject.Find("HomeMobileTestCamera").GetComponent<Camera>();
|
||||
var canvas = hud.GetComponent<Canvas>();
|
||||
Canvas.ForceUpdateCanvases();
|
||||
foreach (var button in hud.transform.Find("Layout_Top").GetComponentsInChildren<Button>())
|
||||
{
|
||||
var rect = (RectTransform)button.transform;
|
||||
var hits = new List<RaycastResult>();
|
||||
EventSystem.current.RaycastAll(new PointerEventData(EventSystem.current) { position = rect.TransformPoint(rect.rect.center) }, hits);
|
||||
if (hits.Count == 0 || ExecuteEvents.GetEventHandler<IPointerClickHandler>(hits[0].gameObject) != button.gameObject)
|
||||
throw new InvalidOperationException("Header hit blocked: " + button.name);
|
||||
}
|
||||
var target = new RenderTexture(Screen.width, Screen.height, 24);
|
||||
var pixels = new Texture2D(Screen.width, Screen.height, TextureFormat.RGB24, false);
|
||||
var mode = canvas.renderMode; var canvasCamera = canvas.worldCamera; var mask = camera.cullingMask;
|
||||
var previous = RenderTexture.active;
|
||||
try
|
||||
{
|
||||
camera.targetTexture = target; camera.cullingMask = 1 << LayerMask.NameToLayer("UI");
|
||||
canvas.renderMode = RenderMode.ScreenSpaceCamera; canvas.worldCamera = camera; canvas.planeDistance = 1;
|
||||
Canvas.ForceUpdateCanvases(); camera.Render(); RenderTexture.active = target;
|
||||
pixels.ReadPixels(new Rect(0, 0, Screen.width, Screen.height), 0, 0); pixels.Apply();
|
||||
var dir = Path.GetFullPath(Path.Combine(Application.dataPath, "../../Tools/output/homecamp_integration"));
|
||||
Directory.CreateDirectory(dir); var path = Path.Combine(dir, "home_" + Screen.width + "x" + Screen.height + ".png");
|
||||
File.WriteAllBytes(path, pixels.EncodeToPNG()); return "PASS header hit targets; " + path;
|
||||
}
|
||||
finally
|
||||
{
|
||||
RenderTexture.active = previous; camera.targetTexture = null; camera.cullingMask = mask;
|
||||
canvas.renderMode = mode; canvas.worldCamera = canvasCamera;
|
||||
target.Release(); UnityEngine.Object.Destroy(target); UnityEngine.Object.Destroy(pixels);
|
||||
}
|
||||
}
|
||||
public static string Build()
|
||||
{
|
||||
if (EditorApplication.isPlaying) throw new InvalidOperationException("Stop Play Mode before building the HUD.");
|
||||
var json = Path.GetFullPath(Path.Combine(Application.dataPath, "../../Doc/UIPrefabCreater/UI_MainHUD.json"));
|
||||
var validation = JsonUIPrefabBuilder.Validate(json);
|
||||
if (!validation.Ok) throw new InvalidOperationException(string.Join("\n", validation.Errors));
|
||||
var path = JsonUIPrefabBuilder.BuildFromJsonFile(json);
|
||||
var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(path);
|
||||
if (prefab == null || prefab.transform.Find("CampWorld")?.GetComponent<RawImage>() == null)
|
||||
throw new InvalidOperationException("HUD CampWorld missing");
|
||||
if (prefab.transform.Find("CampWorld").GetComponent<RawImage>().raycastTarget)
|
||||
throw new InvalidOperationException("Camp image must not intercept buttons");
|
||||
return "HUD: " + path + "; " + BuildAndStageArt.EnsureLoadableScenePrefabs();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
using AOT.Editor.YooAssetTools;
|
||||
|
||||
public static class PublishCdn
|
||||
{
|
||||
public static string Build() => CdnPublisher.BuildPackages();
|
||||
public static string Status() => CdnPublisher.Status;
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using AIGame.EditorTools;
|
||||
using AOT.UI;
|
||||
using HotUpdate.Gameplay;
|
||||
using Newtonsoft.Json;
|
||||
using TMPro;
|
||||
using UnityEditor;
|
||||
using UnityEditor.SceneManagement;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using Object = UnityEngine.Object;
|
||||
|
||||
// Run through Unity Pipeline. Uses isolated preview scenes and in-memory saves;
|
||||
// never contacts the game server or modifies player progress.
|
||||
public static class ReviewSecondaryUi
|
||||
{
|
||||
static string Repo => Path.GetFullPath(Path.Combine(Application.dataPath, "../.."));
|
||||
static string Output => Path.Combine(Repo, "Tools/output/secondary_ui");
|
||||
const BindingFlags Private = BindingFlags.Instance | BindingFlags.NonPublic;
|
||||
static void Check(bool valid, string message) { if (!valid) throw new Exception(message); }
|
||||
static void Set(object target, string field, object value) => target.GetType().GetField(field, Private).SetValue(target, value);
|
||||
static void Refresh(PrototypeFlow flow, string page) => typeof(PrototypeFlow).GetMethod("Refresh" + page, Private).Invoke(flow, null);
|
||||
|
||||
public static string Build()
|
||||
{
|
||||
Check(!EditorApplication.isPlaying, "Exit Play Mode before authoring prefabs.");
|
||||
CreateControls();
|
||||
return BuildPages();
|
||||
}
|
||||
|
||||
public static string BuildBackground()
|
||||
{
|
||||
Check(!EditorApplication.isPlaying, "Exit Play Mode before authoring prefabs.");
|
||||
const string path = "Assets/Bundles/UI/Bg/journal_paper_v1.png";
|
||||
const string folder = "Assets/Bundles/UI/Bg";
|
||||
if (!AssetDatabase.IsValidFolder(folder)) AssetDatabase.CreateFolder("Assets/Bundles/UI", "Bg");
|
||||
if (!File.Exists(path)) File.Copy(Path.Combine(Output, "journal_background_generated.png"), path);
|
||||
AssetDatabase.ImportAsset(path, ImportAssetOptions.ForceSynchronousImport);
|
||||
var importer = (TextureImporter)AssetImporter.GetAtPath(path);
|
||||
importer.textureType = TextureImporterType.Sprite;
|
||||
importer.spriteImportMode = SpriteImportMode.Single;
|
||||
importer.spriteBorder = Vector4.zero;
|
||||
importer.mipmapEnabled = false; importer.wrapMode = TextureWrapMode.Clamp;
|
||||
importer.filterMode = FilterMode.Bilinear; importer.maxTextureSize = 2048;
|
||||
importer.npotScale = TextureImporterNPOTScale.None;
|
||||
importer.textureCompression = TextureImporterCompression.CompressedHQ;
|
||||
var settings = new TextureImporterSettings(); importer.ReadTextureSettings(settings);
|
||||
settings.spriteMeshType = SpriteMeshType.FullRect; importer.SetTextureSettings(settings);
|
||||
importer.SaveAndReimport();
|
||||
return BuildPages();
|
||||
}
|
||||
|
||||
static string BuildPages()
|
||||
{
|
||||
foreach (string key in new[] { "Tasks", "Signin", "Mail", "Challenges" })
|
||||
{
|
||||
string json = Path.Combine(Repo, "Doc/UIPrefabCreater/UI_Prototype" + key + ".json");
|
||||
var validation = JsonUIPrefabBuilder.Validate(json);
|
||||
Check(validation.Ok, string.Join("\n", validation.Errors));
|
||||
Check(!string.IsNullOrEmpty(JsonUIPrefabBuilder.BuildFromJsonFile(json)), "Build failed: " + key);
|
||||
}
|
||||
return "Built Tasks, Signin, Mail and Challenges through the JSON authoring pipeline.";
|
||||
}
|
||||
|
||||
static void CreateControls()
|
||||
{
|
||||
const string folder = "Assets/Bundles/UI/Texture/Journal";
|
||||
if (!AssetDatabase.IsValidFolder(folder)) AssetDatabase.CreateFolder("Assets/Bundles/UI/Texture", "Journal");
|
||||
Control("journal_card", "#FFFDF7", "#F8F1E4", "#DFDCCF");
|
||||
Control("journal_hero", "#345D72", "#23465B", "#203D50");
|
||||
Control("journal_gold_card", "#FFF2D4", "#F7DFAD", "#DCBF83");
|
||||
Control("journal_primary_normal", "#FFD987", "#F1B94F", "#D5A044");
|
||||
Control("journal_primary_pressed", "#EDB650", "#DCA43F", "#BB8530");
|
||||
Control("journal_secondary_normal", "#EBF4F7", "#D5E5EC", "#B5CAD4");
|
||||
Control("journal_secondary_pressed", "#C8DDE5", "#B5CCD7", "#99B5C3");
|
||||
Control("journal_disabled", "#E4E8E7", "#D1D8D8", "#BBC7C9");
|
||||
Control("journal_progress_bg", "#D8DFDF", "#D0D9D9", "#C7D2D3");
|
||||
Control("journal_progress_fill", "#F6D48A", "#EDBA58", "#D9A747");
|
||||
}
|
||||
static Color Hex(string hex) { ColorUtility.TryParseHtmlString(hex, out var color); return color; }
|
||||
static float RoundedDistance(float x, float y, float cx, float cy, float hx, float hy, float radius)
|
||||
{
|
||||
float dx = Mathf.Abs(x-cx)-(hx-radius), dy = Mathf.Abs(y-cy)-(hy-radius);
|
||||
return new Vector2(Mathf.Max(dx,0),Mathf.Max(dy,0)).magnitude + Mathf.Min(Mathf.Max(dx,dy),0)-radius;
|
||||
}
|
||||
static void Control(string name, string topHex, string bottomHex, string edgeHex)
|
||||
{
|
||||
string path = "Assets/Bundles/UI/Texture/Journal/" + name + ".png";
|
||||
var texture = new Texture2D(128,128,TextureFormat.RGBA32,false);
|
||||
var top=Hex(topHex); var bottom=Hex(bottomHex); var edge=Hex(edgeHex);
|
||||
for(int y=0;y<128;y++) for(int x=0;x<128;x++)
|
||||
{
|
||||
float distance=RoundedDistance(x+.5f,y+.5f,64,66,60,58,22);
|
||||
float alpha=Mathf.Clamp01(.5f-distance);
|
||||
float shadow=Mathf.Clamp01(.5f-RoundedDistance(x+.5f,y+.5f,64,62,60,58,22))*.13f;
|
||||
Color color=Color.Lerp(bottom,top,Mathf.Clamp01((y-8)/116f));
|
||||
color=Color.Lerp(edge,color,Mathf.Clamp01(-distance-1));
|
||||
// A restrained inner highlight gives the candy controls volume.
|
||||
if(distance < -2 && y > 114) color=Color.Lerp(color,Color.white,.12f);
|
||||
color.a=alpha;
|
||||
if(alpha<1) { var shade=new Color(.12f,.21f,.26f,shadow); color=Color.Lerp(shade,color,alpha); color.a=Mathf.Max(alpha,shadow); }
|
||||
texture.SetPixel(x,y,color);
|
||||
}
|
||||
texture.Apply(); File.WriteAllBytes(path,texture.EncodeToPNG()); Object.DestroyImmediate(texture);
|
||||
AssetDatabase.ImportAsset(path,ImportAssetOptions.ForceSynchronousImport);
|
||||
var importer=(TextureImporter)AssetImporter.GetAtPath(path);
|
||||
importer.textureType=TextureImporterType.Sprite; importer.spriteImportMode=SpriteImportMode.Single;
|
||||
importer.spriteBorder=new Vector4(30,32,30,28); importer.spritePixelsPerUnit=100;
|
||||
importer.alphaIsTransparency=true; importer.mipmapEnabled=false;
|
||||
importer.textureCompression=TextureImporterCompression.Uncompressed;
|
||||
var settings=new TextureImporterSettings(); importer.ReadTextureSettings(settings);
|
||||
settings.spriteMeshType=SpriteMeshType.FullRect; importer.SetTextureSettings(settings); importer.SaveAndReimport();
|
||||
}
|
||||
|
||||
public static string Review()
|
||||
{
|
||||
Directory.CreateDirectory(Output);
|
||||
foreach (string key in new[] { "Tasks", "Signin", "Mail", "Challenges" }) Render(key, 1080, 1920, false);
|
||||
Render("Tasks", 1080, 2400, true);
|
||||
Render("Tasks", 1080, 1680, false);
|
||||
Render("Challenges", 1080, 2400, true);
|
||||
Render("Challenges", 1080, 1680, false);
|
||||
File.WriteAllText(Path.Combine(Output, "verification_journal_bg.txt"),
|
||||
"PASS: JSON prefabs; real RefreshTasks/Signin/Mail/Challenges with isolated save data; task states incomplete/ready/claimed, request blocking and daily reset; signin locked/available/claimed; mail empty/selected; challenges unlocked/detail/start/squad; full-screen safe-area insets; short-screen scroll reachability; journal background serialized sprite and proportional full-screen cover.\nRendered 4 pages and additional aspect/safe-area scenarios.\n");
|
||||
return "PASS. Screenshots and verification: " + Output;
|
||||
}
|
||||
|
||||
static void Render(string key, int width, int height, bool notch)
|
||||
{
|
||||
var scene = EditorSceneManager.NewPreviewScene();
|
||||
var previous = RenderTexture.active;
|
||||
RenderTexture target = null; Texture2D pixels = null;
|
||||
try
|
||||
{
|
||||
var prefab = AssetDatabase.LoadAssetAtPath<GameObject>("Assets/Bundles/UI/Prefab/UI_Prototype" + key + ".prefab");
|
||||
var root = (GameObject)PrefabUtility.InstantiatePrefab(prefab, scene);
|
||||
root.name = prefab.name;
|
||||
var canvas = root.GetComponent<Canvas>();
|
||||
root.GetComponent<CanvasScaler>().enabled = false;
|
||||
var cameraGo = new GameObject("ReviewCamera", typeof(Camera));
|
||||
UnityEngine.SceneManagement.SceneManager.MoveGameObjectToScene(cameraGo, scene);
|
||||
var camera = cameraGo.GetComponent<Camera>(); camera.scene = scene;
|
||||
camera.clearFlags = CameraClearFlags.SolidColor; camera.backgroundColor = Color.white;
|
||||
camera.orthographic = true; camera.orthographicSize = height / 2f;
|
||||
camera.transform.position = new Vector3(0, 0, -10);
|
||||
target = new RenderTexture(width, height, 24); target.Create(); camera.targetTexture = target;
|
||||
canvas.renderMode = RenderMode.ScreenSpaceCamera; canvas.worldCamera = camera;
|
||||
canvas.planeDistance = 1; canvas.scaleFactor = 1;
|
||||
Canvas.ForceUpdateCanvases();
|
||||
|
||||
var background = root.transform.Find("Background");
|
||||
var cover = background.GetComponent<BootArtworkCover>();
|
||||
Check(cover != null && background.GetComponent<Image>().sprite != null, "Missing journal background asset or cover component.");
|
||||
cover.SendMessage("OnEnable"); Canvas.ForceUpdateCanvases();
|
||||
var backgroundRect = (RectTransform)background;
|
||||
var parentRect = (RectTransform)root.transform;
|
||||
float aspect = background.GetComponent<Image>().sprite.rect.width / background.GetComponent<Image>().sprite.rect.height;
|
||||
Check(backgroundRect.rect.width >= parentRect.rect.width - 1 && backgroundRect.rect.height >= parentRect.rect.height - 1,
|
||||
"Journal background does not cover the page.");
|
||||
Check(Mathf.Abs(backgroundRect.rect.width / backgroundRect.rect.height - aspect) < .001f, "Journal background is stretched.");
|
||||
|
||||
var config = JsonConvert.DeserializeObject<PrototypeConfig>(File.ReadAllText(Path.Combine(Repo, "Config/Gameplay/greenfield.json")));
|
||||
var save = new PrototypeSave {
|
||||
schema = 10,
|
||||
clearedStages = new int[20] { 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
|
||||
stages = new int[10] { 1, 1, 1, 1, 1, 0, 0, 0, 0, 0 },
|
||||
shards = new int[10],
|
||||
formation = new int[6] { 0, 1, 2, -1, -1, -1 },
|
||||
decorations = new int[3],
|
||||
campLayout = new int[3] { -1, -1, -1 },
|
||||
dailyProgress = new[] { 1, 1, 1 },
|
||||
dailyClaimed = new[] { 0, 0, 1 },
|
||||
signinDays = 3,
|
||||
signinClaimed = new[] { 1, 1, 0, 0, 0, 0, 0 },
|
||||
shopBought = new int[3],
|
||||
chests = new int[3],
|
||||
challengeBadges = new[] { 1, 0, 0, 0, 0 },
|
||||
challengeBestMs = new[] { 4200, 0, 0, 0, 0 },
|
||||
cleared = true, goalClaimed = true,
|
||||
mails = new[] {
|
||||
new PrototypeMail { mailId="review_welcome", title="欢迎来到小镇", body="亲爱的探险家:\n欢迎回到营地!送上一份冒险补给,愿你和伙伴度过愉快的一天。", sentAt=100000, expiresAt=200000, reward=new StageReward { balls=10 } },
|
||||
new PrototypeMail { mailId="review_gift", title="伙伴们的见面礼", body="带上伙伴,一起出发吧。", read=true, sentAt=100000, expiresAt=200000, reward=new StageReward { coins=100 } },
|
||||
new PrototypeMail { mailId="review_notice", title="营地生活小贴士", body="每日委托会在零点更新。", read=true, sentAt=100000, expiresAt=200000, reward=new StageReward() }
|
||||
}
|
||||
};
|
||||
var flowGo = new GameObject("ReviewFlow");
|
||||
UnityEngine.SceneManagement.SceneManager.MoveGameObjectToScene(flowGo, scene);
|
||||
var flow = flowGo.AddComponent<PrototypeFlow>(); flow.enabled = false;
|
||||
var panelType = typeof(PrototypeFlow).GetNestedType("Panel", BindingFlags.NonPublic);
|
||||
Set(flow, "_page", Activator.CreateInstance(panelType, new object[] { root }));
|
||||
Set(flow, "_game", new PrototypeGame(save)); Set(flow, "_config", config);
|
||||
var reply = new PrototypeReply { daily = new DailyStatus { resetAt = 125200 } };
|
||||
Set(flow, "_reply", reply); Set(flow, "_serverUnix", 100000L);
|
||||
Set(flow, "_receivedAt", Time.realtimeSinceStartupAsDouble);
|
||||
Set(flow, "_mailSelected", "review_welcome");
|
||||
Refresh(flow, key);
|
||||
var all = root.GetComponentsInChildren<Transform>(true).Where(t => t.GetComponent<TMP_SubMeshUI>() == null).ToDictionary(t => t.name);
|
||||
Func<string, Button> button = name => all["Btn_" + name].GetComponent<Button>();
|
||||
if (key == "Tasks")
|
||||
{
|
||||
Check(button("GoDaily0").gameObject.activeSelf && !button("ClaimDaily0").gameObject.activeSelf, "Incomplete task must have only Go.");
|
||||
Check(button("ClaimDaily1").gameObject.activeSelf && !button("GoDaily1").gameObject.activeSelf, "Ready task must have only Claim.");
|
||||
Check(all["Txt_DailyDone2"].gameObject.activeSelf && !button("GoDaily2").gameObject.activeSelf && !button("ClaimDaily2").gameObject.activeSelf, "Claimed task must be a status label.");
|
||||
Check(all["Txt_GoalDone"].gameObject.activeSelf && !button("ClaimGoal").gameObject.activeSelf, "Claimed growth task must be a status label.");
|
||||
Set(flow, "_busy", true); Refresh(flow, key);
|
||||
Check(!button("GoDaily0").interactable && !button("ClaimDaily1").interactable, "Busy state must block actions.");
|
||||
Set(flow, "_busy", false); reply.daily.resetAt = 99999; Refresh(flow, key);
|
||||
Check(!button("GoDaily0").interactable && !button("ClaimDaily1").interactable, "Reset must block stale daily actions.");
|
||||
reply.daily.resetAt = 125200; Refresh(flow, key);
|
||||
}
|
||||
else if (key == "Signin")
|
||||
{
|
||||
Check(!button("Signin0").gameObject.activeSelf && button("Signin2").gameObject.activeSelf && !button("Signin3").gameObject.activeSelf,
|
||||
"Signin claimed/available/locked state mismatch.");
|
||||
}
|
||||
else if (key == "Mail")
|
||||
{
|
||||
Check(button("ClaimMail").interactable && all["MailUnread0"].gameObject.activeSelf, "Selected unread reward mail state mismatch.");
|
||||
var mails = save.mails; save.mails = new PrototypeMail[0]; Refresh(flow, key);
|
||||
Check(all["Txt_MailEmpty"].gameObject.activeSelf && !button("ClaimAllMail").interactable, "Empty mailbox state mismatch.");
|
||||
save.mails = mails; Refresh(flow, key);
|
||||
}
|
||||
else if (key == "Challenges")
|
||||
{
|
||||
Check(all["Txt_Summary"].gameObject.activeSelf, "Summary text missing.");
|
||||
Check(all["Txt_Detail"].gameObject.activeSelf, "Detail text missing.");
|
||||
Check(button("Start").gameObject.activeSelf, "Start button missing.");
|
||||
Check(button("Challenge0").gameObject.activeSelf, "Challenge0 button missing.");
|
||||
Check(button("Capture").gameObject.activeSelf, "Capture button missing.");
|
||||
Check(button("Fish").gameObject.activeSelf, "Fish button missing.");
|
||||
}
|
||||
|
||||
var safe = root.GetComponentInChildren<UiSafeArea>();
|
||||
UiSafeArea.SetPreviewInsets(notch ? new Vector4(0, .04f, 0, .11f) : Vector4.zero);
|
||||
safe.SendMessage("OnEnable"); safe.SendMessage("LateUpdate");
|
||||
Canvas.ForceUpdateCanvases();
|
||||
if (key == "Mail")
|
||||
{
|
||||
Check(((RectTransform)all["Btn_Mail0"]).rect.width >= 990, "Mail list cards must fill the stack width.");
|
||||
Check(((RectTransform)all["MailDetail"]).rect.width >= 990, "Letter panel must fill the stack width.");
|
||||
}
|
||||
if (notch)
|
||||
{
|
||||
var safeRect = (RectTransform)safe.transform;
|
||||
Check(Math.Abs(safeRect.offsetMax.y + height * .11f) < 2, "Safe-area top inset missing.");
|
||||
Check(Math.Abs(safeRect.offsetMin.y - height * .04f) < 2, "Safe-area bottom inset missing.");
|
||||
}
|
||||
var scroll = all["Scroll_Page"].GetComponent<ScrollRect>();
|
||||
if (height == 1680)
|
||||
{
|
||||
Check(scroll.content.rect.height > scroll.viewport.rect.height, "Short phone needs scrollable content.");
|
||||
scroll.verticalNormalizedPosition = 0; Canvas.ForceUpdateCanvases();
|
||||
Check(Mathf.Abs(scroll.verticalNormalizedPosition) < .01f, "Footer cannot be reached by scrolling.");
|
||||
scroll.verticalNormalizedPosition = 1;
|
||||
}
|
||||
foreach (var text in root.GetComponentsInChildren<TextMeshProUGUI>(true)) text.ForceMeshUpdate(true);
|
||||
Canvas.ForceUpdateCanvases(); camera.Render();
|
||||
RenderTexture.active = target;
|
||||
pixels = new Texture2D(width, height, TextureFormat.RGB24, false);
|
||||
pixels.ReadPixels(new Rect(0, 0, width, height), 0, 0); pixels.Apply();
|
||||
File.WriteAllBytes(Path.Combine(Output, key.ToLowerInvariant() + "_" + width + "x" + height + (notch ? "_safe" : "") + "_journal_v1.png"), pixels.EncodeToPNG());
|
||||
}
|
||||
finally
|
||||
{
|
||||
UiSafeArea.SetPreviewInsets(null); RenderTexture.active = previous;
|
||||
EditorSceneManager.ClosePreviewScene(scene);
|
||||
if (target != null) { target.Release(); Object.DestroyImmediate(target); }
|
||||
if (pixels != null) Object.DestroyImmediate(pixels);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,9 +14,9 @@ public static class SetupCharacter
|
||||
const string ArtPackage = "Art";
|
||||
const float TargetHeight = 1.8f;
|
||||
|
||||
public static string SetupCaoCao()
|
||||
public static string SetupExplorer()
|
||||
{
|
||||
return Run("Human", "CaoCao");
|
||||
return Run("Human", "Explorer", 1.6f);
|
||||
}
|
||||
|
||||
public static string SetupCampGuide()
|
||||
@@ -24,10 +24,7 @@ public static class SetupCharacter
|
||||
return Run("Human", "CampGuide", 1.55f);
|
||||
}
|
||||
|
||||
public static string SetupLmGirl()
|
||||
{
|
||||
return Run("Human", "LmGirl", 1.65f);
|
||||
}
|
||||
|
||||
|
||||
public static string SetupLmCat()
|
||||
{
|
||||
|
||||
@@ -0,0 +1,236 @@
|
||||
#if UNITY_EDITOR
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEditor.Animations;
|
||||
using UnityEngine;
|
||||
|
||||
public static class SetupExplorer
|
||||
{
|
||||
const string Root = "Assets/Bundles/Characters/Human/Explorer";
|
||||
const string FbxPath = Root + "/Explorer.fbx";
|
||||
const string IdleFbxPath = Root + "/Explorer@Idle.fbx";
|
||||
const string WalkFbxPath = Root + "/Explorer@Walk.fbx";
|
||||
const string BaseTexPath = Root + "/Explorer_base.png";
|
||||
const string MixTexPath = Root + "/Explorer_mix.png";
|
||||
const string MatPath = Root + "/Explorer.mat";
|
||||
const string ControllerPath = Root + "/Explorer.controller";
|
||||
const string PrefabPath = Root + "/Explorer.prefab";
|
||||
const float TargetHeight = 1.6f;
|
||||
|
||||
public static string Run()
|
||||
{
|
||||
AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport);
|
||||
SetupTexture(BaseTexPath, true);
|
||||
SetupTexture(MixTexPath, false);
|
||||
|
||||
var modelImporter = (ModelImporter)AssetImporter.GetAtPath(FbxPath);
|
||||
if (modelImporter == null) return "FAIL: ModelImporter not found for " + FbxPath;
|
||||
modelImporter.globalScale = 1.0f;
|
||||
modelImporter.useFileScale = false;
|
||||
modelImporter.bakeAxisConversion = true;
|
||||
modelImporter.materialImportMode = ModelImporterMaterialImportMode.None;
|
||||
modelImporter.animationType = ModelImporterAnimationType.Human;
|
||||
modelImporter.avatarSetup = ModelImporterAvatarSetup.CreateFromThisModel;
|
||||
modelImporter.SaveAndReimport();
|
||||
|
||||
var avatar = AssetDatabase.LoadAssetAtPath<Avatar>(FbxPath);
|
||||
Debug.Log($"[Explorer] Avatar generated: {avatar != null && avatar.isValid}");
|
||||
|
||||
SetupAnimationFbx(IdleFbxPath, avatar, "Idle");
|
||||
SetupAnimationFbx(WalkFbxPath, avatar, "Walk");
|
||||
|
||||
var mat = AssetDatabase.LoadAssetAtPath<Material>(MatPath);
|
||||
var shader = Shader.Find("Test/XPbr") ?? Shader.Find("Universal Render Pipeline/Lit");
|
||||
if (mat == null)
|
||||
{
|
||||
mat = new Material(shader) { name = "Explorer" };
|
||||
AssetDatabase.CreateAsset(mat, MatPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
mat.shader = shader;
|
||||
}
|
||||
|
||||
var baseTex = AssetDatabase.LoadAssetAtPath<Texture2D>(BaseTexPath);
|
||||
var mixTex = AssetDatabase.LoadAssetAtPath<Texture2D>(MixTexPath);
|
||||
if (baseTex != null)
|
||||
{
|
||||
mat.SetTexture("_MainTex", baseTex);
|
||||
mat.SetTexture("_BaseMap", baseTex);
|
||||
}
|
||||
if (mixTex != null)
|
||||
{
|
||||
mat.SetTexture("_MixTex", mixTex);
|
||||
}
|
||||
mat.SetColor("_BaseColor", Color.white);
|
||||
mat.SetColor("_Color", Color.white);
|
||||
EditorUtility.SetDirty(mat);
|
||||
AssetDatabase.SaveAssetIfDirty(mat);
|
||||
|
||||
var controller = AssetDatabase.LoadAssetAtPath<AnimatorController>(ControllerPath);
|
||||
if (controller == null)
|
||||
{
|
||||
controller = AnimatorController.CreateAnimatorControllerAtPath(ControllerPath);
|
||||
}
|
||||
|
||||
if (controller.parameters.All(p => p.name != "isMoving"))
|
||||
controller.AddParameter("isMoving", AnimatorControllerParameterType.Bool);
|
||||
if (controller.parameters.All(p => p.name != "speed"))
|
||||
controller.AddParameter("speed", AnimatorControllerParameterType.Float);
|
||||
if (controller.parameters.All(p => p.name != "Motion"))
|
||||
controller.AddParameter("Motion", AnimatorControllerParameterType.Int);
|
||||
|
||||
var idleClip = LoadClip(IdleFbxPath);
|
||||
var walkClip = LoadClip(WalkFbxPath);
|
||||
|
||||
var rootStateMachine = controller.layers[0].stateMachine;
|
||||
foreach (var s in rootStateMachine.states.ToArray()) rootStateMachine.RemoveState(s.state);
|
||||
|
||||
var idleState = rootStateMachine.AddState("Idle");
|
||||
idleState.motion = idleClip;
|
||||
rootStateMachine.defaultState = idleState;
|
||||
|
||||
var walkState = rootStateMachine.AddState("Walk");
|
||||
walkState.motion = walkClip;
|
||||
|
||||
// Transition: Idle -> Walk
|
||||
var toWalk = idleState.AddTransition(walkState);
|
||||
toWalk.hasExitTime = false;
|
||||
toWalk.duration = 0.15f;
|
||||
toWalk.AddCondition(AnimatorConditionMode.If, 0, "isMoving");
|
||||
|
||||
// Transition: Walk -> Idle
|
||||
var toIdle = walkState.AddTransition(idleState);
|
||||
toIdle.hasExitTime = false;
|
||||
toIdle.duration = 0.15f;
|
||||
toIdle.AddCondition(AnimatorConditionMode.IfNot, 0, "isMoving");
|
||||
|
||||
// Also add Motion transitions for ExplorerLocomotion compatibility
|
||||
var toWalkMotion = idleState.AddTransition(walkState);
|
||||
toWalkMotion.hasExitTime = false;
|
||||
toWalkMotion.duration = 0.15f;
|
||||
toWalkMotion.AddCondition(AnimatorConditionMode.Equals, 1, "Motion");
|
||||
|
||||
var toIdleMotion = walkState.AddTransition(idleState);
|
||||
toIdleMotion.hasExitTime = false;
|
||||
toIdleMotion.duration = 0.15f;
|
||||
toIdleMotion.AddCondition(AnimatorConditionMode.Equals, 0, "Motion");
|
||||
|
||||
EditorUtility.SetDirty(controller);
|
||||
AssetDatabase.SaveAssetIfDirty(controller);
|
||||
|
||||
var fbxPrefab = AssetDatabase.LoadAssetAtPath<GameObject>(FbxPath);
|
||||
var instance = (GameObject)PrefabUtility.InstantiatePrefab(fbxPrefab);
|
||||
instance.name = "Explorer";
|
||||
|
||||
foreach (var r in instance.GetComponentsInChildren<SkinnedMeshRenderer>(true))
|
||||
{
|
||||
r.sharedMaterial = mat;
|
||||
}
|
||||
|
||||
var animator = instance.GetComponent<Animator>();
|
||||
if (animator == null) animator = instance.AddComponent<Animator>();
|
||||
animator.runtimeAnimatorController = controller;
|
||||
if (avatar != null) animator.avatar = avatar;
|
||||
animator.applyRootMotion = false;
|
||||
|
||||
// Add Locomotion Controller
|
||||
var ctrlType = AppDomain.CurrentDomain.GetAssemblies()
|
||||
.Select(a => a.GetType("HotUpdate.Gameplay.CampExplorerController"))
|
||||
.FirstOrDefault(t => t != null);
|
||||
if (ctrlType != null)
|
||||
{
|
||||
if (instance.GetComponent(ctrlType) == null) instance.AddComponent(ctrlType);
|
||||
}
|
||||
|
||||
// Add CapsuleCollider
|
||||
var col = instance.GetComponent<CapsuleCollider>();
|
||||
if (col == null) col = instance.AddComponent<CapsuleCollider>();
|
||||
col.center = new Vector3(0, 0.8f, 0);
|
||||
col.radius = 0.35f;
|
||||
col.height = 1.6f;
|
||||
|
||||
// Set layer 29
|
||||
foreach (var t in instance.GetComponentsInChildren<Transform>(true))
|
||||
{
|
||||
t.gameObject.layer = 29;
|
||||
}
|
||||
|
||||
var renderers = instance.GetComponentsInChildren<Renderer>(true);
|
||||
if (renderers.Length > 0)
|
||||
{
|
||||
var b = renderers[0].bounds;
|
||||
foreach (var r in renderers.Skip(1)) b.Encapsulate(r.bounds);
|
||||
float currentH = b.size.y;
|
||||
if (currentH > 0.01f)
|
||||
{
|
||||
float scale = TargetHeight / currentH;
|
||||
instance.transform.localScale = Vector3.one * scale;
|
||||
}
|
||||
}
|
||||
|
||||
PrefabUtility.SaveAsPrefabAsset(instance, PrefabPath);
|
||||
UnityEngine.Object.DestroyImmediate(instance);
|
||||
|
||||
AssetDatabase.SaveAssets();
|
||||
return $"SUCCESS: Authored {PrefabPath} with Avatar={avatar != null}, Idle={idleClip != null}, Walk={walkClip != null}";
|
||||
}
|
||||
|
||||
static void SetupTexture(string path, bool isSrgb)
|
||||
{
|
||||
var importer = (TextureImporter)AssetImporter.GetAtPath(path);
|
||||
if (importer == null) return;
|
||||
importer.textureType = TextureImporterType.Default;
|
||||
importer.sRGBTexture = isSrgb;
|
||||
importer.alphaIsTransparency = false;
|
||||
importer.mipmapEnabled = true;
|
||||
importer.maxTextureSize = 1024;
|
||||
importer.textureCompression = TextureImporterCompression.Compressed;
|
||||
importer.SaveAndReimport();
|
||||
}
|
||||
|
||||
static void SetupAnimationFbx(string path, Avatar sourceAvatar, string clipName)
|
||||
{
|
||||
var importer = (ModelImporter)AssetImporter.GetAtPath(path);
|
||||
if (importer == null) return;
|
||||
importer.globalScale = 1.0f;
|
||||
importer.importAnimation = true;
|
||||
importer.animationType = ModelImporterAnimationType.Human;
|
||||
if (sourceAvatar != null && sourceAvatar.isValid)
|
||||
{
|
||||
importer.avatarSetup = ModelImporterAvatarSetup.CopyFromOther;
|
||||
importer.sourceAvatar = sourceAvatar;
|
||||
}
|
||||
else
|
||||
{
|
||||
importer.avatarSetup = ModelImporterAvatarSetup.CreateFromThisModel;
|
||||
}
|
||||
|
||||
var clip = new ModelImporterClipAnimation
|
||||
{
|
||||
name = clipName,
|
||||
takeName = "Armature|Scene",
|
||||
loopTime = true,
|
||||
loopPose = true,
|
||||
lockRootRotation = true,
|
||||
lockRootHeightY = true,
|
||||
lockRootPositionXZ = true
|
||||
};
|
||||
importer.clipAnimations = new[] { clip };
|
||||
importer.SaveAndReimport();
|
||||
}
|
||||
|
||||
static AnimationClip LoadClip(string path)
|
||||
{
|
||||
var assets = AssetDatabase.LoadAllAssetsAtPath(path);
|
||||
foreach (var a in assets)
|
||||
{
|
||||
if (a is AnimationClip c && !c.name.StartsWith("__preview__"))
|
||||
return c;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,66 @@
|
||||
#if UNITY_EDITOR
|
||||
using System.IO;
|
||||
using UnityEditor;
|
||||
using UnityEditor.SceneManagement;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Rendering.Universal;
|
||||
|
||||
public static class TestRealtimeShadowCapture
|
||||
{
|
||||
const string ScenePath = "Assets/Bundles/Scenes/Town/HomeCamp/HomeCamp.unity";
|
||||
|
||||
public static string Run()
|
||||
{
|
||||
var scene = EditorSceneManager.OpenScene(ScenePath, OpenSceneMode.Additive);
|
||||
try
|
||||
{
|
||||
var root = scene.GetRootGameObjects()[0];
|
||||
var sun = root.transform.Find("HomeCampSun")?.GetComponent<Light>();
|
||||
if (sun != null)
|
||||
{
|
||||
sun.lightmapBakeType = LightmapBakeType.Realtime;
|
||||
sun.shadows = LightShadows.Soft;
|
||||
sun.shadowStrength = 0.85f;
|
||||
sun.shadowBias = 0.05f;
|
||||
sun.shadowNormalBias = 0.3f;
|
||||
sun.intensity = 1.8f;
|
||||
}
|
||||
|
||||
var cam = root.transform.Find("HomeCampCamera")?.GetComponent<Camera>();
|
||||
if (cam == null) return "Camera missing";
|
||||
|
||||
var uacd = cam.GetComponent<UniversalAdditionalCameraData>();
|
||||
if (uacd == null) uacd = cam.gameObject.AddComponent<UniversalAdditionalCameraData>();
|
||||
uacd.renderShadows = true;
|
||||
|
||||
int w = 720, h = 1280;
|
||||
var rt = new RenderTexture(w, h, 24, RenderTextureFormat.ARGB32) { name = "ShadowTestRT" };
|
||||
rt.Create();
|
||||
cam.targetTexture = rt;
|
||||
cam.Render();
|
||||
|
||||
var old = RenderTexture.active;
|
||||
RenderTexture.active = rt;
|
||||
var tex = new Texture2D(w, h, TextureFormat.RGB24, false);
|
||||
tex.ReadPixels(new Rect(0, 0, w, h), 0, 0);
|
||||
tex.Apply();
|
||||
RenderTexture.active = old;
|
||||
|
||||
cam.targetTexture = null;
|
||||
rt.Release();
|
||||
Object.DestroyImmediate(rt);
|
||||
|
||||
string outPath = Path.Combine(Application.dataPath, "../../Tools/output/shadow_test.png");
|
||||
File.WriteAllBytes(outPath, tex.EncodeToPNG());
|
||||
Object.DestroyImmediate(tex);
|
||||
|
||||
return "Rendered to " + outPath;
|
||||
}
|
||||
finally
|
||||
{
|
||||
EditorSceneManager.CloseScene(scene, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Rendering;
|
||||
using UnityEngine.Rendering.Universal;
|
||||
|
||||
public static class TuneHomeCampExposure
|
||||
{
|
||||
const string ProfilePath = "Assets/Bundles/Scenes/Town/HomeCamp/HomeCampVolume.asset";
|
||||
public static string Apply()
|
||||
{
|
||||
if (EditorApplication.isPlaying) throw new InvalidOperationException("Apply in Edit Mode.");
|
||||
var profile = AssetDatabase.LoadAssetAtPath<VolumeProfile>(ProfilePath);
|
||||
if (!profile || !profile.TryGet<ColorAdjustments>(out var color)) throw new InvalidOperationException("Missing camp ColorAdjustments");
|
||||
float before = color.postExposure.value;
|
||||
Undo.RecordObject(color, "Lower HomeCamp exposure");
|
||||
color.postExposure.Override(.30f);
|
||||
EditorUtility.SetDirty(color);
|
||||
AssetDatabase.SaveAssetIfDirty(color);
|
||||
return "Saved HomeCamp postExposure " + before + " -> " + color.postExposure.value + " EV";
|
||||
}
|
||||
|
||||
public static string Capture()
|
||||
{
|
||||
var hud = HotUpdate.UI.UiManager.Instance?.Get("UI_MainHUD");
|
||||
if (!hud) throw new InvalidOperationException("Start the isolated home preview first.");
|
||||
var camera = GameObject.Find("HomeMobileTestCamera").GetComponent<Camera>();
|
||||
var canvas = hud.GetComponent<Canvas>();
|
||||
var target = new RenderTexture(Screen.width, Screen.height, 24);
|
||||
var pixels = new Texture2D(Screen.width, Screen.height, TextureFormat.RGB24, false);
|
||||
var mode = canvas.renderMode; var worldCamera = canvas.worldCamera; var mask = camera.cullingMask;
|
||||
var previous = RenderTexture.active;
|
||||
try
|
||||
{
|
||||
camera.targetTexture = target; camera.cullingMask = 1 << LayerMask.NameToLayer("UI");
|
||||
canvas.renderMode = RenderMode.ScreenSpaceCamera; canvas.worldCamera = camera; canvas.planeDistance = 1;
|
||||
Canvas.ForceUpdateCanvases(); camera.Render(); RenderTexture.active = target;
|
||||
pixels.ReadPixels(new Rect(0, 0, Screen.width, Screen.height), 0, 0); pixels.Apply();
|
||||
var dir = Path.GetFullPath(Path.Combine(Application.dataPath, "../../Tools/output/homecamp_exposure_20261104"));
|
||||
Directory.CreateDirectory(dir);
|
||||
var path = Path.Combine(dir, "home_exposure_030.png");
|
||||
File.WriteAllBytes(path, pixels.EncodeToPNG());
|
||||
return path;
|
||||
}
|
||||
finally
|
||||
{
|
||||
RenderTexture.active = previous; camera.targetTexture = null; camera.cullingMask = mask;
|
||||
canvas.renderMode = mode; canvas.worldCamera = worldCamera;
|
||||
target.Release(); UnityEngine.Object.Destroy(target); UnityEngine.Object.Destroy(pixels);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
#if UNITY_EDITOR
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using UnityEditor;
|
||||
using UnityEditor.SceneManagement;
|
||||
using UnityEngine;
|
||||
|
||||
public static class VerifyCampInteractionsAndPets
|
||||
{
|
||||
const string ScenePath = "Assets/Bundles/Scenes/Town/HomeCamp/HomeCamp.unity";
|
||||
|
||||
public static string Run()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("=== Automated Verification: Camp Interactions, Building Nav & Pet Wandering ===");
|
||||
bool allPassed = true;
|
||||
|
||||
var scene = EditorSceneManager.OpenScene(ScenePath, OpenSceneMode.Additive);
|
||||
try
|
||||
{
|
||||
var root = scene.GetRootGameObjects().FirstOrDefault(g => g.name == "HomeCamp");
|
||||
if (root == null)
|
||||
{
|
||||
sb.AppendLine("[FAIL] HomeCamp root not found in " + ScenePath);
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
// Phase 1: Entities Existence, Scale & Component Checks
|
||||
sb.AppendLine("\n[Phase 1: Scene Entities, 4x Scale & Component Checks]");
|
||||
var explorerTf = root.transform.Find("Explorer");
|
||||
if (explorerTf == null)
|
||||
{
|
||||
sb.AppendLine("[FAIL] Explorer missing in scene");
|
||||
allPassed = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
var ctrl = explorerTf.GetComponent<HotUpdate.Gameplay.CampExplorerController>();
|
||||
bool scale4x = Mathf.Approximately(explorerTf.localScale.x, 4.0f);
|
||||
if (scale4x)
|
||||
{
|
||||
sb.AppendLine($"[PASS] Explorer scaled 4x ({explorerTf.localScale}), pos={explorerTf.position}, Controller={ctrl != null}, Layer={explorerTf.gameObject.layer}");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendLine($"[FAIL] Explorer scale is {explorerTf.localScale}, expected (4, 4, 4)");
|
||||
allPassed = false;
|
||||
}
|
||||
}
|
||||
|
||||
var petNames = new[] { "Pet_FireCat", "Pet_RockBall", "Pet_BudBud" };
|
||||
foreach (var petName in petNames)
|
||||
{
|
||||
var petTf = root.transform.Find(petName);
|
||||
if (petTf == null)
|
||||
{
|
||||
sb.AppendLine($"[FAIL] Companion {petName} missing in scene");
|
||||
allPassed = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
var wanderer = petTf.GetComponent<HotUpdate.Gameplay.CampPetWanderer>();
|
||||
var anim = petTf.GetComponentInChildren<Animator>();
|
||||
bool hasWanderer = wanderer != null;
|
||||
bool hasAnim = anim != null && anim.runtimeAnimatorController != null;
|
||||
bool correctLayer = petTf.gameObject.layer == 29;
|
||||
bool isEnlarged = petTf.localScale.x >= 2.8f;
|
||||
|
||||
if (hasWanderer && hasAnim && correctLayer && isEnlarged)
|
||||
{
|
||||
sb.AppendLine($"[PASS] {petName} scaled 4x ({petTf.localScale}), Controller={anim.runtimeAnimatorController.name}, Layer={petTf.gameObject.layer}, WanderRadius={wanderer.wanderRadius}m");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendLine($"[FAIL] {petName} check failed: hasWanderer={hasWanderer}, hasAnim={hasAnim}, correctLayer={correctLayer}, isEnlarged={isEnlarged}");
|
||||
allPassed = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 2: Companion Forward-Walking Facing Verification
|
||||
sb.AppendLine("\n[Phase 2: Companion Forward-Walking Facing Verification]");
|
||||
var fireCatTf = root.transform.Find("Pet_FireCat");
|
||||
if (fireCatTf != null)
|
||||
{
|
||||
var wanderer = fireCatTf.GetComponent<HotUpdate.Gameplay.CampPetWanderer>();
|
||||
var anim = fireCatTf.GetComponentInChildren<Animator>();
|
||||
var motionHash = Animator.StringToHash("Motion");
|
||||
|
||||
// Initial state
|
||||
if (!wanderer.IsMoving)
|
||||
{
|
||||
sb.AppendLine("[PASS] Pet_FireCat initially idle (IsMoving=False)");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendLine("[FAIL] Pet_FireCat initially moving");
|
||||
allPassed = false;
|
||||
}
|
||||
|
||||
// Trigger Wander MoveTo
|
||||
Vector3 moveDelta = new Vector3(1.2f, 0, 0.8f);
|
||||
Vector3 petTarget = fireCatTf.position + moveDelta;
|
||||
wanderer.MoveTo(petTarget);
|
||||
|
||||
// Rotate towards movement target using wanderer facing convention
|
||||
Vector3 lookDir = wanderer.modelFacesMinusZ ? -moveDelta.normalized : moveDelta.normalized;
|
||||
fireCatTf.rotation = Quaternion.LookRotation(lookDir, Vector3.up);
|
||||
|
||||
// Companion front face is local -Z (Vector3.back in model space)
|
||||
Vector3 visualFront = fireCatTf.TransformDirection(Vector3.back);
|
||||
float facingError = Vector3.Angle(visualFront, moveDelta.normalized);
|
||||
|
||||
if (wanderer.IsMoving && anim.GetInteger(motionHash) == 1 && facingError < 1.0f)
|
||||
{
|
||||
sb.AppendLine($"[PASS] Pet Walk Facing verified: Model front face (-Z) aligns with move direction (+{moveDelta.normalized}), facingError={facingError:F2}° (NOT walking backwards)");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendLine($"[FAIL] Pet Walk Facing incorrect: facingError={facingError:F2}°, IsMoving={wanderer.IsMoving}");
|
||||
allPassed = false;
|
||||
}
|
||||
|
||||
// Stop Pet
|
||||
wanderer.Stop();
|
||||
if (!wanderer.IsMoving && anim.GetInteger(motionHash) == 0)
|
||||
{
|
||||
sb.AppendLine($"[PASS] Pet Stop triggered: IsMoving=False, Motion parameter = 0 (Idle)");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendLine($"[FAIL] Pet Stop did not return to Motion=0: IsMoving={wanderer.IsMoving}, Motion={anim.GetInteger(motionHash)}");
|
||||
allPassed = false;
|
||||
}
|
||||
|
||||
// Social greeting simulation
|
||||
if (explorerTf != null)
|
||||
{
|
||||
explorerTf.position = fireCatTf.position + new Vector3(1.0f, 0, 0.8f);
|
||||
Vector3 toExplorer = explorerTf.position - fireCatTf.position;
|
||||
toExplorer.y = 0;
|
||||
if (toExplorer.magnitude < wanderer.greetingDistance)
|
||||
{
|
||||
Vector3 greetDir = wanderer.modelFacesMinusZ ? -toExplorer.normalized : toExplorer.normalized;
|
||||
fireCatTf.rotation = Quaternion.LookRotation(greetDir, Vector3.up);
|
||||
Vector3 greetFront = fireCatTf.TransformDirection(Vector3.back);
|
||||
float greetError = Vector3.Angle(greetFront, toExplorer.normalized);
|
||||
if (greetError < 1.0f)
|
||||
{
|
||||
sb.AppendLine($"[PASS] Social Greeting: Pet front face directly faces Explorer (dist={toExplorer.magnitude:F2}m, greetError={greetError:F2}°)");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendLine($"[FAIL] Social Greeting face error: {greetError:F2}°");
|
||||
allPassed = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 3: Building Interaction & Arrival Callback Simulation
|
||||
sb.AppendLine("\n[Phase 3: Building Navigation & Arrival Callback Simulation]");
|
||||
if (explorerTf != null)
|
||||
{
|
||||
var explorerCtrl = explorerTf.GetComponent<HotUpdate.Gameplay.CampExplorerController>();
|
||||
bool arrivalCallbackFired = false;
|
||||
|
||||
Vector3 mainHouseEntrance = new Vector3(0f, 0f, 3.5f);
|
||||
Vector3 mainHouseFacing = Vector3.forward;
|
||||
|
||||
explorerTf.position = new Vector3(0f, 0f, -3.5f);
|
||||
explorerCtrl.MoveTo(mainHouseEntrance, () => { arrivalCallbackFired = true; }, mainHouseFacing);
|
||||
|
||||
if (explorerCtrl.IsMoving)
|
||||
{
|
||||
sb.AppendLine($"[PASS] MoveTo building entrance initiated: IsMoving=True, Target={explorerCtrl.TargetPosition}");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendLine("[FAIL] MoveTo building entrance failed to start");
|
||||
allPassed = false;
|
||||
}
|
||||
|
||||
// Simulate movement until arrival
|
||||
int steps = 0;
|
||||
while (explorerCtrl.IsMoving && steps < 150)
|
||||
{
|
||||
Vector3 toTgt = explorerCtrl.TargetPosition - explorerTf.position;
|
||||
toTgt.y = 0;
|
||||
float dist = toTgt.magnitude;
|
||||
if (dist <= explorerCtrl.stopDistance)
|
||||
{
|
||||
explorerCtrl.Stop();
|
||||
break;
|
||||
}
|
||||
explorerTf.position += toTgt.normalized * Mathf.Min(explorerCtrl.moveSpeed * 0.05f, dist);
|
||||
steps++;
|
||||
}
|
||||
|
||||
float finalDist = Vector2.Distance(new Vector2(explorerTf.position.x, explorerTf.position.z), new Vector2(mainHouseEntrance.x, mainHouseEntrance.z));
|
||||
float facingAngleDiff = Vector3.Angle(explorerTf.forward, mainHouseFacing);
|
||||
|
||||
if (!explorerCtrl.IsMoving && finalDist <= explorerCtrl.stopDistance + 0.05f && arrivalCallbackFired && facingAngleDiff < 5.0f)
|
||||
{
|
||||
sb.AppendLine($"[PASS] Explorer reached MainHouse entrance in {steps} steps! ArrivalCallbackFired=True, FacingAngleDiff={facingAngleDiff:F1}°, FinalPos={explorerTf.position}");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendLine($"[FAIL] Building navigation arrival failed: finalDist={finalDist:F3}, callback={arrivalCallbackFired}, facingDiff={facingAngleDiff:F1}°");
|
||||
allPassed = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 4: Click Indicator Validation
|
||||
sb.AppendLine("\n[Phase 4: Click Indicator Validation]");
|
||||
if (explorerTf != null)
|
||||
{
|
||||
var explorerCtrl = explorerTf.GetComponent<HotUpdate.Gameplay.CampExplorerController>();
|
||||
explorerCtrl.MoveTo(new Vector3(1.5f, 0, -2.0f));
|
||||
var indicator = root.transform.Find("ClickIndicator");
|
||||
if (indicator != null && indicator.gameObject.activeSelf)
|
||||
{
|
||||
sb.AppendLine($"[PASS] ClickIndicator instantiated and active at: {indicator.position}");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendLine("[FAIL] ClickIndicator not active during movement");
|
||||
allPassed = false;
|
||||
}
|
||||
|
||||
explorerCtrl.Stop();
|
||||
if (indicator != null && !indicator.gameObject.activeSelf)
|
||||
{
|
||||
sb.AppendLine("[PASS] ClickIndicator deactivated upon arrival/stop");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendLine("[FAIL] ClickIndicator failed to deactivate upon stop");
|
||||
allPassed = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
EditorSceneManager.CloseScene(scene, true);
|
||||
}
|
||||
|
||||
sb.AppendLine($"\nOverall Result: {(allPassed ? "ALL TESTS PASSED" : "TESTS FAILED")}");
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,223 @@
|
||||
#if UNITY_EDITOR
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using UnityEditor;
|
||||
using UnityEditor.Animations;
|
||||
using UnityEditor.SceneManagement;
|
||||
using UnityEngine;
|
||||
|
||||
public static class VerifyExplorerAndCampLocomotion
|
||||
{
|
||||
const string PrefabPath = "Assets/Bundles/Characters/Human/Explorer/Explorer.prefab";
|
||||
const string ControllerPath = "Assets/Bundles/Characters/Human/Explorer/Explorer.controller";
|
||||
const string ScenePath = "Assets/Bundles/Scenes/Town/HomeCamp/HomeCamp.unity";
|
||||
|
||||
public static string Run()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("=== Automated Verification: Explorer Protagonist & Camp Locomotion ===");
|
||||
bool allPassed = true;
|
||||
|
||||
// Phase 1: Explorer Asset & Rigging Validation
|
||||
sb.AppendLine("\n[Phase 1: Explorer Asset & Controller Validation]");
|
||||
var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(PrefabPath);
|
||||
if (prefab == null)
|
||||
{
|
||||
sb.AppendLine("[FAIL] Explorer.prefab not found at " + PrefabPath);
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
var animator = prefab.GetComponent<Animator>();
|
||||
if (animator == null || animator.avatar == null || !animator.avatar.isValid)
|
||||
{
|
||||
sb.AppendLine("[FAIL] Animator or valid Humanoid Avatar missing on Explorer.prefab");
|
||||
allPassed = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendLine($"[PASS] Explorer.prefab has valid Humanoid Avatar: {animator.avatar.name}");
|
||||
}
|
||||
|
||||
var controller = AssetDatabase.LoadAssetAtPath<AnimatorController>(ControllerPath);
|
||||
if (controller == null)
|
||||
{
|
||||
sb.AppendLine("[FAIL] Explorer.controller not found at " + ControllerPath);
|
||||
allPassed = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
var sm = controller.layers[0].stateMachine;
|
||||
var states = sm.states.Select(s => s.state.name).ToArray();
|
||||
bool hasIdle = states.Contains("Idle");
|
||||
bool hasWalk = states.Contains("Walk");
|
||||
bool hasParam = controller.parameters.Any(p => p.name == "isMoving");
|
||||
if (hasIdle && hasWalk && hasParam)
|
||||
{
|
||||
sb.AppendLine($"[PASS] Explorer.controller configured with states: [{string.Join(", ", states)}], default={sm.defaultState.name}, parameter 'isMoving' present");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendLine($"[FAIL] Explorer.controller states or parameter missing: hasIdle={hasIdle}, hasWalk={hasWalk}, hasParam={hasParam}");
|
||||
allPassed = false;
|
||||
}
|
||||
}
|
||||
|
||||
var smr = prefab.GetComponentInChildren<SkinnedMeshRenderer>();
|
||||
if (smr == null || smr.sharedMaterial == null)
|
||||
{
|
||||
sb.AppendLine("[FAIL] SkinnedMeshRenderer or material missing on Explorer.prefab");
|
||||
allPassed = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
var mat = smr.sharedMaterial;
|
||||
sb.AppendLine($"[PASS] SkinnedMeshRenderer bound to material: {mat.name} (Shader: {mat.shader.name})");
|
||||
}
|
||||
|
||||
// Phase 2: Scene Placement & Locomotion Logic Validation
|
||||
sb.AppendLine("\n[Phase 2: Scene Placement & Locomotion Step Simulation]");
|
||||
var scene = EditorSceneManager.OpenScene(ScenePath, OpenSceneMode.Additive);
|
||||
try
|
||||
{
|
||||
var root = scene.GetRootGameObjects().FirstOrDefault(g => g.name == "HomeCamp");
|
||||
if (root == null)
|
||||
{
|
||||
sb.AppendLine("[FAIL] HomeCamp root not found in " + ScenePath);
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
var explorerTf = root.transform.Find("Explorer");
|
||||
if (explorerTf == null)
|
||||
{
|
||||
sb.AppendLine("[FAIL] Explorer not found under HomeCamp scene root");
|
||||
allPassed = false;
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
sb.AppendLine($"[PASS] Explorer placed in HomeCamp scene at: {explorerTf.position}, rotation: {explorerTf.eulerAngles}");
|
||||
|
||||
// Find Controller Type via reflection
|
||||
var ctrlType = AppDomain.CurrentDomain.GetAssemblies()
|
||||
.Select(a => a.GetType("HotUpdate.Gameplay.CampExplorerController"))
|
||||
.FirstOrDefault(t => t != null);
|
||||
|
||||
if (ctrlType == null)
|
||||
{
|
||||
sb.AppendLine("[FAIL] Type HotUpdate.Gameplay.CampExplorerController not found in AppDomain");
|
||||
allPassed = false;
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
var ctrl = explorerTf.GetComponent(ctrlType);
|
||||
if (ctrl == null)
|
||||
{
|
||||
sb.AppendLine("[FAIL] CampExplorerController missing on Explorer in scene");
|
||||
allPassed = false;
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
var isMovingProp = ctrlType.GetProperty("IsMoving");
|
||||
var targetPosProp = ctrlType.GetProperty("TargetPosition");
|
||||
var moveToMethod = ctrlType.GetMethod("MoveTo", new[] { typeof(Vector3) });
|
||||
var stopMethod = ctrlType.GetMethod("Stop");
|
||||
var stopDistField = ctrlType.GetField("stopDistance");
|
||||
var moveSpeedField = ctrlType.GetField("moveSpeed");
|
||||
|
||||
bool isMoving = (bool)isMovingProp.GetValue(ctrl);
|
||||
if (isMoving)
|
||||
{
|
||||
sb.AppendLine("[FAIL] Explorer initially moving when should be idle");
|
||||
allPassed = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendLine("[PASS] Initial state: Explorer is idle (IsMoving=False)");
|
||||
}
|
||||
|
||||
// Issue Move Command
|
||||
Vector3 startPos = explorerTf.position;
|
||||
Vector3 targetPos = startPos + new Vector3(2.5f, 0, 1.5f);
|
||||
moveToMethod.Invoke(ctrl, new object[] { targetPos });
|
||||
|
||||
isMoving = (bool)isMovingProp.GetValue(ctrl);
|
||||
Vector3 curTarget = (Vector3)targetPosProp.GetValue(ctrl);
|
||||
if (!isMoving)
|
||||
{
|
||||
sb.AppendLine("[FAIL] MoveTo did not set IsMoving to true");
|
||||
allPassed = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendLine($"[PASS] MoveTo({targetPos}) triggered: IsMoving=True, Target={curTarget}");
|
||||
}
|
||||
|
||||
// Simulate walk
|
||||
float stopDist = (float)stopDistField.GetValue(ctrl);
|
||||
float moveSpeed = (float)moveSpeedField.GetValue(ctrl);
|
||||
float simDt = 0.05f;
|
||||
int frames = 0;
|
||||
while ((bool)isMovingProp.GetValue(ctrl) && frames < 100)
|
||||
{
|
||||
Vector3 toTarget = curTarget - explorerTf.position;
|
||||
toTarget.y = 0;
|
||||
float dist = toTarget.magnitude;
|
||||
if (dist <= stopDist)
|
||||
{
|
||||
stopMethod.Invoke(ctrl, null);
|
||||
break;
|
||||
}
|
||||
explorerTf.rotation = Quaternion.LookRotation(toTarget.normalized, Vector3.up);
|
||||
explorerTf.position += toTarget.normalized * Mathf.Min(moveSpeed * simDt, dist);
|
||||
frames++;
|
||||
}
|
||||
|
||||
isMoving = (bool)isMovingProp.GetValue(ctrl);
|
||||
float finalDist = Vector3.Distance(new Vector3(explorerTf.position.x, 0, explorerTf.position.z), new Vector3(curTarget.x, 0, curTarget.z));
|
||||
if (!isMoving && finalDist <= stopDist + 0.05f)
|
||||
{
|
||||
sb.AppendLine($"[PASS] Explorer successfully walked to target in {frames} steps! Final position: {explorerTf.position}, remaining dist: {finalDist:F3}m, IsMoving={isMoving} (returned to Idle)");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendLine($"[FAIL] Explorer failed to reach target: finalDist={finalDist}, IsMoving={isMoving}");
|
||||
allPassed = false;
|
||||
}
|
||||
|
||||
// Phase 3: Camera Raycast to Ground
|
||||
sb.AppendLine("\n[Phase 3: Camera Raycast to Ground Verification]");
|
||||
var camGo = root.transform.Find("HomeCampCamera")?.gameObject;
|
||||
var camera = camGo?.GetComponent<Camera>();
|
||||
if (camera == null)
|
||||
{
|
||||
sb.AppendLine("[FAIL] HomeCampCamera missing in scene");
|
||||
allPassed = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Raycast from bottom-center of screen (plaza ground)
|
||||
Ray ray = camera.ViewportPointToRay(new Vector3(0.5f, 0.35f, 0));
|
||||
int layerMask = 1 << 29;
|
||||
if (Physics.Raycast(ray, out RaycastHit hit, 150f, layerMask))
|
||||
{
|
||||
sb.AppendLine($"[PASS] Camera raycast hit ground at {hit.point} on collider '{hit.collider.name}' (layer {hit.collider.gameObject.layer})");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendLine("[FAIL] Camera raycast missed ground surface");
|
||||
allPassed = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
EditorSceneManager.CloseScene(scene, true);
|
||||
}
|
||||
|
||||
sb.AppendLine($"\nOverall Result: {(allPassed ? "ALL TESTS PASSED" : "TESTS FAILED")}");
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,232 @@
|
||||
#if UNITY_EDITOR
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using UnityEditor;
|
||||
using UnityEditor.SceneManagement;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using TMPro;
|
||||
using HotUpdate.UI;
|
||||
|
||||
public static class VerifyHomeCampAdaptationAndEntrances
|
||||
{
|
||||
const string ScenePath = "Assets/Bundles/Scenes/Town/HomeCamp/HomeCamp.unity";
|
||||
const string PrefabPath = "Assets/Bundles/UI/Prefab/UI_MainHUD.prefab";
|
||||
const float ReferenceAspect = 1080f / 1920f;
|
||||
|
||||
static readonly (string name, int w, int h)[] TestResolutions = new[]
|
||||
{
|
||||
("Standard 9:16", 1080, 1920),
|
||||
("Long 9:18", 1080, 2160),
|
||||
("iPhone 9:19.5", 1080, 2340),
|
||||
("Android Long 9:20", 1080, 2400),
|
||||
("Ultra Long 9:22", 1080, 2640),
|
||||
("Wide 2:3", 1080, 1620),
|
||||
("Tablet 3:4", 1080, 1440),
|
||||
};
|
||||
|
||||
public static string Run()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("=== Automated Verification: HomeCamp Camera Adaptation & Building Entrances ===");
|
||||
bool allPassed = true;
|
||||
|
||||
// 1. Verify UI Prefab & Bindings
|
||||
sb.AppendLine("\n[Phase 1: UI Prefab & Event Dispatch Verification]");
|
||||
var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(PrefabPath);
|
||||
if (prefab == null) return "FAIL: UI_MainHUD.prefab not found";
|
||||
|
||||
var hudGo = UnityEngine.Object.Instantiate(prefab);
|
||||
try
|
||||
{
|
||||
var hudView = hudGo.GetComponent<MainHudView>() ?? hudGo.AddComponent<MainHudView>();
|
||||
var group = hudGo.transform.Find("Group_CampBuildings");
|
||||
if (group == null)
|
||||
{
|
||||
sb.AppendLine("FAIL: Group_CampBuildings not found in UI_MainHUD.prefab");
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
var triggeredKeys = new List<string>();
|
||||
hudView.OnHomeActionClicked += key => triggeredKeys.Add(key);
|
||||
hudView.Bind();
|
||||
|
||||
var expectedButtons = new (string btnName, string expectedKey, string expectedTitle)[]
|
||||
{
|
||||
("Btn_Building_MainHouse", "Camp", "营地大厅"),
|
||||
("Btn_Building_Shop", "Shop", "小镇商店"),
|
||||
("Btn_Building_Pets", "Pets", "伙伴之家"),
|
||||
("Btn_Building_Fountain", "Rewards", "祈愿喷泉"),
|
||||
("Btn_Building_Stall", "Daily", "集市货摊"),
|
||||
};
|
||||
|
||||
foreach (var (btnName, expectedKey, expectedTitle) in expectedButtons)
|
||||
{
|
||||
var btnTf = group.Find(btnName);
|
||||
if (btnTf == null)
|
||||
{
|
||||
sb.AppendLine($"[FAIL] Button {btnName} not found under Group_CampBuildings");
|
||||
allPassed = false;
|
||||
continue;
|
||||
}
|
||||
var btn = btnTf.GetComponent<Button>();
|
||||
var tmp = btnTf.GetComponentInChildren<TextMeshProUGUI>();
|
||||
if (btn == null)
|
||||
{
|
||||
sb.AppendLine($"[FAIL] Button component missing on {btnName}");
|
||||
allPassed = false;
|
||||
continue;
|
||||
}
|
||||
if (tmp == null || tmp.text != expectedTitle)
|
||||
{
|
||||
sb.AppendLine($"[FAIL] TextMeshProUGUI mismatch on {btnName}: expected '{expectedTitle}', got '{(tmp != null ? tmp.text : "null")}'");
|
||||
allPassed = false;
|
||||
}
|
||||
|
||||
// Simulate click
|
||||
triggeredKeys.Clear();
|
||||
btn.onClick.Invoke();
|
||||
if (triggeredKeys.Count == 1 && triggeredKeys[0] == expectedKey)
|
||||
{
|
||||
sb.AppendLine($"[PASS] {btnName} clicked -> dispatched key '{expectedKey}', title='{tmp?.text}'");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendLine($"[FAIL] {btnName} clicked -> dispatched '{string.Join(",", triggeredKeys)}', expected '{expectedKey}'");
|
||||
allPassed = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Verify Scene Camera & Multi-resolution Adaptation
|
||||
sb.AppendLine("\n[Phase 2: Scene Multi-resolution Adaptation Verification]");
|
||||
var scene = EditorSceneManager.OpenScene(ScenePath, OpenSceneMode.Additive);
|
||||
try
|
||||
{
|
||||
var root = scene.GetRootGameObjects().FirstOrDefault(g => g.name == "HomeCamp");
|
||||
if (root == null)
|
||||
{
|
||||
sb.AppendLine("FAIL: HomeCamp root not found in " + ScenePath);
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
var camGo = root.transform.Find("HomeCampCamera")?.gameObject;
|
||||
var camera = camGo?.GetComponent<Camera>();
|
||||
var backdrop = root.transform.Find("DistantLakeWaterfallCastle");
|
||||
var ground = root.transform.Find("Ground");
|
||||
|
||||
if (camera == null || backdrop == null || ground == null)
|
||||
{
|
||||
sb.AppendLine("FAIL: Missing essential scene elements (Camera/Backdrop/Ground)");
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
// Buildings in scene
|
||||
var buildings = new (string name, Transform tf, Vector3 anchor)[]
|
||||
{
|
||||
("MainHouse", root.transform.Find("MainHouse"), new Vector3(0, 6.7f, 7.2f)),
|
||||
("HouseLeft", root.transform.Find("HouseLeft"), new Vector3(-6.8f, 4.0f, 3.6f)),
|
||||
("HouseRight", root.transform.Find("HouseRight"), new Vector3(7.5f, 4.2f, 4.0f)),
|
||||
("Fountain", root.transform.Find("Fountain"), new Vector3(0, 2.5f, 0.8f)),
|
||||
("StallRight", root.transform.Find("StallRight"), new Vector3(4.5f, 2.6f, 2.8f)),
|
||||
};
|
||||
|
||||
foreach (var b in buildings)
|
||||
{
|
||||
if (b.tf == null)
|
||||
{
|
||||
sb.AppendLine($"[FAIL] Scene building transform missing: {b.name}");
|
||||
allPassed = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Simulate presenter's Resize and Anchoring for each test resolution
|
||||
var hudRt = hudGo.GetComponent<RectTransform>();
|
||||
foreach (var res in TestResolutions)
|
||||
{
|
||||
float aspect = (float)res.w / res.h;
|
||||
float adaptedFov = 2f * Mathf.Atan(Mathf.Tan(25f * Mathf.Deg2Rad) * Mathf.Max(1f, ReferenceAspect / aspect)) * Mathf.Rad2Deg;
|
||||
camera.fieldOfView = adaptedFov;
|
||||
camera.aspect = aspect;
|
||||
|
||||
// Presenter runtime backdrop adaptation
|
||||
const float dist = 75f;
|
||||
float frustumHalfH = dist * Mathf.Tan(adaptedFov * Mathf.Deg2Rad * 0.5f);
|
||||
float frustumHalfW = frustumHalfH * aspect;
|
||||
float bdW = Mathf.Max(72f, frustumHalfW * 2f * 1.15f);
|
||||
float bdH = Mathf.Max(56f, frustumHalfH * 1.35f);
|
||||
float topHeight = frustumHalfH * 1.08f;
|
||||
backdrop.localScale = new Vector3(bdW, bdH, 1f);
|
||||
backdrop.position = camera.transform.position + camera.transform.forward * dist + camera.transform.up * (topHeight - bdH * 0.5f);
|
||||
backdrop.rotation = camera.transform.rotation;
|
||||
|
||||
// Check backdrop bounds in camera space
|
||||
Vector3 bdCamPos = camera.transform.InverseTransformPoint(backdrop.position);
|
||||
float bdHalfW = backdrop.lossyScale.x * 0.5f;
|
||||
float bdHalfH = backdrop.lossyScale.y * 0.5f;
|
||||
float bdMinX = bdCamPos.x - bdHalfW;
|
||||
float bdMaxX = bdCamPos.x + bdHalfW;
|
||||
float bdMaxY = bdCamPos.y + bdHalfH;
|
||||
|
||||
bool leakLeft = bdMinX > -frustumHalfW;
|
||||
bool leakRight = bdMaxX < frustumHalfW;
|
||||
bool leakTop = bdMaxY < frustumHalfH;
|
||||
|
||||
if (leakLeft || leakRight || leakTop)
|
||||
{
|
||||
sb.AppendLine($"[FAIL] {res.name}: Backdrop Leak! Left={leakLeft}, Right={leakRight}, Top={leakTop}");
|
||||
allPassed = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendLine($"[PASS] {res.name} (FOV={adaptedFov:F1}°): Backdrop 100% covers visible frustum (X:[{bdMinX:F1},{bdMaxX:F1}] vs Frustum:[{-frustumHalfW:F1},{frustumHalfW:F1}], Y_top:{bdMaxY:F1} vs Frustum:{frustumHalfH:F1})");
|
||||
}
|
||||
|
||||
// Check building world-to-screen UI anchoring
|
||||
Vector2 canvasSize = new Vector2(1080f, 1080f / aspect);
|
||||
foreach (var (bName, bTf, anchor) in buildings)
|
||||
{
|
||||
Vector3 worldPos = bTf != null ? bTf.position : anchor;
|
||||
worldPos.x = anchor.x; worldPos.y = anchor.y; worldPos.z = anchor.z;
|
||||
Vector3 vp = camera.WorldToViewportPoint(worldPos);
|
||||
|
||||
if (vp.z <= 0)
|
||||
{
|
||||
sb.AppendLine($"[FAIL] {res.name}: {bName} is behind camera");
|
||||
allPassed = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
float x = (vp.x - 0.5f) * canvasSize.x;
|
||||
float y = (vp.y - 0.5f) * canvasSize.y;
|
||||
float halfW = canvasSize.x * 0.5f;
|
||||
float halfH = canvasSize.y * 0.5f;
|
||||
float clampedX = Mathf.Clamp(x, -halfW + 110f + 15f, halfW - 110f - 15f);
|
||||
float clampedY = Mathf.Clamp(y, -halfH + 280f, halfH - 180f);
|
||||
|
||||
// Ensure coordinates are finite and inside safe canvas
|
||||
if (float.IsNaN(clampedX) || float.IsNaN(clampedY) || Mathf.Abs(clampedX) > halfW || Mathf.Abs(clampedY) > halfH)
|
||||
{
|
||||
sb.AppendLine($"[FAIL] {res.name}: {bName} anchoredPosition invalid: ({clampedX}, {clampedY})");
|
||||
allPassed = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
EditorSceneManager.CloseScene(scene, true);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
UnityEngine.Object.DestroyImmediate(hudGo);
|
||||
}
|
||||
|
||||
sb.AppendLine($"\nOverall Result: {(allPassed ? "ALL TESTS PASSED" : "TESTS FAILED")}");
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -1,119 +0,0 @@
|
||||
#if UNITY_EDITOR
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using AIGame.Review;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
using UnityEngine.UI;
|
||||
|
||||
public static class VerifyLmGirlAnimationLab
|
||||
{
|
||||
const string Root="Assets/Review/CharacterAnimationLab";
|
||||
static string Output=>Path.Combine(Directory.GetParent(Application.dataPath).Parent.FullName,"Tools/output/lmgirl_tpose_studio/rig_mixamo41");
|
||||
static void Check(bool ok,string message){if(!ok)throw new InvalidOperationException(message);}
|
||||
static CharacterAnimationLab Viewer()=>UnityEngine.Object.FindObjectOfType<CharacterAnimationLab>();
|
||||
public static string Verify()
|
||||
{
|
||||
var data=JObject.Parse(File.ReadAllText(Path.Combine(Output,"unity_review_data.json")));
|
||||
var expected=JObject.Parse(File.ReadAllText(Path.Combine(Output,"walking_comparison.json")));
|
||||
var root=PrefabUtility.LoadPrefabContents(Root+"/LmGirl/Generated/LmGirlAnimationLab.prefab");
|
||||
var baked=new Mesh();var quality=QualitySettings.skinWeights;
|
||||
try
|
||||
{
|
||||
QualitySettings.skinWeights=SkinWeights.Unlimited;
|
||||
var v=root.GetComponent<CharacterAnimationLab>();v.playing=false;v.inPlace=false;v.motion=0;
|
||||
// Historical bind/weight baseline, deliberately excluding the later
|
||||
// foot-path correction. Active corrected playback has a gait audit.
|
||||
foreach(var actor in v.actors)actor.walkingClip=null;
|
||||
foreach(var actor in v.actors)actor.rig.transform.localScale=Vector3.one;
|
||||
Check(v.clips[0]==AssetDatabase.LoadAssetAtPath<AnimationClip>(Root+"/Generated/Walking.anim"),"Walking clip must be the same asset.");
|
||||
Check(v.actors.Length==4&&v.actors[0].renderer.bones.Length==33,"Website 33-bone actor required.");
|
||||
Check(v.actors.Skip(1).All(a=>a.renderer.bones.Length==41&&a.translationOffsets.Length==41),"TokenRig 41 bones/offsets required.");
|
||||
var results=new List<object>();
|
||||
foreach(var variant in data["variants"])
|
||||
{
|
||||
string name=(string)variant["name"];
|
||||
var mesh=AssetDatabase.LoadAssetAtPath<Mesh>(Root+"/LmGirl/Generated/"+name+".asset");
|
||||
Check(mesh.triangles.Length/3==4999,"Topology changed.");
|
||||
v.actors[1].renderer.sharedMesh=mesh;
|
||||
float[] maxima=new float[3];int[] masks={1,2,4};
|
||||
for(int frame=0;frame<32;frame++)
|
||||
{
|
||||
v.time=frame/30f;v.Sample();v.actors[1].renderer.BakeMesh(baked);var points=baked.vertices;
|
||||
foreach(var edge in variant["auditEdges"])
|
||||
{
|
||||
float ratio=Vector3.Distance(points[(int)edge[0]],points[(int)edge[1]])/(float)edge[2];
|
||||
for(int r=0;r<3;r++)if(((int)edge[3]&masks[r])!=0)maxima[r]=Mathf.Max(maxima[r],ratio);
|
||||
}
|
||||
}
|
||||
string[] regions={"all","hands","neck"};float error=0;
|
||||
for(int r=0;r<3;r++)error=Mathf.Max(error,Mathf.Abs(maxima[r]-(float)expected[name]["maxStretch"][regions[r]]));
|
||||
Check(error<.003f,name+" native comparison mismatch: "+error);
|
||||
results.Add(new{variant=name,frames=32,maxStretch=maxima[0],hands=maxima[1],neck=maxima[2],maxDifference=error});
|
||||
}
|
||||
var website=JObject.Parse(File.ReadAllText(Path.Combine(Output,"website/unity_review_data.json")));
|
||||
var websiteExpected=JObject.Parse(File.ReadAllText(Path.Combine(Output,"website/walking_comparison.json")));
|
||||
Check(v.actors[0].renderer.sharedMesh.GetBonesPerVertex().ToArray().Max()==5,"Website influences were truncated.");
|
||||
float[] siteMax=new float[3];int[] siteMasks={1,2,4};
|
||||
for(int frame=0;frame<32;frame++)
|
||||
{
|
||||
v.time=frame/30f;v.Sample();v.actors[0].renderer.BakeMesh(baked);var points=baked.vertices;
|
||||
foreach(var edge in website["variants"][0]["auditEdges"])
|
||||
{
|
||||
float ratio=Vector3.Distance(points[(int)edge[0]],points[(int)edge[1]])/(float)edge[2];
|
||||
for(int r=0;r<3;r++)if(((int)edge[3]&siteMasks[r])!=0)siteMax[r]=Mathf.Max(siteMax[r],ratio);
|
||||
}
|
||||
}
|
||||
string[] siteRegions={"all","hands","neck"};float siteError=0;
|
||||
for(int r=0;r<3;r++)siteError=Mathf.Max(siteError,Mathf.Abs(siteMax[r]-(float)websiteExpected["maxStretch"][siteRegions[r]]));
|
||||
Check(siteError<.003f,"Website rest-axis mapping mismatch: "+siteError);
|
||||
results.Add(new{variant="mixamo",frames=32,maxStretch=siteMax[0],hands=siteMax[1],neck=siteMax[2],maxDifference=siteError});
|
||||
var report=new{passed=true,samples=192,bones=41,websiteBones=33,websiteMaxInfluences=5,triangles=4999,sameWalkingClip=true,results};
|
||||
File.WriteAllText(Path.Combine(Output,"unity_verification.json"),JsonConvert.SerializeObject(report,Formatting.Indented));
|
||||
return JsonConvert.SerializeObject(report);
|
||||
}
|
||||
finally{QualitySettings.skinWeights=quality;UnityEngine.Object.DestroyImmediate(baked);PrefabUtility.UnloadPrefabContents(root);}
|
||||
}
|
||||
public static string Controls()
|
||||
{
|
||||
var v=Viewer();Check(Application.isPlaying,"Play Mode required.");
|
||||
var buttons=v.ui.GetComponentsInChildren<Button>(true).ToDictionary(b=>b.name);
|
||||
void Click(string name)=>buttons["Btn_"+name].onClick.Invoke();
|
||||
Check(buttons.Count==22,"22 serialized buttons required.");
|
||||
v.motion=0;v.time=0;v.playing=true;v.inPlace=true;v.Sample();Click("Play");Check(!v.playing,"Pause");
|
||||
Click("Next");Check(Mathf.Abs(v.time-1f/30)<.00001f,"Next frame");Click("Previous");Check(v.time==0,"Previous frame");
|
||||
Click("Breathe");Check(v.motion==1,"Breathing");Click("Happy");Check(v.motion==2,"Happy");Click("Walk");
|
||||
foreach(var speed in new[]{"Speed025","Speed05","Speed1","Speed2"})Click(speed);
|
||||
Check(v.speed==2,"Speed");Click("Speed1");
|
||||
var seek=v.ui.GetComponentInChildren<AnimationLabSeekBar>();var corners=new Vector3[4];((RectTransform)seek.transform).GetWorldCorners(corners);
|
||||
seek.OnPointerDown(new PointerEventData(EventSystem.current){position=RectTransformUtility.WorldToScreenPoint(null,Vector3.Lerp(corners[0],corners[2],.5f))});
|
||||
Check(Mathf.Abs(v.time-v.Duration*.5f)<.001f&&!v.playing,"Pointer seek");
|
||||
v.Seek(1);var stationary=v.actors[0].hips.localPosition;Click("Travel");
|
||||
Check(Vector3.Distance(v.actors[0].hips.localPosition-stationary,v.travel[0]*v.actors[0].translationScale)<.00001f,"Proportional travel");Click("Travel");
|
||||
Click("Focus4");Click("Detail");Click("Detail");Check(v.focus==3&&v.detail==2&&v.actors[3].rightHand,"Fourth actor hand close-up");
|
||||
Check(v.actors.Count(a=>a.renderer.enabled)==1,"Focused view must isolate the selected actor.");
|
||||
Click("Material");Check(v.actors.All(a=>a.renderer.sharedMaterial==v.clayMaterial),"Clay");Click("Material");
|
||||
Click("Side");Click("Back");Click("Front");Click("All");
|
||||
Check(v.actors.All(a=>a.renderer.enabled),"All-view must restore four actors.");
|
||||
var first=v.actors[1].renderer.bones;
|
||||
foreach(var actor in v.actors.Skip(2))for(int i=0;i<41;i++)
|
||||
{
|
||||
Check(Vector3.Distance(first[i].localPosition,actor.renderer.bones[i].localPosition)<.00001f,"Pose translations differ.");
|
||||
Check(Mathf.Abs(Quaternion.Dot(first[i].localRotation,actor.renderer.bones[i].localRotation))>.99999f,"Pose rotations differ.");
|
||||
}
|
||||
var result=new{passed=true,buttons=22,checks=new[]{"pause","frame step","three clips","four speeds","pointer seek","scaled in-place travel","fourth actor focus/detail","materials","views","TokenRig 41-bone pose synchronization"}};
|
||||
File.WriteAllText(Path.Combine(Output,"unity_controls.json"),JsonConvert.SerializeObject(result,Formatting.Indented));
|
||||
v.time=0;v.playing=true;v.Sample();return JsonConvert.SerializeObject(result);
|
||||
}
|
||||
public static string Pose(int frame,int focus,int detail,int yaw,bool textured)
|
||||
{
|
||||
var v=Viewer();v.motion=0;v.time=frame/30f;v.playing=false;v.inPlace=true;v.focus=focus;v.detail=detail;v.yaw=yaw;v.textured=textured;v.Sample();
|
||||
return JsonConvert.SerializeObject(new{v.time,v.focus,v.detail,v.yaw,v.textured});
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -1,112 +0,0 @@
|
||||
#if UNITY_EDITOR
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using AIGame.Review;
|
||||
|
||||
public static class VerifyLmGirlGait
|
||||
{
|
||||
const string Root="Assets/Review/CharacterAnimationLab/LmGirl/Generated";
|
||||
static string Output=>Path.Combine(Directory.GetParent(Application.dataPath).Parent.FullName,"Tools/output/lmgirl_tpose_studio/rig_mixamo41");
|
||||
static Vector3 V(JToken t)=>new Vector3((float)t[0],(float)t[1],(float)t[2]);
|
||||
static void Check(bool valid,string message){if(!valid)throw new InvalidOperationException(message);}
|
||||
public static string Verify()
|
||||
{
|
||||
var expected=JObject.Parse(File.ReadAllText(Path.Combine(Output,"gait_correction.json")));
|
||||
var data=JObject.Parse(File.ReadAllText(Path.Combine(Output,"unity_review_data.json")));
|
||||
var website=JObject.Parse(File.ReadAllText(Path.Combine(Output,"website/unity_review_data.json")));
|
||||
var root=PrefabUtility.LoadPrefabContents(Root+"/LmGirlAnimationLab.prefab");
|
||||
var baked=new Mesh();var previousQuality=QualitySettings.skinWeights;
|
||||
try
|
||||
{
|
||||
QualitySettings.skinWeights=SkinWeights.Unlimited;
|
||||
var v=root.GetComponent<CharacterAnimationLab>();v.playing=false;v.inPlace=false;v.motion=0;v.focus=-1;
|
||||
Check(v.actors.All(a=>a.walkingClip&&Mathf.Abs(a.walkingClip.length-v.Duration)<.00001f),"Missing mapped walk or wrong timing.");
|
||||
Check(v.actors.Skip(1).All(a=>a.walkingClip==v.actors[1].walkingClip),"All TokenRig variants must share one corrected motion.");
|
||||
foreach(var a in v.actors)a.rig.transform.localScale=Vector3.one;
|
||||
var bones=v.actors.Select(a=>a.renderer.bones.ToDictionary(b=>b.name.Split(':').Last())).ToArray();
|
||||
float maxPositionError=0,maxLengthDrift=0;var minGaps=Enumerable.Repeat(float.MaxValue,4).ToArray();
|
||||
var lengths=new float[4,4];
|
||||
for(int ai=0;ai<4;ai++)
|
||||
{
|
||||
var bind=ai==0?website:data;
|
||||
var rest=bind["bones"].ToDictionary(b=>(string)b["name"],b=>Matrix(b["bindpose"]).inverse.GetColumn(3));
|
||||
int segment=0;
|
||||
foreach(string side in new[]{"Left","Right"})foreach(var pair in new[]{new[]{"UpLeg","Leg"},new[]{"Leg","Foot"}})
|
||||
lengths[ai,segment++]=Vector3.Distance(rest["mixamorig:"+side+pair[0]],rest["mixamorig:"+side+pair[1]]);
|
||||
}
|
||||
for(int frame=0;frame<32;frame++)
|
||||
{
|
||||
v.time=frame/30f;v.Sample();
|
||||
for(int ai=0;ai<4;ai++)
|
||||
{
|
||||
var a=v.actors[ai];var track=expected["reports"][ai==0?"mixamo":"tokenrig"]["tracks"][frame];
|
||||
foreach(var p in (JObject)track["positions"])
|
||||
{
|
||||
var actual=a.rig.transform.InverseTransformPoint(bones[ai][p.Key].position);
|
||||
maxPositionError=Mathf.Max(maxPositionError,Vector3.Distance(actual,V(p.Value)));
|
||||
}
|
||||
}
|
||||
}
|
||||
Check(maxPositionError<.00005f,"Native mapped pose disagrees with independent IK reference: "+maxPositionError);
|
||||
// 120Hz scan covers interpolation between original 30Hz keyframes.
|
||||
for(int frame=0;frame<=124;frame++)
|
||||
{
|
||||
v.time=frame/120f;v.Sample();
|
||||
for(int ai=0;ai<4;ai++)
|
||||
{
|
||||
var b=bones[ai];var lateral=(b["LeftUpLeg"].position-b["RightUpLeg"].position).normalized;
|
||||
float gap=Vector3.Dot(b["LeftFoot"].position-b["RightFoot"].position,lateral);minGaps[ai]=Mathf.Min(minGaps[ai],gap);
|
||||
int segment=0;
|
||||
foreach(string side in new[]{"Left","Right"})foreach(var pair in new[]{new[]{"UpLeg","Leg"},new[]{"Leg","Foot"}})
|
||||
maxLengthDrift=Mathf.Max(maxLengthDrift,Mathf.Abs(Vector3.Distance(b[side+pair[0]].position,b[side+pair[1]].position)-lengths[ai,segment++]));
|
||||
}
|
||||
}
|
||||
Check(minGaps.All(g=>g>.08f),"Feet cross or become too close: "+string.Join(",",minGaps));
|
||||
Check(maxLengthDrift<.0001f,"Leg segment lengths changed: "+maxLengthDrift);
|
||||
var deformation=new List<object>();
|
||||
foreach(var variant in data["variants"].Concat(website["variants"]))
|
||||
{
|
||||
string name=(string)variant["name"];int ai=name=="mixamo"?0:1;var actor=v.actors[ai];
|
||||
actor.renderer.sharedMesh=AssetDatabase.LoadAssetAtPath<Mesh>(Root+"/"+name+".asset");
|
||||
float[] maxima=new float[3];int[] bits={1,2,4};
|
||||
for(int frame=0;frame<32;frame++)
|
||||
{
|
||||
v.time=frame/30f;v.Sample();actor.renderer.BakeMesh(baked);var vertices=baked.vertices;
|
||||
foreach(var edge in variant["auditEdges"])
|
||||
{
|
||||
float ratio=Vector3.Distance(vertices[(int)edge[0]],vertices[(int)edge[1]])/(float)edge[2];
|
||||
for(int i=0;i<3;i++)if(((int)edge[3]&bits[i])!=0)maxima[i]=Mathf.Max(maxima[i],ratio);
|
||||
}
|
||||
}
|
||||
string[] regions={"all","hands","neck"};float error=0;
|
||||
for(int i=0;i<3;i++)error=Mathf.Max(error,Mathf.Abs(maxima[i]-(float)expected["deformation"][name]["maxStretch"][regions[i]]));
|
||||
Check(error<.003f,"Corrected skin mismatch "+name+": "+error);
|
||||
deformation.Add(new{variant=name,maxStretch=maxima[0],hands=maxima[1],neck=maxima[2],maxDifference=error});
|
||||
}
|
||||
var result=new{passed=true,originalFrames=32,interpolationHz=120,interpolationSamples=125*4,
|
||||
minimumFootSignedGaps=minGaps,maxPositionError,maxLengthDrift,deformationSamples=192,deformation};
|
||||
File.WriteAllText(Path.Combine(Output,"unity_gait_verification.json"),JsonConvert.SerializeObject(result,Formatting.Indented));
|
||||
return JsonConvert.SerializeObject(result);
|
||||
}
|
||||
finally{QualitySettings.skinWeights=previousQuality;UnityEngine.Object.DestroyImmediate(baked);PrefabUtility.UnloadPrefabContents(root);}
|
||||
}
|
||||
static Matrix4x4 Matrix(JToken rows)
|
||||
{
|
||||
var m=new Matrix4x4();for(int r=0;r<4;r++)for(int c=0;c<4;c++)m[r,c]=(float)rows[r][c];return m;
|
||||
}
|
||||
public static string Pose(bool corrected,int frame,int focus)
|
||||
{
|
||||
var v=UnityEngine.Object.FindObjectOfType<CharacterAnimationLab>();
|
||||
for(int i=0;i<v.actors.Length;i++)v.actors[i].walkingClip=corrected?
|
||||
AssetDatabase.LoadAssetAtPath<AnimationClip>(Root+"/Walking_LmGirl"+(i==0?"Mixamo33":"41")+".anim"):null;
|
||||
v.motion=0;v.time=frame/30f;v.playing=false;v.inPlace=true;v.focus=focus;v.detail=0;v.yaw=0;v.textured=false;v.Sample();
|
||||
return corrected?"Corrected foot-path mapping":"Previous angle-only mapping (temporary Play Mode preview)";
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,173 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using AIGame.EditorTools;
|
||||
using AOT.UI;
|
||||
using Newtonsoft.Json;
|
||||
using TMPro;
|
||||
using UnityEditor;
|
||||
using UnityEditor.SceneManagement;
|
||||
using UnityEngine;
|
||||
|
||||
public static class VerifyStartupPresentation
|
||||
{
|
||||
static string Output => Path.GetFullPath(Path.Combine(Application.dataPath, "../../Tools/output/startup_presentation"));
|
||||
public static string Build() => BuildBootLoading.Build();
|
||||
public static string Export()
|
||||
{
|
||||
if (EditorApplication.isPlaying || BuildPipeline.isBuildingPlayer) throw new Exception("Editor must be idle.");
|
||||
if (EditorUserBuildSettings.activeBuildTarget != BuildTarget.WebGL) throw new Exception("Expected WebGL target.");
|
||||
if (BuildBootLoading.ValidateConfigured() != null) throw new Exception(BuildBootLoading.ValidateConfigured());
|
||||
Directory.CreateDirectory(Output);
|
||||
string file = Path.Combine(Output,"export.json");
|
||||
File.WriteAllText(file,JsonConvert.SerializeObject(new {status="building",utc=DateTime.UtcNow}));
|
||||
try
|
||||
{
|
||||
string result = AOT.Editor.WeChat.ExportWeChatBasePackage.Export();
|
||||
bool passed = result.StartsWith("ok minigame=",StringComparison.Ordinal);
|
||||
File.WriteAllText(file,JsonConvert.SerializeObject(new {status=passed?"completed":"failed",result,utc=DateTime.UtcNow},Formatting.Indented));
|
||||
if (!passed) throw new Exception(result);
|
||||
return result;
|
||||
}
|
||||
catch(Exception error)
|
||||
{
|
||||
File.WriteAllText(file,JsonConvert.SerializeObject(new {status="failed",error=error.ToString(),utc=DateTime.UtcNow},Formatting.Indented));
|
||||
throw;
|
||||
}
|
||||
}
|
||||
const string SceneKey = "AIGame.StartupPresentation.Setup";
|
||||
public static string Prepare()
|
||||
{
|
||||
if (EditorApplication.isPlaying) throw new Exception("Stop Play Mode first.");
|
||||
for (int i=0; i<UnityEngine.SceneManagement.SceneManager.sceneCount; i++)
|
||||
if (UnityEngine.SceneManagement.SceneManager.GetSceneAt(i).isDirty) throw new Exception("Existing scene has unsaved changes.");
|
||||
if (!string.IsNullOrEmpty(SessionState.GetString(SceneKey, ""))) throw new Exception("Restore prior preview first.");
|
||||
SessionState.SetString(SceneKey, JsonConvert.SerializeObject(EditorSceneManager.GetSceneManagerSetup()));
|
||||
EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);
|
||||
return Resolution(1080,1920,true);
|
||||
}
|
||||
public static string Restore()
|
||||
{
|
||||
if (EditorApplication.isPlaying) throw new Exception("Stop Play Mode first.");
|
||||
string saved = SessionState.GetString(SceneKey, "");
|
||||
if (string.IsNullOrEmpty(saved)) throw new Exception("No scene setup saved.");
|
||||
EditorSceneManager.RestoreSceneManagerSetup(JsonConvert.DeserializeObject<SceneSetup[]>(saved));
|
||||
var type = typeof(Editor).Assembly.GetType("UnityEditor.GameView");
|
||||
var window = EditorWindow.GetWindow(type);
|
||||
type.GetProperty("selectedSizeIndex", BindingFlags.Instance|BindingFlags.Public|BindingFlags.NonPublic)
|
||||
.SetValue(window,SessionState.GetInt(SceneKey+".Size",0)); window.Repaint();
|
||||
SessionState.EraseString(SceneKey);
|
||||
return "Original scenes and Game View restored.";
|
||||
}
|
||||
public static string Phone() => Resolution(1080,2338,false);
|
||||
public static string Plain() => Resolution(1080,1920,false);
|
||||
static string Resolution(int width, int height, bool remember)
|
||||
{
|
||||
var editor = typeof(Editor).Assembly;
|
||||
var type = editor.GetType("UnityEditor.GameView");
|
||||
var window = EditorWindow.GetWindow(type);
|
||||
var selected = type.GetProperty("selectedSizeIndex",BindingFlags.Instance|BindingFlags.Public|BindingFlags.NonPublic);
|
||||
if (remember) SessionState.SetInt(SceneKey+".Size",(int)selected.GetValue(window));
|
||||
var sizesType = editor.GetType("UnityEditor.GameViewSizes");
|
||||
var sizes = typeof(ScriptableSingleton<>).MakeGenericType(sizesType).GetProperty("instance").GetValue(null);
|
||||
var group = sizesType.GetMethod("GetGroup").Invoke(sizes,new[]{sizesType.GetProperty("currentGroupType").GetValue(sizes)});
|
||||
int count = (int)group.GetType().GetMethod("GetTotalCount").Invoke(group,null), index=count;
|
||||
for (int i=0;i<count;i++)
|
||||
{
|
||||
var size = group.GetType().GetMethod("GetGameViewSize").Invoke(group,new object[]{i});
|
||||
if ((int)size.GetType().GetProperty("width").GetValue(size)==width && (int)size.GetType().GetProperty("height").GetValue(size)==height) { index=i;break; }
|
||||
}
|
||||
if(index==count)
|
||||
{
|
||||
var size = Activator.CreateInstance(editor.GetType("UnityEditor.GameViewSize"),new object[]{Enum.ToObject(editor.GetType("UnityEditor.GameViewSizeType"),1),width,height,"Startup "+width+"x"+height});
|
||||
group.GetType().GetMethod("AddCustomSize").Invoke(group,new[]{size});
|
||||
}
|
||||
selected.SetValue(window,index);window.Repaint();
|
||||
return "Preview resolution "+width+"x"+height;
|
||||
}
|
||||
public static string Start()
|
||||
{
|
||||
if (!EditorApplication.isPlaying) throw new Exception("Enter Play Mode first.");
|
||||
Application.runInBackground = true;
|
||||
var camera = new GameObject("StartupPreviewCamera", typeof(Camera)).GetComponent<Camera>();
|
||||
camera.clearFlags = CameraClearFlags.SolidColor;
|
||||
camera.backgroundColor = new Color(.54f, .72f, .76f);
|
||||
camera.cullingMask = 0;
|
||||
var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(BuildBootLoading.PrefabPath);
|
||||
var view = BootPatchView.Create(prefab.GetComponent<BootPatchView>());
|
||||
view.SetStatus("正在布置营地…");
|
||||
view.SetProgress(.76f);
|
||||
return "Preview started from the real local prefab; no network requests.";
|
||||
}
|
||||
public static string Check()
|
||||
{
|
||||
var view = UnityEngine.Object.FindObjectOfType<BootPatchView>();
|
||||
if (view == null) throw new Exception("Loading preview missing.");
|
||||
var texts = view.GetComponentsInChildren<TMP_Text>();
|
||||
foreach (var text in texts)
|
||||
{
|
||||
text.ForceMeshUpdate();
|
||||
if (!text.font.HasCharacters(text.text)) throw new Exception("Missing glyphs: " + text.text);
|
||||
if (text.isTextOverflowing) throw new Exception("Text overflow: " + text.name);
|
||||
}
|
||||
var image = view.transform.Find("Artwork").GetComponent<UnityEngine.UI.Image>();
|
||||
var canvas = view.GetComponent<RectTransform>();
|
||||
var art = (RectTransform)image.transform;
|
||||
if (art.rect.width + 1 < canvas.rect.width || art.rect.height + 1 < canvas.rect.height)
|
||||
throw new Exception("Artwork does not cover the viewport.");
|
||||
float ratio = image.sprite.rect.width / image.sprite.rect.height;
|
||||
if (Mathf.Abs(art.rect.width / art.rect.height - ratio) > .001f) throw new Exception("Artwork stretched.");
|
||||
var font = texts[0].font;
|
||||
if (font.atlasPopulationMode != AtlasPopulationMode.Static || font.sourceFontFile != null)
|
||||
throw new Exception("Boot font must not depend on runtime glyph generation.");
|
||||
float shown = (float)typeof(BootPatchView).GetField("_shown", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(view);
|
||||
view.SetProgress(.1f);
|
||||
float target = (float)typeof(BootPatchView).GetField("_target", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(view);
|
||||
if (target < .75f) throw new Exception("Progress moved backward.");
|
||||
Directory.CreateDirectory(Output);
|
||||
string report = JsonConvert.SerializeObject(new { passed=true, screen=new [] {Screen.width,Screen.height}, chineseGlyphs=true,
|
||||
noTextOverflow=true, aspectPreserved=true, progressMonotonic=true, shown, target,
|
||||
splash=PlayerSettings.SplashScreen.show, fontAtlas=new[]{font.atlasWidth,font.atlasHeight},
|
||||
bootReference=BuildBootLoading.ValidateConfigured() }, Formatting.Indented);
|
||||
File.WriteAllText(Path.Combine(Output, "validation-" + Screen.width + "x" + Screen.height + ".json"), report);
|
||||
return report;
|
||||
}
|
||||
public static string Complete()
|
||||
{
|
||||
var view = UnityEngine.Object.FindObjectOfType<BootPatchView>();
|
||||
view.SetStatus("准备就绪,即将出发"); view.SetProgress(1f); view.Hide();
|
||||
return "Fade requested; inspect completion on the following frames.";
|
||||
}
|
||||
public static string Capture()
|
||||
{
|
||||
var canvas = UnityEngine.Object.FindObjectOfType<BootPatchView>().GetComponent<Canvas>();
|
||||
var camera = GameObject.Find("StartupPreviewCamera").GetComponent<Camera>();
|
||||
var mode = canvas.renderMode; var worldCamera = canvas.worldCamera; float plane = canvas.planeDistance;
|
||||
var previous = RenderTexture.active; int mask = camera.cullingMask;
|
||||
int width = Screen.width, height = Screen.height;
|
||||
var target = new RenderTexture(width,height,24);
|
||||
var pixels = new Texture2D(width,height,TextureFormat.RGB24,false);
|
||||
string path = Path.Combine(Output,"loading-"+width+"x"+height+".png");
|
||||
try
|
||||
{
|
||||
camera.targetTexture=target; camera.cullingMask=1<<LayerMask.NameToLayer("UI");
|
||||
canvas.renderMode=RenderMode.ScreenSpaceCamera;canvas.worldCamera=camera;canvas.planeDistance=1;
|
||||
Canvas.ForceUpdateCanvases();camera.Render();RenderTexture.active=target;
|
||||
pixels.ReadPixels(new Rect(0,0,width,height),0,0);pixels.Apply();
|
||||
Directory.CreateDirectory(Output);File.WriteAllBytes(path,pixels.EncodeToPNG());
|
||||
}
|
||||
finally
|
||||
{
|
||||
RenderTexture.active=previous;camera.targetTexture=null;camera.cullingMask=mask;
|
||||
canvas.renderMode=mode;canvas.worldCamera=worldCamera;canvas.planeDistance=plane;Canvas.ForceUpdateCanvases();
|
||||
target.Release();UnityEngine.Object.Destroy(target);UnityEngine.Object.Destroy(pixels);
|
||||
}
|
||||
return path;
|
||||
}
|
||||
public static string CheckHidden()
|
||||
{
|
||||
if (UnityEngine.Object.FindObjectOfType<BootPatchView>() != null) throw new Exception("Fade did not finish.");
|
||||
return "PASS: full progress and fade completed; loading overlay inactive.";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user