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:
udx
2026-11-04 16:18:01 +08:00
co-authored by Cursor
parent 9eec364f30
commit 80678ec499
405 changed files with 48053 additions and 420302 deletions
@@ -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.");
+73 -11
View File
@@ -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.
+3 -3
View File
@@ -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);
+41
View File
@@ -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
+132
View File
@@ -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();
}
}
+7
View File
@@ -0,0 +1,7 @@
using AOT.Editor.YooAssetTools;
public static class PublishCdn
{
public static string Build() => CdnPublisher.BuildPackages();
public static string Status() => CdnPublisher.Status;
}
+274
View File
@@ -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);
}
}
}
+3 -6
View File
@@ -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()
{
+236
View File
@@ -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
-112
View File
@@ -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.";
}
}