Replace TownSquare with HomeCamp art and land humanoid bots, LittleBear, and LmGirl review assets.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -18,7 +18,7 @@ public static class AuditCharacterVisual
|
||||
"Tools/output/campguide_review/idle",
|
||||
1.55f,
|
||||
"Assets/Bundles/Characters/Human/CampGuide/Clips/Idle.anim",
|
||||
0.45f);
|
||||
0.45f, -1f); // Existing CampGuide asset uses legacy -Z.
|
||||
}
|
||||
|
||||
public static string CaptureCampGuideWalk()
|
||||
@@ -28,11 +28,12 @@ public static class AuditCharacterVisual
|
||||
"Tools/output/campguide_review/walk",
|
||||
1.55f,
|
||||
"Assets/Bundles/Characters/Human/CampGuide/Clips/Walk.anim",
|
||||
0.3f);
|
||||
0.3f, -1f);
|
||||
}
|
||||
|
||||
public static string Capture(string prefabPath,string outputDirectory,float height=.55f,string clipPath="",float phase=0)
|
||||
public static string Capture(string prefabPath,string outputDirectory,float height=.55f,string clipPath="",float phase=0,float forwardZ=1f)
|
||||
{
|
||||
var directions=ViewDirections(forwardZ);
|
||||
string dir=Path.GetFullPath(Path.Combine(Application.dataPath,"../..",outputDirectory));Directory.CreateDirectory(dir);
|
||||
var scene=EditorSceneManager.NewPreviewScene();
|
||||
try
|
||||
@@ -50,7 +51,7 @@ public static class AuditCharacterVisual
|
||||
int triangles=filters.Sum(m=>m.sharedMesh.triangles.Length/3);
|
||||
if(Mathf.Abs(minY)>.002f || Mathf.Abs(actualHeight-height)>.002f)throw new Exception("Static height/bottom invalid");
|
||||
if(unit.GetComponentsInChildren<Renderer>().Any(r=>r.sharedMaterial==null || r.sharedMaterial.shader.name!="Test/XPbr"))throw new Exception("Static XPbr material missing");
|
||||
File.WriteAllText(Path.Combine(dir,"static_verification.txt"),$"height={actualHeight:F6} bottom={minY:F6} triangles={triangles} XPbr=True; facing verified in front/side/back views");
|
||||
File.WriteAllText(Path.Combine(dir,"static_verification.txt"),$"height={actualHeight:F6} bottom={minY:F6} triangles={triangles} XPbr=True; frontAxis={(forwardZ>0?"+Z":"-Z")}; facing requires visual review of front/side/back views");
|
||||
}
|
||||
if(!string.IsNullOrEmpty(clipPath))
|
||||
{
|
||||
@@ -77,7 +78,6 @@ public static class AuditCharacterVisual
|
||||
var rt=new RenderTexture(640,640,24);var tex=new Texture2D(640,640,TextureFormat.RGB24,false);var previous=RenderTexture.active;
|
||||
try
|
||||
{
|
||||
var directions=new[]{new Vector3(0,.22f,-1),new Vector3(1,.22f,0),new Vector3(0,.22f,1),new Vector3(.7f,.3f,-1)};
|
||||
string[] names={"front","side","back","perspective"};
|
||||
for(int i=0;i<directions.Length;i++)
|
||||
{
|
||||
@@ -94,5 +94,18 @@ public static class AuditCharacterVisual
|
||||
finally {EditorSceneManager.ClosePreviewScene(scene);}
|
||||
return dir;
|
||||
}
|
||||
|
||||
static Vector3[] ViewDirections(float forwardZ = 1f)
|
||||
{
|
||||
if (forwardZ != 1f && forwardZ != -1f) throw new ArgumentException("forwardZ must be +1 or legacy -1");
|
||||
return new[]{new Vector3(0,.22f,forwardZ),new Vector3(1,.22f,0),new Vector3(0,.22f,-forwardZ),new Vector3(.7f,.3f,forwardZ)};
|
||||
}
|
||||
|
||||
public static string VerifyForwardConvention()
|
||||
{
|
||||
if(ViewDirections()[0].z!=1 || ViewDirections()[2].z!=-1 || ViewDirections(-1)[0].z!=-1)
|
||||
throw new Exception("Front/back or legacy view directions are reversed");
|
||||
return "PASS +Z default views and explicit legacy -Z views";
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,305 @@
|
||||
#if UNITY_EDITOR
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
using AIGame.EditorTools;
|
||||
using AIGame.Review;
|
||||
using TMPro;
|
||||
using UnityEditor;
|
||||
using UnityEditor.Animations;
|
||||
using UnityEditor.SceneManagement;
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
using UnityEngine.EventSystems;
|
||||
using UnityEngine.Rendering;
|
||||
using UnityEngine.UI;
|
||||
|
||||
public static class BuildBotAnimationLab
|
||||
{
|
||||
public const string Common = "Assets/Bundles/Characters/Human/Common";
|
||||
public const string Root = "Assets/Review/BotAnimationLab";
|
||||
public const string ScenePath = Root + "/BotAnimationLab.unity";
|
||||
static readonly string[] Names = { "XBot", "YBot", "XBot25", "XBot41" };
|
||||
static string Repo => Directory.GetParent(Application.dataPath).Parent.FullName;
|
||||
static string Output => Path.Combine(Repo, "Tools/output/bot_animation_lab");
|
||||
static void Folder(string path)
|
||||
{
|
||||
if (AssetDatabase.IsValidFolder(path)) return;
|
||||
Folder(Path.GetDirectoryName(path).Replace('\\', '/'));
|
||||
AssetDatabase.CreateFolder(Path.GetDirectoryName(path).Replace('\\', '/'), Path.GetFileName(path));
|
||||
}
|
||||
static void Report(string name, object report)
|
||||
{
|
||||
Directory.CreateDirectory(Output);
|
||||
File.WriteAllText(Path.Combine(Output, name + ".json"), JsonConvert.SerializeObject(report, Formatting.Indented));
|
||||
}
|
||||
public static string Configure()
|
||||
{
|
||||
if (EditorApplication.isPlayingOrWillChangePlaymode) throw new Exception("Edit Mode required");
|
||||
Folder(Root); Folder(Common + "/Prefabs");
|
||||
var reports = new List<object>();
|
||||
float referenceHeight = 0;
|
||||
foreach (string name in Names)
|
||||
{
|
||||
string path = Common + "/" + name + ".fbx";
|
||||
AssetDatabase.ImportAsset(path, ImportAssetOptions.ForceSynchronousImport);
|
||||
var importer = (ModelImporter)AssetImporter.GetAtPath(path);
|
||||
if (importer.animationType != ModelImporterAnimationType.Human || importer.avatarSetup != ModelImporterAvatarSetup.CreateFromThisModel)
|
||||
{
|
||||
importer.animationType = ModelImporterAnimationType.Human;
|
||||
importer.avatarSetup = ModelImporterAvatarSetup.CreateFromThisModel;
|
||||
importer.SaveAndReimport();
|
||||
}
|
||||
var avatar = AssetDatabase.LoadAllAssetsAtPath(path).OfType<Avatar>().Single();
|
||||
if (!avatar.isValid || !avatar.isHuman) throw new Exception(name + " invalid Humanoid Avatar");
|
||||
var model = AssetDatabase.LoadAssetAtPath<GameObject>(path);
|
||||
var bones = model.GetComponentsInChildren<Transform>(true).Where(t => t.name.StartsWith("mixamorig:")).Select(t => t.name).ToArray();
|
||||
var mapped = importer.humanDescription.human;
|
||||
string controllerPath = Common + "/" + (name == "YBot" ? "YBot" : "XBot") + ".controller";
|
||||
var controller = AssetDatabase.LoadAssetAtPath<AnimatorController>(controllerPath);
|
||||
if (!controller) throw new Exception("Missing controller " + controllerPath);
|
||||
var preview = EditorSceneManager.NewPreviewScene();
|
||||
try
|
||||
{
|
||||
var go = (GameObject)PrefabUtility.InstantiatePrefab(model, preview);
|
||||
go.name = name;
|
||||
float sourceHeight = BoundsOf(go).size.y;
|
||||
if (name == "XBot") referenceHeight = sourceHeight;
|
||||
if (name == "XBot25" || name == "XBot41") go.transform.localScale *= referenceHeight / sourceHeight;
|
||||
var animator = go.GetComponent<Animator>();
|
||||
if (!animator) animator = go.AddComponent<Animator>();
|
||||
animator.avatar = avatar; animator.runtimeAnimatorController = controller;
|
||||
animator.applyRootMotion = false; animator.cullingMode = AnimatorCullingMode.AlwaysAnimate;
|
||||
foreach (var skin in go.GetComponentsInChildren<SkinnedMeshRenderer>()) skin.updateWhenOffscreen = true;
|
||||
string prefab = Common + "/Prefabs/" + name + ".prefab";
|
||||
PrefabUtility.SaveAsPrefabAsset(go, prefab);
|
||||
reports.Add(new { name, path, bones = bones.Length, boneNames = bones, mapped = mapped.Select(b => new { b.humanName, b.boneName }),
|
||||
avatar = avatar.name, avatarValid = avatar.isValid, avatarHuman = avatar.isHuman, controller = controllerPath, prefab,
|
||||
sourceHeight, prefabHeight = BoundsOf(go).size.y, prefabScale = new[] { go.transform.localScale.x, go.transform.localScale.y, go.transform.localScale.z },
|
||||
renderers = go.GetComponentsInChildren<SkinnedMeshRenderer>().Select(r => new { r.name, skinBones = r.bones.Length, material = r.sharedMaterial ? r.sharedMaterial.name : null }).ToArray() });
|
||||
}
|
||||
finally { EditorSceneManager.ClosePreviewScene(preview); }
|
||||
}
|
||||
Report("avatar_configuration", reports);
|
||||
return JsonConvert.SerializeObject(reports);
|
||||
}
|
||||
static Material Material(string name, Color color)
|
||||
{
|
||||
Folder(Root + "/Generated");
|
||||
string path = Root + "/Generated/" + name + ".mat";
|
||||
var material = AssetDatabase.LoadAssetAtPath<Material>(path);
|
||||
if (!material)
|
||||
{
|
||||
var shader = Shader.Find("Universal Render Pipeline/Lit") ?? Shader.Find("Standard");
|
||||
material = new Material(shader) { name = name };
|
||||
AssetDatabase.CreateAsset(material, path);
|
||||
}
|
||||
material.color = color;
|
||||
if (material.HasProperty("_Smoothness")) material.SetFloat("_Smoothness", .35f);
|
||||
EditorUtility.SetDirty(material); AssetDatabase.SaveAssetIfDirty(material);
|
||||
return material;
|
||||
}
|
||||
static Bounds BoundsOf(GameObject go)
|
||||
{
|
||||
var renderers = go.GetComponentsInChildren<Renderer>();
|
||||
var bounds = renderers[0].bounds;
|
||||
foreach (var r in renderers.Skip(1)) bounds.Encapsulate(r.bounds);
|
||||
return bounds;
|
||||
}
|
||||
public static string Build()
|
||||
{
|
||||
if (EditorApplication.isPlayingOrWillChangePlaymode) throw new Exception("Edit Mode required");
|
||||
for (int i = 0; i < SceneManager.sceneCount; i++)
|
||||
if (SceneManager.GetSceneAt(i).isDirty) throw new Exception("Unsaved scene: " + SceneManager.GetSceneAt(i).path);
|
||||
Configure();
|
||||
string uiPath = JsonUIPrefabBuilder.BuildFromJsonFile(Path.Combine(Repo, "Doc/UIPrefabCreater/UI_BotAnimationLab.json"));
|
||||
if (string.IsNullOrEmpty(uiPath)) throw new Exception("UI JSON generation failed");
|
||||
var scene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);
|
||||
var root = new GameObject("BotAnimationLab");
|
||||
var viewer = root.AddComponent<BotAnimationLab>();
|
||||
viewer.actors = new BotAnimationLab.Actor[4];
|
||||
viewer.clayMaterial = Material("Clay", new Color(.65f, .69f, .74f));
|
||||
Color[] colors = { new Color(.20f,.60f,.90f), new Color(.96f,.55f,.18f), new Color(.28f,.72f,.43f), new Color(.66f,.42f,.85f) };
|
||||
for (int i = 0; i < Names.Length; i++)
|
||||
{
|
||||
string name = Names[i];
|
||||
var stage = new GameObject("Stage_" + name); stage.transform.SetParent(root.transform, false);
|
||||
stage.transform.localPosition = new Vector3((1.5f - i) * 20f, 0, 0);
|
||||
var go = (GameObject)PrefabUtility.InstantiatePrefab(AssetDatabase.LoadAssetAtPath<GameObject>(Common + "/Prefabs/" + name + ".prefab"), stage.transform);
|
||||
var bounds = BoundsOf(go);
|
||||
go.transform.localScale *= 1.6f / bounds.size.y;
|
||||
bounds = BoundsOf(go);
|
||||
go.transform.position -= new Vector3(bounds.center.x - stage.transform.position.x, bounds.min.y, bounds.center.z);
|
||||
var a = go.GetComponent<Animator>();
|
||||
var skins = go.GetComponentsInChildren<SkinnedMeshRenderer>();
|
||||
var color = Material(name + "Color", colors[i]);
|
||||
var joints = Material(name + "Joints", colors[i] * .6f);
|
||||
foreach (var skin in skins) skin.sharedMaterial = skin.name.Contains("Joint") ? joints : color;
|
||||
var controller = (AnimatorController)a.runtimeAnimatorController;
|
||||
var states = controller.layers[0].stateMachine.states.Select(s => s.state).ToArray();
|
||||
viewer.actors[i] = new BotAnimationLab.Actor { label = name, animator = a, stage = stage.transform, skins = skins,
|
||||
originalMaterials = skins.Select(s => s.sharedMaterial).ToArray(),
|
||||
clips = BotAnimationLab.StateNames.Select(n => (AnimationClip)states.Single(s => s.name == n).motion).ToArray() };
|
||||
var pedestal = GameObject.CreatePrimitive(PrimitiveType.Cylinder); pedestal.name = "Pedestal_" + name;
|
||||
pedestal.transform.SetParent(root.transform, false); pedestal.transform.position = stage.transform.position + new Vector3(0,-.035f,0);
|
||||
pedestal.transform.localScale = new Vector3(1.25f,.035f,1.25f); pedestal.GetComponent<Renderer>().sharedMaterial = color;
|
||||
UnityEngine.Object.DestroyImmediate(pedestal.GetComponent<Collider>());
|
||||
}
|
||||
var floor = GameObject.CreatePrimitive(PrimitiveType.Plane); floor.name = "ReviewFloor"; floor.transform.SetParent(root.transform, false);
|
||||
floor.transform.position = new Vector3(0,-.08f,0); floor.transform.localScale = new Vector3(10,1,1);
|
||||
floor.GetComponent<Renderer>().sharedMaterial = Material("Floor", new Color(.79f,.84f,.87f));
|
||||
UnityEngine.Object.DestroyImmediate(floor.GetComponent<Collider>());
|
||||
var background = new GameObject("BackgroundCamera").AddComponent<Camera>(); background.transform.SetParent(root.transform, false);
|
||||
background.clearFlags = CameraClearFlags.SolidColor; background.backgroundColor = new Color(.82f,.89f,.94f); background.depth = -10; background.cullingMask = 0;
|
||||
var camera = new GameObject("ReviewCamera").AddComponent<Camera>(); camera.transform.SetParent(root.transform, false);
|
||||
camera.tag = "MainCamera"; camera.rect = new Rect(0,.335f,1,.54f); camera.orthographic = true;
|
||||
camera.clearFlags = CameraClearFlags.SolidColor; camera.backgroundColor = background.backgroundColor;
|
||||
camera.nearClipPlane = .05f; camera.farClipPlane = 40; camera.allowHDR = false; camera.gameObject.AddComponent<AudioListener>();
|
||||
viewer.reviewCamera = camera;
|
||||
viewer.comparisonCameras = new Camera[4];
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
var cell = new GameObject("CompareCamera_" + Names[i]).AddComponent<Camera>(); cell.transform.SetParent(root.transform, false);
|
||||
cell.rect = new Rect((i % 2) * .5f, i < 2 ? .605f : .335f, .5f, .27f);
|
||||
cell.orthographic = true; cell.clearFlags = CameraClearFlags.SolidColor; cell.backgroundColor = background.backgroundColor;
|
||||
cell.nearClipPlane = .05f; cell.farClipPlane = 40; cell.allowHDR = false;
|
||||
viewer.comparisonCameras[i] = cell;
|
||||
}
|
||||
foreach (var config in new[] { ("KeyLight", new Vector3(35, -40, 0), 1.3f), ("FillLight", new Vector3(30, 140, 0), .6f) })
|
||||
{
|
||||
var light = new GameObject(config.Item1).AddComponent<Light>(); light.transform.SetParent(root.transform, false);
|
||||
light.type = LightType.Directional; light.transform.rotation = Quaternion.Euler(config.Item2); light.intensity = config.Item3;
|
||||
}
|
||||
RenderSettings.ambientMode = AmbientMode.Flat; RenderSettings.ambientLight = new Color(.65f,.68f,.73f); RenderSettings.fog = false;
|
||||
var ui = (GameObject)PrefabUtility.InstantiatePrefab(AssetDatabase.LoadAssetAtPath<GameObject>(uiPath), root.transform);
|
||||
viewer.ui = ui.transform; ui.GetComponentInChildren<BotAnimationSeekBar>(true).viewer = viewer;
|
||||
var events = new GameObject("EventSystem", typeof(EventSystem), typeof(StandaloneInputModule)); events.transform.SetParent(root.transform, false);
|
||||
viewer.RefreshView();
|
||||
EditorSceneManager.MarkSceneDirty(scene); EditorSceneManager.SaveScene(scene, ScenePath);
|
||||
EditorGUIUtility.PingObject(AssetDatabase.LoadAssetAtPath<SceneAsset>(ScenePath));
|
||||
return Verify();
|
||||
}
|
||||
static void Advance(Animator a, float seconds)
|
||||
{
|
||||
a.speed = 1;
|
||||
for (int i = 0; i < Mathf.CeilToInt(seconds * 60); i++) a.Update(1f / 60);
|
||||
a.speed = 0;
|
||||
}
|
||||
static void Expect(Animator a, string name)
|
||||
{
|
||||
if (!a.GetCurrentAnimatorStateInfo(0).IsName("Base Layer." + name) || a.IsInTransition(0)) throw new Exception("Expected " + name);
|
||||
}
|
||||
public static string Verify()
|
||||
{
|
||||
var reports = new List<object>();
|
||||
foreach (string name in Names)
|
||||
{
|
||||
var scene = EditorSceneManager.NewPreviewScene();
|
||||
var mesh = new Mesh();
|
||||
try
|
||||
{
|
||||
var go = (GameObject)PrefabUtility.InstantiatePrefab(AssetDatabase.LoadAssetAtPath<GameObject>(Common + "/Prefabs/" + name + ".prefab"), scene);
|
||||
var a = go.GetComponent<Animator>();
|
||||
if (!a.avatar || !a.avatar.isHuman || !a.avatar.isValid) throw new Exception(name + " invalid Avatar");
|
||||
var controller = (AnimatorController)a.runtimeAnimatorController;
|
||||
string expected = Common + "/" + (name == "YBot" ? "YBot" : "XBot") + ".controller";
|
||||
if (AssetDatabase.GetAssetPath(controller) != expected) throw new Exception(name + " wrong controller");
|
||||
a.Rebind(); a.speed = 0; a.Update(0);
|
||||
var states = new List<object>();
|
||||
foreach (var child in controller.layers[0].stateMachine.states)
|
||||
{
|
||||
var state = child.state;
|
||||
float minGap = float.MaxValue, maxGap = float.MinValue, motion = 0;
|
||||
Vector3 first = Vector3.zero;
|
||||
for (int i = 0; i < 25; i++)
|
||||
{
|
||||
a.Play("Base Layer." + state.name, 0, i / 25f); a.Update(0);
|
||||
Vector3 hip = a.GetBoneTransform(HumanBodyBones.Hips).position;
|
||||
Vector3 hand = a.GetBoneTransform(HumanBodyBones.LeftHand).position - hip;
|
||||
Vector3 foot = a.GetBoneTransform(HumanBodyBones.LeftFoot).position - hip;
|
||||
if (i == 0) first = hand + foot;
|
||||
motion = Mathf.Max(motion, Vector3.Distance(first, hand + foot));
|
||||
float gap = go.transform.InverseTransformPoint(a.GetBoneTransform(HumanBodyBones.RightFoot).position).x - go.transform.InverseTransformPoint(a.GetBoneTransform(HumanBodyBones.LeftFoot).position).x;
|
||||
minGap = Mathf.Min(minGap, gap); maxGap = Mathf.Max(maxGap, gap);
|
||||
foreach (var skin in go.GetComponentsInChildren<SkinnedMeshRenderer>())
|
||||
{
|
||||
skin.BakeMesh(mesh);
|
||||
if (mesh.vertices.Any(v => float.IsNaN(v.sqrMagnitude) || float.IsInfinity(v.sqrMagnitude))) throw new Exception(name + " invalid skin");
|
||||
}
|
||||
}
|
||||
if (motion < .00001f) throw new Exception(name + " has no motion in " + state.name);
|
||||
states.Add(new { state = state.name, motion, minAnkleGap = minGap, maxAnkleGap = maxGap, samples = 25, source = AssetDatabase.GetAssetPath(state.motion) });
|
||||
}
|
||||
var checks = new List<string>();
|
||||
a.SetInteger("Motion", 0); a.Play("Base Layer.Idle", 0, 0); a.Update(0);
|
||||
a.SetInteger("Motion", 1); Advance(a,.4f); Expect(a,"Walk"); checks.Add("Idle -> Walk");
|
||||
a.SetInteger("Motion", 0); Advance(a,.4f); Expect(a,"Idle"); checks.Add("Walk -> Idle");
|
||||
foreach (int locomotion in new[] { 0, 1 }) foreach (string attack in new[] { "Attack01", "Attack02" })
|
||||
{
|
||||
string rest = locomotion == 0 ? "Idle" : "Walk";
|
||||
a.SetInteger("Motion", locomotion); a.Play("Base Layer." + rest, 0, 0); a.Update(0);
|
||||
a.SetTrigger(attack); Advance(a,.2f); Expect(a,attack);
|
||||
float length = ((AnimationClip)controller.layers[0].stateMachine.states.Single(s=>s.state.name==attack).state.motion).length;
|
||||
Advance(a,length+.4f); Expect(a,rest); checks.Add(rest + " -> " + attack + " -> " + rest);
|
||||
}
|
||||
reports.Add(new { actor = name, controller = expected, avatar = a.avatar.name, validHumanoid = a.isHuman, states, checks });
|
||||
}
|
||||
finally { UnityEngine.Object.DestroyImmediate(mesh); EditorSceneManager.ClosePreviewScene(scene); }
|
||||
}
|
||||
var result = new { passed = true, scene = ScenePath, samples = 400, transitionPaths = 24, actors = reports, verification = "Isolated Editor Animator and BakeMesh" };
|
||||
Report("playback_validation", result);
|
||||
return JsonConvert.SerializeObject(result);
|
||||
}
|
||||
public static string Controls()
|
||||
{
|
||||
if (!Application.isPlaying) throw new Exception("Play Mode required");
|
||||
var v = UnityEngine.Object.FindObjectOfType<BotAnimationLab>();
|
||||
if (!v || v.actors.Length != 4) throw new Exception("Bot scene not active");
|
||||
var buttons = v.ui.GetComponentsInChildren<Button>(true).ToDictionary(b => b.name);
|
||||
void Click(string n) => buttons["Btn_" + n].onClick.Invoke();
|
||||
void Tick(float seconds) { for (int i = 0; i < Mathf.CeilToInt(seconds * 60); i++) v.Tick(1f / 60); }
|
||||
void All(string state) { foreach (var actor in v.actors) Expect(actor.animator, state); }
|
||||
Click("Idle"); v.Seek(0); Click("Walk"); Tick(.4f); All("Walk");
|
||||
Click("Idle"); Tick(.4f); All("Idle");
|
||||
foreach (string attack in new[] { "Attack01", "Attack02" })
|
||||
{
|
||||
Click(attack); Tick(.2f); All(attack); Tick(6); All("Idle");
|
||||
}
|
||||
Click("Walk"); Tick(.4f); Click("Play");
|
||||
if (v.playing) throw new Exception("Pause failed");
|
||||
v.Seek(.2f); float time = v.TimePosition;
|
||||
Click("Next"); if (v.TimePosition <= time) throw new Exception("Next frame failed");
|
||||
Click("Previous"); if (Mathf.Abs(v.TimePosition - time) > .002f) throw new Exception("Previous frame failed");
|
||||
var seek = v.ui.GetComponentInChildren<BotAnimationSeekBar>(true);
|
||||
Canvas.ForceUpdateCanvases(); 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)) });
|
||||
if (Mathf.Abs(v.TimePosition / v.Duration - .5f) > .002f) throw new Exception("Pointer seek failed");
|
||||
foreach (string speed in new[] { "Speed025", "Speed05", "Speed1", "Speed2" }) Click(speed);
|
||||
if (v.speed != 2) throw new Exception("Speed failed"); Click("Speed1");
|
||||
foreach (int i in Enumerable.Range(0,4))
|
||||
{
|
||||
Click("Focus" + (i + 1));
|
||||
if (v.actors.Where((a,j) => a.skins.Any(s => s.enabled) != (j == i)).Any()) throw new Exception("Focus failed");
|
||||
}
|
||||
Click("Detail"); Click("Detail"); if (v.detail != 2) throw new Exception("Hand view failed");
|
||||
Click("All"); Click("Material");
|
||||
if (!v.actors.All(a => a.skins.All(s => s.sharedMaterial == v.clayMaterial))) throw new Exception("Clay failed");
|
||||
Click("Material"); Click("Side"); Click("Back"); Click("Front");
|
||||
Click("Loop"); if (!v.repeatAttacks) throw new Exception("Loop toggle failed");
|
||||
Click("Attack01"); Tick(6);
|
||||
if (v.actors.Any(a=>a.animator.GetCurrentAnimatorStateInfo(0).IsName("Base Layer.Idle") && !a.animator.IsInTransition(0))) throw new Exception("Attack repeat failed");
|
||||
Click("Loop"); Click("Walk"); Tick(.4f); v.Seek(.25f); v.RefreshView();
|
||||
if (v.ui.GetComponentsInChildren<TMP_Text>(true).Any(t => t.text.Contains("Infinity") || t.text.Contains("NaN"))) throw new Exception("Invalid time display");
|
||||
var result = new { passed = true, buttons = buttons.Count, checks = new[] { "locomotion controller transitions", "attack triggers and auto return", "pause", "next/previous frame", "pointer seek", "four speeds", "four actor focus", "hand detail", "gray/color", "front/side/back", "repeat attacks" } };
|
||||
Report("play_controls", result); return JsonConvert.SerializeObject(result);
|
||||
}
|
||||
public static string Pose(int motion, int focus, float phase)
|
||||
{
|
||||
var v = UnityEngine.Object.FindObjectOfType<BotAnimationLab>();
|
||||
v.selectedMotion = motion; v.SetFocus(focus); v.Seek(phase); v.RefreshView();
|
||||
return JsonConvert.SerializeObject(new { v.selectedMotion, v.focus, phase });
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -16,6 +16,7 @@ using UnityEngine.SceneManagement;
|
||||
public sealed class CharacterReviewSubject
|
||||
{
|
||||
public string id, kind, prefab, notes;
|
||||
public string localForward = "+Z";
|
||||
public float expectedHeight = 1.8f, heightTolerance = 0.15f;
|
||||
public int maxTriangles = 3500;
|
||||
public bool requireXpbr = true;
|
||||
@@ -134,6 +135,7 @@ public static class BuildCharacterReview
|
||||
kind = ExtractString(slice, "kind"),
|
||||
prefab = ExtractString(slice, "prefab"),
|
||||
notes = ExtractString(slice, "notes"),
|
||||
localForward = slice.Contains("\"localForward\"") ? ExtractString(slice, "localForward") : "+Z",
|
||||
expectedHeight = ExtractFloat(slice, "expectedHeight", 1.8f),
|
||||
heightTolerance = ExtractFloat(slice, "heightTolerance", 0.15f),
|
||||
maxTriangles = ExtractInt(slice, "maxTriangles"),
|
||||
@@ -177,6 +179,8 @@ public static class BuildCharacterReview
|
||||
{
|
||||
if (string.IsNullOrEmpty(subject.id) || string.IsNullOrEmpty(subject.prefab))
|
||||
throw new Exception("Subject missing id/prefab");
|
||||
if (subject.localForward != "+Z" && subject.localForward != "-Z")
|
||||
throw new Exception("Subject localForward must be +Z or legacy -Z: " + subject.id);
|
||||
if (AssetDatabase.LoadAssetAtPath<GameObject>(Norm(subject.prefab)) == null)
|
||||
throw new Exception("Missing prefab " + subject.prefab);
|
||||
}
|
||||
@@ -207,7 +211,7 @@ public static class BuildCharacterReview
|
||||
instance.name = subject.id;
|
||||
instance.transform.localPosition = new Vector3(i * data.spacing, 0f, 0f);
|
||||
instance.transform.localRotation = Quaternion.identity;
|
||||
AddForwardMarker(instance.transform);
|
||||
AddForwardMarker(instance.transform, subject.localForward);
|
||||
AddNameLabel(instance.transform, subject.id + " / " + subject.kind);
|
||||
EnsureAnimCycle(instance);
|
||||
}
|
||||
@@ -295,12 +299,14 @@ public static class BuildCharacterReview
|
||||
}
|
||||
}
|
||||
|
||||
static void AddForwardMarker(Transform subject)
|
||||
static void AddForwardMarker(Transform subject, string localForward = "+Z")
|
||||
{
|
||||
if (localForward != "+Z" && localForward != "-Z") throw new ArgumentException("Unsupported localForward");
|
||||
var marker = GameObject.CreatePrimitive(PrimitiveType.Cube);
|
||||
marker.name = "ForwardMinusZ";
|
||||
bool legacy = localForward == "-Z";
|
||||
marker.name = legacy ? "ForwardMinusZ" : "ForwardPlusZ";
|
||||
marker.transform.SetParent(subject, false);
|
||||
marker.transform.localPosition = new Vector3(0f, 0.05f, -0.55f);
|
||||
marker.transform.localPosition = new Vector3(0f, 0.05f, legacy ? -0.55f : 0.55f);
|
||||
marker.transform.localScale = new Vector3(0.12f, 0.04f, 0.45f);
|
||||
UnityEngine.Object.DestroyImmediate(marker.GetComponent<Collider>());
|
||||
var renderer = marker.GetComponent<Renderer>();
|
||||
@@ -309,6 +315,17 @@ public static class BuildCharacterReview
|
||||
renderer.sharedMaterial = mat;
|
||||
}
|
||||
|
||||
public static string VerifyForwardConvention()
|
||||
{
|
||||
var subjects=ParseSubjects("[{\"id\":\"new\"},{\"id\":\"legacy\",\"localForward\":\"-Z\"}]");
|
||||
if(subjects.Length!=2 || subjects[0].localForward!="+Z" || subjects[1].localForward!="-Z")
|
||||
throw new Exception("Manifest forward defaults or explicit legacy override failed");
|
||||
var manifest=ReadManifest();
|
||||
if(manifest.subjects.Any(s=>s.localForward!="+Z" && s.localForward!="-Z"))
|
||||
throw new Exception("Invalid declared forward in current manifest");
|
||||
return "PASS +Z defaults and legacy -Z overrides; manifest subjects="+manifest.subjects.Length;
|
||||
}
|
||||
|
||||
static void AddNameLabel(Transform subject, string label)
|
||||
{
|
||||
var go = new GameObject("NameLabel");
|
||||
@@ -454,7 +471,7 @@ public static class BuildCharacterReview
|
||||
var node = root.transform.Find("Subjects/" + subject.id);
|
||||
if (node == null) throw new Exception("Missing subject node " + subject.id);
|
||||
var renderers = node.GetComponentsInChildren<Renderer>(true)
|
||||
.Where(r => r.name != "ForwardMinusZ" && r.name != "NameLabel" && r.GetComponentInParent<TextMesh>() == null && !r.name.StartsWith("Label_", StringComparison.Ordinal))
|
||||
.Where(r => r.name != "ForwardMinusZ" && r.name != "ForwardPlusZ" && r.name != "NameLabel" && r.GetComponentInParent<TextMesh>() == null && !r.name.StartsWith("Label_", StringComparison.Ordinal))
|
||||
.ToArray();
|
||||
if (renderers.Length == 0) throw new Exception(subject.id + " has no mesh renderer");
|
||||
var bounds = ActualBounds(renderers[0]);
|
||||
|
||||
@@ -7,6 +7,7 @@ using UnityEditor;
|
||||
using UnityEditor.SceneManagement;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Rendering;
|
||||
using UnityEngine.Rendering.Universal;
|
||||
using UnityEngine.SceneManagement;
|
||||
using Object = UnityEngine.Object;
|
||||
|
||||
@@ -14,11 +15,16 @@ using Object = UnityEngine.Object;
|
||||
public static class BuildHomeCamp
|
||||
{
|
||||
const string Root = "Assets/Bundles/Scenes/Town/HomeCamp";
|
||||
const string Town = "Assets/Bundles/Scenes/Town/TownSquare";
|
||||
const string Shared = "Assets/Bundles/Scenes/Common";
|
||||
const string PrefabPath = Root + "/HomeCamp.prefab";
|
||||
const string ScenePath = Root + "/HomeCamp.unity";
|
||||
const int Layer = 29;
|
||||
const float GroundHalfSize = 21;
|
||||
const float StoneTileMeters = 3.2f;
|
||||
static readonly string[] Parts = { "ground", "mainhouse", "cottage", "fountain" };
|
||||
static readonly string[] Props = { "fence", "stall", "flowerpot", "rocks", "steps", "cypress", "bush", "flowercluster", "shadetree" };
|
||||
// Studio HomeCamp props were exported facing -Z. DGX static meshes use the project +Z front, so the prefab root stays unrotated.
|
||||
static readonly string[] LegacyMinusZ = { "fence", "stall", "flowerpot", "rocks", "steps", "cypress", "bush" };
|
||||
static string Repo => Directory.GetParent(Application.dataPath).Parent.FullName;
|
||||
static string Review => Path.Combine(Repo, "Tools/output/homecamp_review");
|
||||
[Serializable] class Receipt { public string status = ""; public int triangles = 0; public int targetTriangles = 0; }
|
||||
@@ -29,6 +35,15 @@ public static class BuildHomeCamp
|
||||
{ var s = SceneManager.GetSceneAt(i); return s.path + " dirty=" + s.isDirty; }));
|
||||
}
|
||||
|
||||
public static string RenderInfo()
|
||||
{
|
||||
var a = GraphicsSettings.currentRenderPipeline as UniversalRenderPipelineAsset;
|
||||
if (!a) return "No URP asset";
|
||||
return QualitySettings.names[QualitySettings.GetQualityLevel()] + " asset=" + AssetDatabase.GetAssetPath(a)
|
||||
+ " mainShadows=" + a.supportsMainLightShadows + " distance=" + a.shadowDistance + " res=" + a.mainLightShadowmapResolution
|
||||
+ " cascades=" + a.shadowCascadeCount + " soft=" + a.supportsSoftShadows + " hdr=" + a.supportsHDR + " msaa=" + a.msaaSampleCount;
|
||||
}
|
||||
|
||||
static void Folder(string path)
|
||||
{
|
||||
if (AssetDatabase.IsValidFolder(path)) return;
|
||||
@@ -54,14 +69,15 @@ public static class BuildHomeCamp
|
||||
File.Copy(source, destination, true);
|
||||
AssetDatabase.ImportAsset(destination, ImportAssetOptions.ForceSynchronousImport);
|
||||
}
|
||||
static void TextureSettings(string path, bool srgb, bool backdrop = false)
|
||||
static void TextureSettings(string path, bool srgb, bool backdrop = false, bool tile = false, bool isProp = false)
|
||||
{
|
||||
var importer = (TextureImporter)AssetImporter.GetAtPath(path);
|
||||
importer.textureType = TextureImporterType.Default;
|
||||
importer.sRGBTexture = srgb; importer.alphaIsTransparency = false;
|
||||
importer.mipmapEnabled = !backdrop; importer.maxTextureSize = 2048;
|
||||
importer.textureCompression = TextureImporterCompression.Uncompressed;
|
||||
importer.wrapMode = TextureWrapMode.Clamp;
|
||||
importer.mipmapEnabled = !backdrop; importer.maxTextureSize = isProp ? 512 : (tile ? 1024 : 2048);
|
||||
importer.textureCompression = (tile || isProp) ? TextureImporterCompression.Compressed : TextureImporterCompression.Uncompressed;
|
||||
importer.wrapMode = tile ? TextureWrapMode.Repeat : TextureWrapMode.Clamp;
|
||||
importer.anisoLevel = tile ? 4 : 1;
|
||||
importer.SaveAndReimport();
|
||||
}
|
||||
static Material SaveMaterial(string path, string shaderName)
|
||||
@@ -86,8 +102,53 @@ public static class BuildHomeCamp
|
||||
if (receipt.status != "ready_for_unity" || receipt.triangles * 10 > receipt.targetTriangles * 13)
|
||||
throw new InvalidOperationException("Unfinished model " + part);
|
||||
}
|
||||
foreach (var part in Parts)
|
||||
foreach (var part in Parts) ImportPart(part);
|
||||
Source(Path.Combine(Repo, "Doc/Design/Scene/HomeCamp/backdrop.png"), Root + "/backdrop.png");
|
||||
TextureSettings(Root + "/backdrop.png", true, true);
|
||||
var background = SaveMaterial(Root + "/backdrop.mat", "Universal Render Pipeline/Unlit");
|
||||
background.SetTexture("_BaseMap", AssetDatabase.LoadAssetAtPath<Texture2D>(Root + "/backdrop.png"));
|
||||
background.SetColor("_BaseColor", Color.white);
|
||||
EditorUtility.SetDirty(background); AssetDatabase.SaveAssetIfDirty(background);
|
||||
BuildLookDev();
|
||||
return "Imported four Studio assets + XPbr, authored four component Prefabs, unlit backdrop and lit look-dev materials.";
|
||||
}
|
||||
|
||||
/// <summary>Imports the v2 B/C props whose five-step receipts are ready; returns the imported ids.</summary>
|
||||
public static string BuildProps()
|
||||
{
|
||||
if (EditorApplication.isPlaying) throw new InvalidOperationException("Stop Play before authoring.");
|
||||
AssetDatabase.ImportAsset("Assets/Bundles/Shaders/XShader/HomeCampXPbr.shader", ImportAssetOptions.ForceSynchronousImport);
|
||||
var done = new List<string>(); var skipped = new List<string>();
|
||||
foreach (var part in Props)
|
||||
{
|
||||
var path = Path.Combine(Repo, "Tools/output/homecamp_studio", part, "production.json");
|
||||
var receipt = File.Exists(path) ? JsonUtility.FromJson<Receipt>(File.ReadAllText(path)) : null;
|
||||
if (receipt == null || receipt.status != "ready_for_unity" || receipt.triangles * 10 > receipt.targetTriangles * 13)
|
||||
{ skipped.Add(part); continue; }
|
||||
ImportPart(part); done.Add(part);
|
||||
}
|
||||
return "Imported props: " + string.Join(",", done) + "; not ready: " + string.Join(",", skipped);
|
||||
}
|
||||
|
||||
/// <summary>Reimports cottage when its five-step receipt is ready (v2 awning side house).</summary>
|
||||
public static string BuildCottage()
|
||||
{
|
||||
if (EditorApplication.isPlaying) throw new InvalidOperationException("Stop Play before authoring.");
|
||||
AssetDatabase.ImportAsset("Assets/Bundles/Shaders/XShader/HomeCampXPbr.shader", ImportAssetOptions.ForceSynchronousImport);
|
||||
var path = Path.Combine(Repo, "Tools/output/homecamp_studio/cottage/production.json");
|
||||
var receipt = File.Exists(path) ? JsonUtility.FromJson<Receipt>(File.ReadAllText(path)) : null;
|
||||
if (receipt == null || receipt.status != "ready_for_unity" || receipt.triangles * 10 > receipt.targetTriangles * 13)
|
||||
throw new InvalidOperationException("Cottage not ready_for_unity");
|
||||
ImportPart("cottage");
|
||||
// Secondary UV for lightmaps (same as mainhouse/fountain).
|
||||
var importer = (ModelImporter)AssetImporter.GetAtPath(Root + "/cottage/cottage.fbx");
|
||||
if (!importer.generateSecondaryUV || importer.secondaryUVPackMargin < 8)
|
||||
{ importer.generateSecondaryUV = true; importer.secondaryUVPackMargin = 8; importer.SaveAndReimport(); }
|
||||
return "Imported cottage triangles=" + receipt.triangles;
|
||||
}
|
||||
|
||||
static void ImportPart(string part)
|
||||
{
|
||||
var dir = Root + "/" + part;
|
||||
var source = Path.Combine(Repo, "Tools/output/homecamp_studio", part, "xpbr");
|
||||
foreach (var suffix in new[] { ".fbx", "_base.png", "_mix.png" })
|
||||
@@ -99,12 +160,17 @@ public static class BuildHomeCamp
|
||||
importer.meshCompression = ModelImporterMeshCompression.Off;
|
||||
importer.isReadable = part == "ground"; importer.addCollider = false;
|
||||
importer.SaveAndReimport();
|
||||
TextureSettings(dir + "/" + part + "_base.png", true);
|
||||
TextureSettings(dir + "/" + part + "_mix.png", false);
|
||||
bool isProp = Props.Contains(part);
|
||||
TextureSettings(dir + "/" + part + "_base.png", true, isProp: isProp);
|
||||
TextureSettings(dir + "/" + part + "_mix.png", false, isProp: isProp);
|
||||
var mat = SaveMaterial(dir + "/" + part + ".mat", "Test/HomeCampXPbr");
|
||||
mat.SetTexture("_MainTex", AssetDatabase.LoadAssetAtPath<Texture2D>(dir + "/" + part + "_base.png"));
|
||||
mat.SetTexture("_MixTex", AssetDatabase.LoadAssetAtPath<Texture2D>(dir + "/" + part + "_mix.png"));
|
||||
mat.SetColor("_BaseColor", Color.white); mat.SetFloat("_Metallic", 1); mat.SetFloat("_Roughness", 1); mat.SetFloat("_AO", 1);
|
||||
// Candy plaster: keep albedo readable, kill hot specular (fountain bake A~0.2 → smoothness 0.8 without scale).
|
||||
mat.SetColor("_BaseColor", new Color(1.08f, 1.06f, 1.03f, 1));
|
||||
mat.SetFloat("_Metallic", .05f); mat.SetFloat("_Roughness", 1); mat.SetFloat("_SmoothnessScale", .22f); mat.SetFloat("_AO", .65f);
|
||||
mat.SetFloat("_LightmapShadeGamma", .58f);
|
||||
mat.enableInstancing = true;
|
||||
EditorUtility.SetDirty(mat); AssetDatabase.SaveAssetIfDirty(mat);
|
||||
var scene = EditorSceneManager.NewPreviewScene();
|
||||
try
|
||||
@@ -116,59 +182,174 @@ public static class BuildHomeCamp
|
||||
if (part == "ground")
|
||||
foreach (var f in go.GetComponentsInChildren<MeshFilter>())
|
||||
{ var c = f.gameObject.AddComponent<MeshCollider>(); c.sharedMesh = f.sharedMesh; c.convex = false; }
|
||||
PrefabUtility.SaveAsPrefabAsset(go, dir + "/" + part + ".prefab");
|
||||
var root = go;
|
||||
if (Props.Contains(part))
|
||||
{
|
||||
root = new GameObject(part); SceneManager.MoveGameObjectToScene(root, scene);
|
||||
go.transform.SetParent(root.transform, false); go.name = part + "_mesh";
|
||||
if (LegacyMinusZ.Contains(part))
|
||||
go.transform.localRotation = Quaternion.Euler(0, 180, 0);
|
||||
}
|
||||
PrefabUtility.SaveAsPrefabAsset(root, dir + "/" + part + ".prefab");
|
||||
}
|
||||
finally { EditorSceneManager.ClosePreviewScene(scene); }
|
||||
}
|
||||
|
||||
/// <summary>Lit foliage cards, tiled XTerrain ground and post Volume; does not reimport Studio models.</summary>
|
||||
public static string BuildLookDev()
|
||||
{
|
||||
if (EditorApplication.isPlaying) throw new InvalidOperationException("Stop Play before authoring.");
|
||||
AssetDatabase.ImportAsset("Assets/Bundles/Shaders/XShader/HomeCampXPbr.shader", ImportAssetOptions.ForceSynchronousImport);
|
||||
AssetDatabase.ImportAsset("Assets/Bundles/Shaders/XShader/XTerrain.shader", ImportAssetOptions.ForceSynchronousImport);
|
||||
BuildCardMaterials();
|
||||
BuildGroundSurface();
|
||||
BuildVolumeProfile();
|
||||
BuildLightingSettings();
|
||||
TuneXPbrMaterials();
|
||||
// Lightmap UVs go to UV1 only; Studio UV0 and baked XPbr textures stay untouched.
|
||||
foreach (var part in new[] { "mainhouse", "fountain", "cottage" })
|
||||
{
|
||||
var importer = (ModelImporter)AssetImporter.GetAtPath(Root + "/" + part + "/" + part + ".fbx");
|
||||
if (!importer) continue;
|
||||
if (importer.generateSecondaryUV && importer.secondaryUVPackMargin >= 8) continue;
|
||||
importer.generateSecondaryUV = true; importer.secondaryUVPackMargin = 8; importer.SaveAndReimport();
|
||||
}
|
||||
Source(Path.Combine(Repo, "Doc/Design/Scene/HomeCamp/backdrop.png"), Root + "/backdrop.png");
|
||||
TextureSettings(Root + "/backdrop.png", true, true);
|
||||
var background = SaveMaterial(Root + "/backdrop.mat", "Universal Render Pipeline/Unlit");
|
||||
background.SetTexture("_BaseMap", AssetDatabase.LoadAssetAtPath<Texture2D>(Root + "/backdrop.png"));
|
||||
background.SetColor("_BaseColor", Color.white);
|
||||
EditorUtility.SetDirty(background); AssetDatabase.SaveAssetIfDirty(background);
|
||||
return "Authored lit foliage cards, XTerrain ground, HomeCampVolume, lighting settings and lightmap UVs.";
|
||||
}
|
||||
|
||||
/// <summary>Matte candy response: lower metallic/smoothness, slight albedo lift for readable diffuse.</summary>
|
||||
static void TuneXPbrMaterials()
|
||||
{
|
||||
foreach (var path in AssetDatabase.FindAssets("t:Material", new[] { Root })
|
||||
.Select(AssetDatabase.GUIDToAssetPath)
|
||||
.Where(p => p.EndsWith(".mat")))
|
||||
{
|
||||
var mat = AssetDatabase.LoadAssetAtPath<Material>(path);
|
||||
if (!mat || mat.shader == null || mat.shader.name != "Test/HomeCampXPbr") continue;
|
||||
mat.SetColor("_BaseColor", new Color(1.08f, 1.06f, 1.03f, 1));
|
||||
mat.SetFloat("_Metallic", .05f);
|
||||
mat.SetFloat("_Roughness", 1);
|
||||
mat.SetFloat("_SmoothnessScale", .22f);
|
||||
mat.SetFloat("_AO", .65f);
|
||||
mat.SetFloat("_LightmapShadeGamma", .58f);
|
||||
mat.enableInstancing = true;
|
||||
EditorUtility.SetDirty(mat); AssetDatabase.SaveAssetIfDirty(mat);
|
||||
}
|
||||
}
|
||||
|
||||
static LightingSettings BuildLightingSettings()
|
||||
{
|
||||
var path = Root + "/HomeCampLighting.lighting";
|
||||
var settings = AssetDatabase.LoadAssetAtPath<LightingSettings>(path);
|
||||
if (!settings) { settings = new LightingSettings { name = "HomeCampLighting" }; AssetDatabase.CreateAsset(settings, path); }
|
||||
settings.bakedGI = true; settings.realtimeGI = false;
|
||||
settings.lightmapper = LightingSettings.Lightmapper.ProgressiveGPU;
|
||||
// Fully-baked sun stores cast shadows in lightmap RGB (offscreen camera.Render has no realtime shadowmaps).
|
||||
settings.mixedBakeMode = MixedLightingMode.Subtractive;
|
||||
settings.lightmapResolution = 16; settings.lightmapPadding = 4; settings.lightmapMaxSize = 1024;
|
||||
settings.directionalityMode = LightmapsMode.NonDirectional;
|
||||
// Enough bounces for readable shade walls; keep indirect modest so plaza cast shadows stay dark.
|
||||
settings.directSampleCount = 48; settings.indirectSampleCount = 512; settings.environmentSampleCount = 128; settings.maxBounces = 3;
|
||||
settings.ao = true; settings.aoMaxDistance = .75f; settings.aoExponentDirect = .55f; settings.aoExponentIndirect = .7f;
|
||||
settings.albedoBoost = 1f; settings.indirectScale = 1f;
|
||||
settings.lightmapCompression = LightmapCompression.NormalQuality; settings.filteringMode = LightingSettings.FilterMode.Auto;
|
||||
EditorUtility.SetDirty(settings); AssetDatabase.SaveAssetIfDirty(settings);
|
||||
return settings;
|
||||
}
|
||||
|
||||
static void BuildCardMaterials()
|
||||
{
|
||||
foreach (var name in new[] { "tree_card", "flower_card" })
|
||||
{
|
||||
Source(Path.Combine(Repo, "Doc/Design/Scene/HomeCamp", name + ".png"), Root + "/" + name + ".png");
|
||||
TextureSettings(Root + "/" + name + ".png", true);
|
||||
var ti = (TextureImporter)AssetImporter.GetAtPath(Root + "/" + name + ".png");
|
||||
ti.alphaIsTransparency = true; ti.SaveAndReimport();
|
||||
var card = SaveMaterial(Root + "/" + name + ".mat", "Universal Render Pipeline/Unlit");
|
||||
card.SetTexture("_BaseMap", AssetDatabase.LoadAssetAtPath<Texture2D>(Root + "/" + name + ".png"));
|
||||
var card = SaveMaterial(Root + "/" + name + ".mat", "Universal Render Pipeline/Simple Lit");
|
||||
var texture = AssetDatabase.LoadAssetAtPath<Texture2D>(Root + "/" + name + ".png");
|
||||
// The lightmapper reads cutout transparency from _MainTex/_Cutoff.
|
||||
card.SetTexture("_BaseMap", texture); card.SetTexture("_MainTex", texture);
|
||||
card.SetColor("_BaseColor", Color.white); card.SetFloat("_AlphaClip", 1); card.SetFloat("_Cutoff", .55f);
|
||||
card.EnableKeyword("_ALPHATEST_ON"); card.SetFloat("_Cull", 0); card.renderQueue = 2450;
|
||||
card.SetFloat("_ReceiveShadows", 1); card.DisableKeyword("_RECEIVE_SHADOWS_OFF");
|
||||
card.SetFloat("_SpecularHighlights", 0); card.DisableKeyword("_SPECULAR_COLOR");
|
||||
card.SetOverrideTag("RenderType", "TransparentCutout");
|
||||
EditorUtility.SetDirty(card); AssetDatabase.SaveAssetIfDirty(card);
|
||||
}
|
||||
BuildGroundSurface();
|
||||
return "Imported four Studio assets + XPbr, authored four component Prefabs and unlit background material.";
|
||||
}
|
||||
|
||||
static Texture2D GroundLayer(string name, bool srgb, bool tile)
|
||||
{
|
||||
Source(Path.Combine(Repo, "Doc/Design/Scene/HomeCamp", name + ".png"), Root + "/" + name + ".png");
|
||||
TextureSettings(Root + "/" + name + ".png", srgb, false, tile);
|
||||
return AssetDatabase.LoadAssetAtPath<Texture2D>(Root + "/" + name + ".png");
|
||||
}
|
||||
|
||||
static void BuildGroundSurface()
|
||||
{
|
||||
Source(Path.Combine(Repo, "Doc/Design/Scene/HomeCamp/ground_surface.png"), Root + "/ground_surface.png");
|
||||
TextureSettings(Root + "/ground_surface.png", true);
|
||||
var mat = SaveMaterial(Root + "/ground_surface.mat", "Universal Render Pipeline/Unlit");
|
||||
mat.SetTexture("_BaseMap", AssetDatabase.LoadAssetAtPath<Texture2D>(Root + "/ground_surface.png"));
|
||||
mat.SetColor("_BaseColor", Color.white);
|
||||
const float half = GroundHalfSize;
|
||||
var stone = GroundLayer("ground_tile_stone", true, true);
|
||||
var grass = GroundLayer("ground_tile_grass", true, true);
|
||||
var grassDark = GroundLayer("ground_tile_grass_dark", true, true);
|
||||
var control = GroundLayer("ground_control", false, false);
|
||||
var mat = SaveMaterial(Root + "/ground_terrain.mat", "XWorld/XTerrain");
|
||||
mat.SetTexture("_LayerTex0", stone); mat.SetTexture("_LayerTex1", grass);
|
||||
mat.SetTexture("_LayerTex2", grassDark); mat.SetTexture("_LayerTex3", grassDark);
|
||||
mat.SetTexture("_Control", control);
|
||||
mat.SetFloat("_Tiling", half * 2 / StoneTileMeters);
|
||||
// Softer micro-relief so the lawn reads as soft grass rather than shiny plastic.
|
||||
// High _Roughness bias → nearly matte ground; metallic off so stone/grass stay diffuse-led.
|
||||
mat.SetFloat("_BumpScale", 1.6f); mat.SetFloat("_Roughness", 1.15f); mat.SetFloat("_Metallic", 0); mat.SetFloat("_AO", .9f);
|
||||
// Less GI lift → ~20% deeper shade on busy ground albedo.
|
||||
mat.SetFloat("_LightmapContrast", .58f);
|
||||
mat.SetFloat("_LightmapShadeMul", 1f);
|
||||
mat.SetColor("_LightmapShadeTint", new Color(.88f, .9f, .95f, 1f));
|
||||
EditorUtility.SetDirty(mat); AssetDatabase.SaveAssetIfDirty(mat);
|
||||
var mesh = AssetDatabase.LoadAssetAtPath<Mesh>(Root + "/GroundSurface.asset");
|
||||
if (!mesh) { mesh = new Mesh(); AssetDatabase.CreateAsset(mesh, Root + "/GroundSurface.asset"); }
|
||||
mesh.Clear(); mesh.name = "HomeCamp continuous ground surface";
|
||||
mesh.vertices = new[] {new Vector3(-18,0,-21), new Vector3(-18,0,21), new Vector3(18,0,21), new Vector3(18,0,-21)};
|
||||
mesh.vertices = new[] {new Vector3(-half,0,-half), new Vector3(-half,0,half), new Vector3(half,0,half), new Vector3(half,0,-half)};
|
||||
mesh.uv = new[] {new Vector2(0,0),new Vector2(0,1),new Vector2(1,1),new Vector2(1,0)};
|
||||
mesh.uv2 = mesh.uv;
|
||||
mesh.triangles = new[] {0,1,2,0,2,3}; mesh.RecalculateNormals(); mesh.RecalculateTangents(); mesh.RecalculateBounds();
|
||||
EditorUtility.SetDirty(mesh); AssetDatabase.SaveAssetIfDirty(mesh);
|
||||
var scene = EditorSceneManager.NewPreviewScene();
|
||||
try
|
||||
{
|
||||
var go = new GameObject("GroundSurface"); SceneManager.MoveGameObjectToScene(go, scene);
|
||||
go.AddComponent<MeshFilter>().sharedMesh = mesh; go.AddComponent<MeshRenderer>().sharedMaterial = mat;
|
||||
go.AddComponent<MeshFilter>().sharedMesh = mesh;
|
||||
var renderer = go.AddComponent<MeshRenderer>(); renderer.sharedMaterial = mat;
|
||||
renderer.shadowCastingMode = ShadowCastingMode.Off; renderer.receiveShadows = true;
|
||||
go.AddComponent<MeshCollider>().sharedMesh = mesh;
|
||||
PrefabUtility.SaveAsPrefabAsset(go, Root + "/GroundSurface.prefab");
|
||||
}
|
||||
finally { EditorSceneManager.ClosePreviewScene(scene); }
|
||||
}
|
||||
|
||||
static VolumeProfile BuildVolumeProfile()
|
||||
{
|
||||
var path = Root + "/HomeCampVolume.asset";
|
||||
var profile = AssetDatabase.LoadAssetAtPath<VolumeProfile>(path);
|
||||
if (!profile) { profile = ScriptableObject.CreateInstance<VolumeProfile>(); AssetDatabase.CreateAsset(profile, path); }
|
||||
foreach (var old in profile.components.ToArray()) { profile.components.Remove(old); Object.DestroyImmediate(old, true); }
|
||||
T Add<T>() where T : VolumeComponent
|
||||
{
|
||||
var c = profile.Add<T>(true); c.name = typeof(T).Name;
|
||||
c.hideFlags = HideFlags.HideInInspector | HideFlags.HideInHierarchy;
|
||||
AssetDatabase.AddObjectToAsset(c, profile); return c;
|
||||
}
|
||||
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;
|
||||
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;
|
||||
var vignette = Add<Vignette>(); vignette.intensity.value = .18f; vignette.smoothness.value = .45f;
|
||||
EditorUtility.SetDirty(profile); AssetDatabase.SaveAssetIfDirty(profile);
|
||||
return profile;
|
||||
}
|
||||
|
||||
static GameObject Place(Transform parent, string path, string name, Vector3 pos, float size, bool width = false, bool faceCenter = false)
|
||||
{
|
||||
var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(path);
|
||||
@@ -193,7 +374,7 @@ public static class BuildHomeCamp
|
||||
var name = part == "bush" ? "shrub" : "tree";
|
||||
return "Assets/Bundles/Scenes/Prototype/Battlefield/A/Components/" + name + "/" + name + ".prefab";
|
||||
}
|
||||
return Town + "/" + part + "/" + part + ".prefab";
|
||||
return Shared + "/" + part + "/" + part + ".prefab";
|
||||
}
|
||||
static float GroundY(GameObject ground, float x, float z)
|
||||
{
|
||||
@@ -209,34 +390,78 @@ public static class BuildHomeCamp
|
||||
var go = GameObject.CreatePrimitive(PrimitiveType.Quad); go.name = name;
|
||||
go.transform.SetParent(parent, false); Object.DestroyImmediate(go.GetComponent<Collider>());
|
||||
var texture = AssetDatabase.LoadAssetAtPath<Texture2D>(Root + "/" + image + ".png");
|
||||
var rotation = Quaternion.LookRotation(new Vector3(0, 1.8f, 3) - new Vector3(0, 15, -27), Vector3.up);
|
||||
var rotation = Quaternion.LookRotation(CamTarget - CamPos, Vector3.up);
|
||||
go.transform.rotation = rotation;
|
||||
go.transform.position = bottom + (rotation * Vector3.up) * height * .5f;
|
||||
go.transform.localScale = new Vector3(height * texture.width / texture.height, height, 1);
|
||||
var renderer = go.GetComponent<Renderer>(); renderer.sharedMaterial = AssetDatabase.LoadAssetAtPath<Material>(Root + "/" + image + ".mat");
|
||||
renderer.shadowCastingMode = ShadowCastingMode.Off; renderer.receiveShadows = false;
|
||||
// Trees cast into the lightmap again (concept art needs canopy shadows); kept behind roofs to avoid plaza ink.
|
||||
renderer.shadowCastingMode = ShadowCastingMode.On;
|
||||
renderer.receiveShadows = true;
|
||||
SetLayer(go); return go;
|
||||
}
|
||||
static readonly Vector3 CamPos = new Vector3(0, 14, -25), CamTarget = new Vector3(0, 1.6f, 3);
|
||||
const float CamFov = 50;
|
||||
static Camera MakeCamera(Scene scene, Transform parent)
|
||||
{
|
||||
var go = new GameObject("HomeCampCamera"); SceneManager.MoveGameObjectToScene(go, scene);
|
||||
if (parent) go.transform.SetParent(parent, false);
|
||||
var camera = go.AddComponent<Camera>();
|
||||
go.transform.position = new Vector3(0, 15, -27);
|
||||
go.transform.LookAt(new Vector3(0, 1.8f, 3));
|
||||
camera.fieldOfView = 48; camera.nearClipPlane = .1f; camera.farClipPlane = 180;
|
||||
go.transform.position = CamPos;
|
||||
go.transform.LookAt(CamTarget);
|
||||
camera.fieldOfView = CamFov; camera.nearClipPlane = .1f; camera.farClipPlane = 180;
|
||||
camera.clearFlags = CameraClearFlags.SolidColor; camera.backgroundColor = new Color(.46f, .72f, .88f);
|
||||
camera.cullingMask = 1 << Layer; camera.scene = scene;
|
||||
camera.allowHDR = false; camera.allowMSAA = true;
|
||||
camera.allowHDR = true; camera.allowMSAA = true;
|
||||
var data = camera.GetUniversalAdditionalCameraData();
|
||||
data.renderPostProcessing = true; data.volumeLayerMask = 1 << Layer;
|
||||
return camera;
|
||||
}
|
||||
static void Light(Scene scene, Transform parent)
|
||||
static Light Light(Scene scene, Transform parent)
|
||||
{
|
||||
var go = new GameObject("HomeCampSun"); SceneManager.MoveGameObjectToScene(go, scene);
|
||||
if (parent) go.transform.SetParent(parent, false);
|
||||
var light = go.AddComponent<Light>(); light.type = LightType.Directional;
|
||||
light.color = new Color(1, .95f, .84f); light.intensity = 1.05f; light.shadows = LightShadows.Soft;
|
||||
light.cullingMask = 1 << Layer; go.transform.rotation = Quaternion.Euler(43, -32, 0);
|
||||
// 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;
|
||||
light.cullingMask = 1 << Layer; go.transform.rotation = Quaternion.Euler(68, 50, 0);
|
||||
BakeFillLight(scene, parent);
|
||||
return light;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Baked-only opposite skylight (no shadows). URP ignores a second realtime Directional,
|
||||
/// but Progressive still bakes this into lightmaps so backfaces stay cream while cast shadows remain.
|
||||
/// </summary>
|
||||
static void BakeFillLight(Scene scene, Transform parent)
|
||||
{
|
||||
var go = new GameObject("HomeCampBakeFill"); SceneManager.MoveGameObjectToScene(go, scene);
|
||||
if (parent) go.transform.SetParent(parent, false);
|
||||
var light = go.AddComponent<Light>(); light.type = LightType.Directional;
|
||||
// Noon skylight fill — held back another ~20% so cast shade reads deeper.
|
||||
light.color = new Color(.82f, .88f, 1f); light.intensity = .22f;
|
||||
light.shadows = LightShadows.None; light.lightmapBakeType = LightmapBakeType.Baked;
|
||||
light.cullingMask = 1 << Layer;
|
||||
go.transform.rotation = Quaternion.Euler(75, -30, 0);
|
||||
}
|
||||
|
||||
/// <summary>Grid of probes over the plaza so houses on LightProbes get baked sky fill, not black tetrahedra.</summary>
|
||||
static void PlaceLightProbes(Transform parent)
|
||||
{
|
||||
var go = new GameObject("HomeCampLightProbes");
|
||||
go.transform.SetParent(parent, false);
|
||||
var group = go.AddComponent<LightProbeGroup>();
|
||||
var positions = new System.Collections.Generic.List<Vector3>();
|
||||
for (int x = -10; x <= 10; x += 4)
|
||||
for (int z = -12; z <= 12; z += 4)
|
||||
for (int y = 1; y <= 5; y += 2)
|
||||
positions.Add(new Vector3(x, y, z));
|
||||
group.probePositions = positions.ToArray();
|
||||
}
|
||||
static void Background(Transform parent, Camera camera)
|
||||
{
|
||||
@@ -260,6 +485,13 @@ public static class BuildHomeCamp
|
||||
if (EditorApplication.isPlaying) throw new InvalidOperationException("Author in Edit Mode.");
|
||||
var previous = SceneManager.GetActiveScene();
|
||||
var scene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Additive);
|
||||
// An already-open HomeCamp keeps this path. Saving the new scene, then baking the stale one, writes the cards back over the file.
|
||||
var stale = SceneManager.GetSceneByPath(ScenePath);
|
||||
if (stale.IsValid() && stale.isLoaded && stale != scene)
|
||||
{
|
||||
if (previous == stale) previous = default;
|
||||
EditorSceneManager.CloseScene(stale, true);
|
||||
}
|
||||
try
|
||||
{
|
||||
SceneManager.SetActiveScene(scene);
|
||||
@@ -287,38 +519,153 @@ public static class BuildHomeCamp
|
||||
placements.Add(name + " groundY=" + y.ToString("F3"));
|
||||
}
|
||||
Put("mainhouse", "MainHouse", 0, 7.2f, 6.5f, false, true);
|
||||
Put("mainhouse", "HouseLeft", -7.5f, 4, 4, false, true);
|
||||
// Left side uses the awning cottage to match the reference market house; right stays a mirrored mainhouse.
|
||||
Put("cottage", "HouseLeft", -6.8f, 3.6f, 3.8f, false, true);
|
||||
Put("mainhouse", "HouseRight", 7.5f, 4, 4, false, true);
|
||||
// Mirrored copy so the reused house does not read as an identical clone; HomeCampXPbr is two-sided.
|
||||
var houseRight = root.transform.Find("HouseRight");
|
||||
houseRight.localScale = Vector3.Scale(houseRight.localScale, new Vector3(-1, 1, 1));
|
||||
// Sink house bases a few centimeters to hide the mesh/ground seam seen in portrait review.
|
||||
foreach (var house in new[] { "MainHouse", "HouseLeft", "HouseRight" })
|
||||
root.transform.Find(house).position += Vector3.down * .08f;
|
||||
Put("fountain", "Fountain", 0, .8f, 4.4f, true);
|
||||
Put("tree", "TreeBackLeft", -9, 8, 7, false, false, true);
|
||||
Put("tree", "TreeBackRight", 9, 9, 6.5f, false, false, true);
|
||||
Put("tree", "TreeRearLeft", -5.6f, 14, 4.8f, false, false, true);
|
||||
Put("tree", "TreeRearRight", 5.6f, 14, 4.8f, false, false, true);
|
||||
Put("tree", "TreeFrontLeft", -10, -4, 5, false, false, true);
|
||||
// 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.
|
||||
Put("tree", "TreeRearLeft", -5.6f, 14.5f, 4.6f, false, false, true);
|
||||
Put("tree", "TreeRearRight", 5.6f, 14.5f, 4.6f, false, false, true);
|
||||
Put("lamp", "LampLeft", -5.2f, -.5f, 2.6f, false, false, true);
|
||||
Put("lamp", "LampRight", 5.2f, 1, 2.6f, false, false, true);
|
||||
Put("lamp", "LampForeground", 5.5f, -6, 2.7f, false, false, true);
|
||||
Put("lamp", "LampPath", 3.4f, -8.4f, 2.7f, false, false, true);
|
||||
Put("bench", "BenchFrontLeft", -4.4f, -4, 2.2f, true, true, true);
|
||||
for (int i = 0; i < 12; i++)
|
||||
{
|
||||
var angle = (i * 30f) * Mathf.Deg2Rad;
|
||||
Put("bush", "FountainFlowers" + i, Mathf.Sin(angle) * 2.7f, .8f + Mathf.Cos(angle) * 2.7f, .55f, false, false, true);
|
||||
}
|
||||
Put("bench", "BenchRight", 6.8f, -2.2f, 2.2f, true, true, true);
|
||||
// Fountain ring is placed after the viewer vector: six flowercluster meshes, or the old cards if the prefab is missing.
|
||||
foreach (var p in new[] { new Vector2(-7,-5), new Vector2(-5,-7), new Vector2(6,-6), new Vector2(8,-3), new Vector2(-9,2), new Vector2(9,2) })
|
||||
Put("bush", "BorderFlowers", p.x, p.y, .9f, false, false, true);
|
||||
// Flower clusters along the plaza rim and the path so the stone/grass edge is never a bare cut.
|
||||
var rim = 0;
|
||||
for (int i = 0; i < 18; i++)
|
||||
{
|
||||
var angle = (-80 + i * 20f) * Mathf.Deg2Rad;
|
||||
var x = Mathf.Sin(angle) * 9.3f; var z = 3.5f + Mathf.Cos(angle) * 6.9f;
|
||||
if (z < -2 && Mathf.Abs(x) < 2.5f) continue;
|
||||
Put("bush", "RimFlowers" + rim++, x, z, .7f + (i % 3) * .12f, false, false, true);
|
||||
}
|
||||
var pathFlowers = new[] {
|
||||
new Vector2(-.6f,-7.5f), new Vector2(.3f,-12.5f),
|
||||
new Vector2(1.3f,-16), new Vector2(4.6f,-15.5f), new Vector2(-3.2f,-14.2f), new Vector2(3.2f,-14.8f),
|
||||
new Vector2(5.1f,-10.2f), new Vector2(.8f,-18.5f) };
|
||||
for (int i = 0; i < pathFlowers.Length; i++)
|
||||
Put("bush", "PathFlowers" + i, pathFlowers[i].x, pathFlowers[i].y, .65f + (i % 3) * .08f, false, false, true);
|
||||
void Prop(string part, string name, float x, float z, float size, bool width, Vector3 face, bool presentToCamera = false)
|
||||
{
|
||||
if (!AssetDatabase.LoadAssetAtPath<GameObject>(PartPath(part))) { placements.Add(name + " skipped (no prefab)"); return; }
|
||||
var y = GroundY(ground, x, z);
|
||||
var go = Place(root.transform, PartPath(part), name, new Vector3(x, y, z), size, width);
|
||||
if (presentToCamera)
|
||||
go.transform.rotation = Quaternion.LookRotation(CamTarget - CamPos, Vector3.up);
|
||||
else
|
||||
{
|
||||
var toward = face - new Vector3(x, face.y, z);
|
||||
go.transform.rotation = Quaternion.LookRotation(toward, Vector3.up);
|
||||
}
|
||||
go.transform.position += Vector3.up * (y - BoundsOf(go).min.y);
|
||||
placements.Add(name + " groundY=" + y.ToString("F3"));
|
||||
}
|
||||
var plaza = new Vector3(0, 0, 2); var viewer = new Vector3(CamPos.x, 0, CamPos.z);
|
||||
// Portrait horizontal view is about 29 degrees. Card slots at |x|>=11 fall outside it,
|
||||
// so the shade trees stand in the lawn beside the roofs instead of clipping the frame edge.
|
||||
if (AssetDatabase.LoadAssetAtPath<GameObject>(PartPath("shadetree")))
|
||||
{
|
||||
Prop("shadetree", "TreeBackLeft", -5.6f, 10.4f, 6.2f, false, viewer);
|
||||
Prop("shadetree", "TreeBackRight", 6.2f, 9.6f, 5.7f, false, plaza);
|
||||
}
|
||||
else
|
||||
{
|
||||
Put("tree", "TreeBackLeft", -5.6f, 10.4f, 6.2f, false, false, true);
|
||||
Put("tree", "TreeBackRight", 6.2f, 9.6f, 5.7f, false, false, true);
|
||||
}
|
||||
foreach (var (n, x, z, s) in new[] {
|
||||
("ClusterNearL", -2.4f, -11.2f, .62f), ("ClusterNearR", 3.6f, -12.2f, .7f),
|
||||
("ClusterPathL", -4.6f, -8.6f, .55f), ("ClusterPlaza", 2.6f, -6.4f, .5f) })
|
||||
Prop("flowercluster", n, x, z, s, false, viewer);
|
||||
if (AssetDatabase.LoadAssetAtPath<GameObject>(PartPath("flowercluster")))
|
||||
{
|
||||
// Blossoms are on local +Z. Yaw-only leaves the leafy top toward this high camera, so present the front.
|
||||
for (int i = 0; i < 6; i++)
|
||||
{
|
||||
var angle = (i * 60f) * Mathf.Deg2Rad;
|
||||
var size = .62f + (i % 3) * .06f;
|
||||
Prop("flowercluster", "FountainCluster" + i, Mathf.Sin(angle) * 3.25f, .8f + Mathf.Cos(angle) * 3.25f, size, false, viewer, true);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (int i = 0; i < 12; i++)
|
||||
{
|
||||
var angle = (i * 30f) * Mathf.Deg2Rad;
|
||||
Put("bush", "FountainFlowers" + i, Mathf.Sin(angle) * 2.7f, .8f + Mathf.Cos(angle) * 2.7f, .55f, false, false, true);
|
||||
}
|
||||
}
|
||||
// StallLeft removed: left cottage already carries the yellow awning counter.
|
||||
Prop("stall", "StallRight", 4.5f, 2.8f, 2.4f, false, plaza);
|
||||
Prop("steps", "StepsRear", 4.3f, 10.2f, 2.6f, true, viewer);
|
||||
Prop("cypress", "CypressLeft", -4.3f, 9.8f, 5, false, viewer);
|
||||
Prop("cypress", "CypressRight", 7.8f, 8.6f, 5.6f, false, viewer);
|
||||
Prop("cypress", "CypressFrontLeft", -6.8f, -3.4f, 4.6f, false, viewer);
|
||||
// Portrait frustum is only about +-5.4m wide at z=-10, so foreground props stay inside it.
|
||||
foreach (var (n, x, z) in new[] { ("FenceLeftA", -5.2f, -8.6f), ("FenceLeftB", -3.4f, -11), ("FenceRightA", 5.4f, -9.6f) })
|
||||
Prop("fence", n, x, z, 2.8f, true, plaza);
|
||||
// Single-pot prefab scaled into small clusters (three-pot Studio mesh failed UV-preserving reduction).
|
||||
foreach (var (n, x, z, s) in new[] {
|
||||
("PotMainL1", -2.9f, 4.8f, .7f), ("PotMainL2", -2.4f, 5.1f, .55f), ("PotMainL3", -2.55f, 4.45f, .48f),
|
||||
("PotMainR1", 2.9f, 4.9f, .7f), ("PotMainR2", 3.35f, 5.15f, .55f), ("PotMainR3", 3.1f, 4.5f, .48f),
|
||||
("PotLeft1", -6.5f, 1.4f, .65f), ("PotLeft2", -6.05f, 1.7f, .5f),
|
||||
("PotRight1", 6.7f, 1.8f, .65f), ("PotRight2", 7.1f, 1.5f, .5f) })
|
||||
Prop("flowerpot", n, x, z, s, false, viewer);
|
||||
foreach (var (n, x, z, s) in new[] {
|
||||
("RockFrontLeft", -1.8f, -13.6f, 1.6f), ("RockFrontRight", 4.6f, -13.4f, 1.3f), ("RockLeft", -6, -6.4f, 1.2f),
|
||||
("RockNear", -.4f, -15.8f, 1.1f), ("RockPath", 2.8f, -11.2f, .95f) })
|
||||
Prop("rocks", n, x, z, s, true, viewer);
|
||||
foreach (var (n, x, z, s) in new[] {
|
||||
("BushFrontLeft", -4.6f, -12.8f, 1.5f), ("BushFront", -1.4f, -9.6f, 1.1f), ("BushFrontRight", 5.6f, -12.2f, 1.3f),
|
||||
("BushRight", 6.4f, -6.2f, 1.3f), ("BushLeft", -7.2f, -1.6f, 1.4f), ("BushRearLeft", -5.6f, 7.8f, 1.2f),
|
||||
("BushRearRight", 6, 8, 1.2f), ("BushNearLeft", -3.6f, -15.2f, 1.25f), ("BushNearRight", 3.8f, -15.6f, 1.15f),
|
||||
("BushMidLeft", -5.4f, -7.4f, 1.2f) })
|
||||
Prop("bush", n, x, z, s, true, viewer);
|
||||
PlaceLightProbes(root.transform);
|
||||
var camera = MakeCamera(scene, root.transform);
|
||||
Light(scene, root.transform); Background(root.transform, camera); SetLayer(root);
|
||||
var sun = Light(scene, root.transform); Background(root.transform, camera);
|
||||
var volume = new GameObject("PostVolume").AddComponent<Volume>();
|
||||
volume.transform.SetParent(root.transform, false);
|
||||
volume.isGlobal = true; volume.priority = 10;
|
||||
volume.sharedProfile = AssetDatabase.LoadAssetAtPath<VolumeProfile>(Root + "/HomeCampVolume.asset");
|
||||
if (!volume.sharedProfile) throw new InvalidOperationException("Run BuildLookDev before Build.");
|
||||
SetLayer(root);
|
||||
RenderSettings.sun = sun;
|
||||
RenderSettings.ambientMode = AmbientMode.Trilight;
|
||||
RenderSettings.ambientSkyColor = new Color(.68f, .75f, .82f);
|
||||
RenderSettings.ambientEquatorColor = new Color(.55f, .57f, .52f);
|
||||
RenderSettings.ambientGroundColor = new Color(.35f, .34f, .29f);
|
||||
// Noon ambient ~+30% vs prior; still cooler in shade so ground cast shadows stay visible.
|
||||
RenderSettings.ambientIntensity = .9f;
|
||||
RenderSettings.ambientSkyColor = new Color(.85f, .9f, .98f);
|
||||
RenderSettings.ambientEquatorColor = new Color(.72f, .7f, .64f);
|
||||
RenderSettings.ambientGroundColor = new Color(.55f, .52f, .46f);
|
||||
RenderSettings.fog = true; RenderSettings.fogMode = FogMode.Linear;
|
||||
RenderSettings.fogColor = new Color(.78f, .88f, .95f);
|
||||
RenderSettings.fogStartDistance = 45; RenderSettings.fogEndDistance = 220;
|
||||
MarkStatic(root);
|
||||
Lightmapping.lightingSettings = AssetDatabase.LoadAssetAtPath<LightingSettings>(Root + "/HomeCampLighting.lighting");
|
||||
if (!Lightmapping.lightingSettings) throw new InvalidOperationException("Run BuildLookDev before Build.");
|
||||
// Subtractive fills shadowed texels with this color; keep it close to sunlit grass so maps stay soft.
|
||||
// Darker than lit plaster so cast shadows read, brighter than ink so walls stay detailed.
|
||||
RenderSettings.subtractiveShadowColor = new Color(.38f, .44f, .55f);
|
||||
PrefabUtility.SaveAsPrefabAsset(root, PrefabPath);
|
||||
EditorSceneManager.SaveScene(scene, ScenePath);
|
||||
Directory.CreateDirectory(Review);
|
||||
File.WriteAllText(Path.Combine(Review, "assembly.txt"), string.Join("\n", placements));
|
||||
CaptureCamera(camera, 1080, 1920, "homecamp_portrait.png");
|
||||
CaptureCamera(camera, 1080, 2400, "homecamp_tall.png");
|
||||
return "Saved " + PrefabPath + " and " + ScenePath + "; " + placements.Count + " placements; portrait/tall rendered.";
|
||||
// Review-only 1.6m stand-in; kept in the scene outside the Prefab, disabled for art captures.
|
||||
ScaleReference(ground).SetActive(false);
|
||||
EditorSceneManager.SaveScene(scene, ScenePath);
|
||||
return "Saved " + PrefabPath + " and " + ScenePath + "; " + placements.Count + " placements. Run Bake next.";
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -326,6 +673,101 @@ public static class BuildHomeCamp
|
||||
EditorSceneManager.CloseScene(scene, true);
|
||||
}
|
||||
}
|
||||
// Ground + buildings lightmapped so Subtractive bake stores visible cast shadows for review captures.
|
||||
static readonly string[] Lightmapped = { "Ground", "MainHouse", "HouseLeft", "HouseRight", "Fountain" };
|
||||
static void MarkStatic(GameObject root)
|
||||
{
|
||||
foreach (var renderer in root.GetComponentsInChildren<Renderer>(true))
|
||||
{
|
||||
var top = renderer.transform;
|
||||
while (top.parent && top.parent != root.transform) top = top.parent;
|
||||
var name = top.name;
|
||||
if (name == "DistantLakeWaterfallCastle" || name.Contains("Flowers")) continue;
|
||||
GameObjectUtility.SetStaticEditorFlags(renderer.gameObject, StaticEditorFlags.ContributeGI | StaticEditorFlags.BatchingStatic);
|
||||
// Lamps, benches and tree cards cast into the lightmap but are lit from probes, so shared assets need no lightmap UVs.
|
||||
if (renderer is MeshRenderer mesh) mesh.receiveGI = Lightmapped.Contains(name) ? ReceiveGI.Lightmaps : ReceiveGI.LightProbes;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Bakes HomeCamp.unity alone, renders review captures, then restores the previously open scenes.</summary>
|
||||
public static async System.Threading.Tasks.Task<string> Bake()
|
||||
{
|
||||
if (EditorApplication.isPlaying) throw new InvalidOperationException("Stop Play before baking.");
|
||||
if (Enumerable.Range(0, SceneManager.sceneCount).Any(i => SceneManager.GetSceneAt(i).isDirty))
|
||||
throw new InvalidOperationException("An open scene has unsaved edits; save or discard it before baking.");
|
||||
var setup = EditorSceneManager.GetSceneManagerSetup();
|
||||
try
|
||||
{
|
||||
// Bake with HomeCamp as the only open scene so other scenes get no lighting data.
|
||||
// Drop a stale in-memory copy first; OpenScene will not reload a scene that is already loaded.
|
||||
var loaded = SceneManager.GetSceneByPath(ScenePath);
|
||||
if (loaded.IsValid() && loaded.isLoaded)
|
||||
{
|
||||
var holder = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Additive);
|
||||
EditorSceneManager.CloseScene(loaded, true);
|
||||
EditorSceneManager.CloseScene(holder, true);
|
||||
}
|
||||
EditorSceneManager.OpenScene(ScenePath, OpenSceneMode.Single);
|
||||
if (!Lightmapping.BakeAsync()) throw new InvalidOperationException("Lightmap bake could not start.");
|
||||
var started = DateTime.UtcNow;
|
||||
while (Lightmapping.isRunning)
|
||||
{
|
||||
if ((DateTime.UtcNow - started).TotalMinutes > 25) { Lightmapping.Cancel(); throw new TimeoutException("Lightmap bake exceeded 25 minutes."); }
|
||||
await System.Threading.Tasks.Task.Delay(500);
|
||||
}
|
||||
// Lightmaps are assigned to the scene a few editor ticks after isRunning clears.
|
||||
for (int i = 0; i < 40 && LightmapSettings.lightmaps.Length == 0; i++) await System.Threading.Tasks.Task.Delay(250);
|
||||
if (LightmapSettings.lightmaps.Length == 0) throw new InvalidOperationException("Bake produced no lightmaps.");
|
||||
EditorSceneManager.SaveScene(SceneManager.GetSceneByPath(ScenePath));
|
||||
return Capture() + " Bake " + (DateTime.UtcNow - started).TotalSeconds.ToString("F0") + "s.";
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (setup.Length > 0 && !setup.Any(s => s.path == ScenePath && s.isActive && setup.Length == 1))
|
||||
EditorSceneManager.RestoreSceneManagerSetup(setup);
|
||||
}
|
||||
}
|
||||
|
||||
public static string Capture()
|
||||
{
|
||||
var previous = SceneManager.GetActiveScene();
|
||||
var scene = SceneManager.GetSceneByPath(ScenePath);
|
||||
var opened = !scene.IsValid() || !scene.isLoaded;
|
||||
if (opened) scene = EditorSceneManager.OpenScene(ScenePath, OpenSceneMode.Additive);
|
||||
try
|
||||
{
|
||||
SceneManager.SetActiveScene(scene);
|
||||
var root = scene.GetRootGameObjects().Single(g => g.name == "HomeCamp");
|
||||
var camera = root.GetComponentInChildren<Camera>();
|
||||
var scale = scene.GetRootGameObjects().Single(g => g.name.StartsWith("ReviewOnly_ScaleReference"));
|
||||
scale.SetActive(true);
|
||||
CaptureCamera(camera, 1080, 1920, "homecamp_scale_reference.png");
|
||||
scale.SetActive(false);
|
||||
CaptureCamera(camera, 1080, 1920, "homecamp_portrait.png");
|
||||
CaptureCamera(camera, 1080, 2400, "homecamp_tall.png");
|
||||
var maps = LightmapSettings.lightmaps.Length;
|
||||
var ground = root.transform.Find("Ground")?.GetComponentInChildren<MeshRenderer>();
|
||||
var gi = ground ? (" groundLM=" + ground.lightmapIndex + " scaleOffset=" + ground.lightmapScaleOffset) : "";
|
||||
return "Rendered scale/portrait/tall with " + maps + " lightmap(s)." + gi;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (previous.IsValid() && previous.isLoaded) SceneManager.SetActiveScene(previous);
|
||||
if (opened) EditorSceneManager.CloseScene(scene, true);
|
||||
}
|
||||
}
|
||||
|
||||
static GameObject ScaleReference(GameObject ground)
|
||||
{
|
||||
var mat = SaveMaterial(Root + "/review_scale.mat", "Universal Render Pipeline/Lit");
|
||||
mat.SetColor("_BaseColor", new Color(.55f, .55f, .58f)); EditorUtility.SetDirty(mat); AssetDatabase.SaveAssetIfDirty(mat);
|
||||
var go = GameObject.CreatePrimitive(PrimitiveType.Capsule); go.name = "ReviewOnly_ScaleReference_1.6m";
|
||||
Object.DestroyImmediate(go.GetComponent<Collider>());
|
||||
go.transform.localScale = new Vector3(.45f, .8f, .45f);
|
||||
go.transform.position = new Vector3(2.8f, GroundY(ground, 2.8f, -2.5f) + .8f, -2.5f);
|
||||
go.GetComponent<Renderer>().sharedMaterial = mat;
|
||||
SetLayer(go); return go;
|
||||
}
|
||||
static void CaptureCamera(Camera camera, int width, int height, string name)
|
||||
{
|
||||
Directory.CreateDirectory(Review);
|
||||
@@ -344,6 +786,7 @@ public static class BuildHomeCamp
|
||||
}
|
||||
public static string CaptureParts() => CaptureSet(Parts);
|
||||
public static string CaptureReuse() => CaptureSet(new[] { "tree", "lamp", "bench", "bush" });
|
||||
public static string CaptureProps() => CaptureSet(Props.Where(p => AssetDatabase.LoadAssetAtPath<GameObject>(PartPath(p))));
|
||||
static string CaptureSet(IEnumerable<string> parts)
|
||||
{
|
||||
foreach (var part in parts)
|
||||
@@ -351,7 +794,7 @@ public static class BuildHomeCamp
|
||||
var scene = EditorSceneManager.NewPreviewScene();
|
||||
try
|
||||
{
|
||||
var path = Parts.Contains(part) ? PartPath(part) : ReusePath(part);
|
||||
var path = Parts.Contains(part) || Props.Contains(part) ? PartPath(part) : ReusePath(part);
|
||||
var go = (GameObject)PrefabUtility.InstantiatePrefab(AssetDatabase.LoadAssetAtPath<GameObject>(path), scene);
|
||||
SetLayer(go); Light(scene, null);
|
||||
var b = BoundsOf(go); var extent = Mathf.Max(b.size.x, Mathf.Max(b.size.y, b.size.z));
|
||||
@@ -403,8 +846,20 @@ public static class BuildHomeCamp
|
||||
if (bg.sharedMaterial.shader.name != "Universal Render Pipeline/Unlit" || bg.shadowCastingMode != ShadowCastingMode.Off)
|
||||
throw new InvalidOperationException("Backdrop must be unlit without shadows");
|
||||
if (EditorBuildSettings.scenes.Any(s => s.path == ScenePath)) throw new InvalidOperationException("Review scene unexpectedly added to build");
|
||||
var groundRenderer = root.transform.Find("Ground").GetComponentInChildren<Renderer>();
|
||||
if (groundRenderer.sharedMaterial.shader.name != "XWorld/XTerrain" || !groundRenderer.receiveShadows)
|
||||
throw new InvalidOperationException("Ground must be tiled XTerrain receiving shadows");
|
||||
var unlitGameplay = root.GetComponentsInChildren<Renderer>(true)
|
||||
.Where(r => r.transform != backdrop && r.sharedMaterial && r.sharedMaterial.shader.name.Contains("Unlit")).Select(r => r.name).ToArray();
|
||||
if (unlitGameplay.Length > 0) throw new InvalidOperationException("Unlit outside backdrop: " + string.Join(",", unlitGameplay));
|
||||
var volume = root.GetComponentInChildren<Volume>();
|
||||
if (!volume || !volume.sharedProfile || !volume.sharedProfile.Has<Tonemapping>()) throw new InvalidOperationException("Missing post Volume");
|
||||
var cards = root.GetComponentsInChildren<Renderer>(true).Count(r => r.sharedMaterial && r.sharedMaterial.shader.name == "Universal Render Pipeline/Simple Lit");
|
||||
Directory.CreateDirectory(Review);
|
||||
File.WriteAllText(Path.Combine(Review, "verification.txt"), "PASS " + string.Join("; ", report) + "; assembledTriangles=" + triangleCount + "; ground collision 25/25; unlit quad backdrop; independent scene; cottage/Studio ground retained as review candidates and not placed");
|
||||
var tech = "TECH PASS " + string.Join("; ", report) + "; assembledTriangles=" + triangleCount + "; ground XTerrain lit+shadow; litFoliageCards=" + cards
|
||||
+ "; post Volume; fog; ground collision 25/25; unlit quad backdrop only; independent scene; cottage v2 placed as HouseLeft; Studio ground still review-only";
|
||||
const string art = "ART PENDING six-axis review against home_camp.png; see Tools/output/homecamp_review/compare_portrait.png";
|
||||
File.WriteAllText(Path.Combine(Review, "verification.txt"), tech + "\n" + art + "\n");
|
||||
return File.ReadAllText(Path.Combine(Review, "verification.txt"));
|
||||
}
|
||||
public static string OpenForReview()
|
||||
@@ -412,10 +867,18 @@ public static class BuildHomeCamp
|
||||
var scene = SceneManager.GetSceneByPath(ScenePath);
|
||||
if (!scene.IsValid() || !scene.isLoaded) scene = EditorSceneManager.OpenScene(ScenePath, OpenSceneMode.Additive);
|
||||
SceneManager.SetActiveScene(scene);
|
||||
var root = scene.GetRootGameObjects().Single(); Selection.activeGameObject = root;
|
||||
var root = scene.GetRootGameObjects().Single(g => g.name == "HomeCamp"); Selection.activeGameObject = root;
|
||||
var camera = root.GetComponentInChildren<Camera>();
|
||||
if (SceneView.lastActiveSceneView != null) SceneView.lastActiveSceneView.AlignViewToObject(camera.transform);
|
||||
return "Opened HomeCamp additively; existing scenes preserved.";
|
||||
}
|
||||
public static string CloseReview()
|
||||
{
|
||||
var scene = SceneManager.GetSceneByPath(ScenePath);
|
||||
if (!scene.IsValid() || !scene.isLoaded) return "HomeCamp not open.";
|
||||
if (scene.isDirty) throw new InvalidOperationException("HomeCamp has unsaved edits; save or discard manually.");
|
||||
EditorSceneManager.CloseScene(scene, true);
|
||||
return "Closed HomeCamp review scene.";
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,264 @@
|
||||
#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 BuildLittleBear
|
||||
{
|
||||
const string FolderPath = "Assets/Bundles/Characters/Human/LittleBear";
|
||||
const string ModelPath = FolderPath + "/LittleBear_Rigged.fbx";
|
||||
const string PrefabPath = FolderPath + "/LittleBear.prefab";
|
||||
const string ControllerPath = FolderPath + "/LittleBear.controller";
|
||||
const string ScenePath = "Assets/Bundles/Scenes/Review/LittleBearReview.unity";
|
||||
static string Repo => Directory.GetParent(Application.dataPath).Parent.FullName;
|
||||
static string Output => Path.Combine(Repo, "Tools/output/littlebear_studio");
|
||||
static string Review => Path.Combine(Output, "review");
|
||||
static float[] V(Vector3 v) => new[] { v.x, v.y, v.z };
|
||||
static string Hash(string p) { using (var s = SHA256.Create()) return BitConverter.ToString(s.ComputeHash(File.ReadAllBytes(p))).Replace("-", "").ToLowerInvariant(); }
|
||||
static void Folder(string p)
|
||||
{
|
||||
if (AssetDatabase.IsValidFolder(p)) return;
|
||||
var parent = Path.GetDirectoryName(p).Replace('\\', '/');
|
||||
Folder(parent); AssetDatabase.CreateFolder(parent, Path.GetFileName(p));
|
||||
}
|
||||
static void CopyNew(string source, string destination, bool updateOwnRig = false)
|
||||
{
|
||||
var full = Path.Combine(Directory.GetParent(Application.dataPath).FullName, destination);
|
||||
if (File.Exists(full))
|
||||
{
|
||||
if (Hash(full) != Hash(source))
|
||||
{
|
||||
if (updateOwnRig && destination == ModelPath) File.Copy(source, full, true);
|
||||
else throw new Exception("Destination exists with different data: " + destination);
|
||||
}
|
||||
}
|
||||
else File.Copy(source, full);
|
||||
AssetDatabase.ImportAsset(destination, ImportAssetOptions.ForceSynchronousImport);
|
||||
}
|
||||
static Bounds BoundsOf(GameObject go)
|
||||
{
|
||||
var bounds = new Bounds(); bool first = true;
|
||||
foreach (var r in go.GetComponentsInChildren<Renderer>()) { if (first) { bounds = r.bounds; first = false; } else bounds.Encapsulate(r.bounds); }
|
||||
return bounds;
|
||||
}
|
||||
static void Place(GameObject go, Scene scene) { SceneManager.MoveGameObjectToScene(go, scene); }
|
||||
public static string Build()
|
||||
{
|
||||
if (EditorApplication.isPlaying) throw new Exception("Requires Edit Mode");
|
||||
Folder(FolderPath); Directory.CreateDirectory(Review);
|
||||
foreach (var suffix in new[] { "base", "mix" })
|
||||
{
|
||||
string target = FolderPath + "/LittleBear_" + suffix + ".png";
|
||||
CopyNew(Path.Combine(Output, "xpbr/LittleBear_" + suffix + ".png"), target);
|
||||
var ti = (TextureImporter)AssetImporter.GetAtPath(target);
|
||||
ti.sRGBTexture = suffix == "base"; ti.alphaIsTransparency = false; ti.maxTextureSize = 1024; ti.SaveAndReimport();
|
||||
}
|
||||
string materialPath = FolderPath + "/LittleBear.mat";
|
||||
var material = AssetDatabase.LoadAssetAtPath<Material>(materialPath);
|
||||
if (!material)
|
||||
{
|
||||
var shader = Shader.Find("Test/XPbr"); if (!shader) throw new Exception("XPbr shader missing");
|
||||
material = new Material(shader); material.name = "LittleBear"; AssetDatabase.CreateAsset(material, materialPath);
|
||||
}
|
||||
material.SetTexture("_MainTex", AssetDatabase.LoadAssetAtPath<Texture2D>(FolderPath + "/LittleBear_base.png"));
|
||||
material.SetTexture("_MixTex", AssetDatabase.LoadAssetAtPath<Texture2D>(FolderPath + "/LittleBear_mix.png"));
|
||||
material.SetColor("_BaseColor", Color.white); material.SetFloat("_Metallic", 1); material.SetFloat("_Roughness", 1); material.SetFloat("_AO", 1);
|
||||
EditorUtility.SetDirty(material); AssetDatabase.SaveAssetIfDirty(material);
|
||||
CopyNew(Path.Combine(Output, "rig_mixamo41/LittleBear_Rigged.fbx"), ModelPath, true);
|
||||
var importer = (ModelImporter)AssetImporter.GetAtPath(ModelPath);
|
||||
importer.animationType = ModelImporterAnimationType.Human; importer.avatarSetup = ModelImporterAvatarSetup.CreateFromThisModel;
|
||||
importer.importAnimation = false; importer.importCameras = false; importer.importLights = false;
|
||||
importer.globalScale = 1; importer.useFileScale = true; importer.isReadable = true;
|
||||
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), "LittleBear_Body"), material);
|
||||
importer.SaveAndReimport();
|
||||
var avatar = AssetDatabase.LoadAllAssetsAtPath(ModelPath).OfType<Avatar>().FirstOrDefault();
|
||||
if (!avatar || !avatar.isValid || !avatar.isHuman) throw new Exception("Invalid LittleBear Humanoid Avatar");
|
||||
|
||||
var shared = AssetDatabase.LoadAssetAtPath<AnimatorController>("Assets/Bundles/Characters/Human/Common/Man.controller");
|
||||
if (!shared) throw new Exception("Missing approved shared human motions");
|
||||
var idleClip = shared.layers[0].stateMachine.states.Single(s => s.state.name == "Idle").state.motion;
|
||||
var walkClip = shared.layers[0].stateMachine.states.Single(s => s.state.name == "walk").state.motion;
|
||||
var controller = AssetDatabase.LoadAssetAtPath<AnimatorController>(ControllerPath);
|
||||
if (!controller)
|
||||
{
|
||||
controller = AnimatorController.CreateAnimatorControllerAtPath(ControllerPath);
|
||||
controller.AddParameter("Motion", AnimatorControllerParameterType.Int);
|
||||
var sm = controller.layers[0].stateMachine;
|
||||
var idle = sm.AddState("Idle"); idle.motion = idleClip; idle.iKOnFeet = true; sm.defaultState = idle;
|
||||
var walk = sm.AddState("Walk"); walk.motion = walkClip; walk.iKOnFeet = true;
|
||||
var toWalk = idle.AddTransition(walk); toWalk.hasExitTime = false; toWalk.duration = .15f; toWalk.AddCondition(AnimatorConditionMode.Equals, 1, "Motion");
|
||||
var toIdle = walk.AddTransition(idle); toIdle.hasExitTime = false; toIdle.duration = .15f; toIdle.AddCondition(AnimatorConditionMode.Equals, 0, "Motion");
|
||||
EditorUtility.SetDirty(controller); AssetDatabase.SaveAssetIfDirty(controller);
|
||||
}
|
||||
|
||||
var preview = EditorSceneManager.NewPreviewScene();
|
||||
try
|
||||
{
|
||||
var go = (GameObject)PrefabUtility.InstantiatePrefab(AssetDatabase.LoadAssetAtPath<GameObject>(ModelPath), preview);
|
||||
go.name = "LittleBear";
|
||||
var a = go.GetComponent<Animator>(); a.avatar = avatar; a.runtimeAnimatorController = controller;
|
||||
a.applyRootMotion = false; a.cullingMode = AnimatorCullingMode.AlwaysAnimate;
|
||||
foreach (var r in go.GetComponentsInChildren<SkinnedMeshRenderer>()) { r.sharedMaterial = material; r.updateWhenOffscreen = true; }
|
||||
PrefabUtility.SaveAsPrefabAsset(go, PrefabPath);
|
||||
}
|
||||
finally { EditorSceneManager.ClosePreviewScene(preview); }
|
||||
|
||||
string staticPath = FolderPath + "/LittleBear_Static.fbx";
|
||||
CopyNew(Path.Combine(Output, "static/LittleBear.fbx"), staticPath);
|
||||
var si = (ModelImporter)AssetImporter.GetAtPath(staticPath); si.animationType = ModelImporterAnimationType.None;
|
||||
si.importAnimation = false; si.importCameras = false; si.importLights = false; si.isReadable = true; si.SaveAndReimport();
|
||||
preview = EditorSceneManager.NewPreviewScene();
|
||||
try
|
||||
{
|
||||
var go = (GameObject)PrefabUtility.InstantiatePrefab(AssetDatabase.LoadAssetAtPath<GameObject>(staticPath), preview);
|
||||
go.name = "LittleBear_Static";
|
||||
foreach (var r in go.GetComponentsInChildren<Renderer>()) r.sharedMaterial = material;
|
||||
go.transform.localScale = Vector3.one * (1.2f / BoundsOf(go).size.y);
|
||||
PrefabUtility.SaveAsPrefabAsset(go, FolderPath + "/LittleBear_Static.prefab");
|
||||
}
|
||||
finally { EditorSceneManager.ClosePreviewScene(preview); }
|
||||
BuildReview();
|
||||
return Verify();
|
||||
}
|
||||
static void BuildReview()
|
||||
{
|
||||
if (File.Exists(Path.Combine(Directory.GetParent(Application.dataPath).FullName, ScenePath))) return;
|
||||
Folder("Assets/Bundles/Scenes/Review");
|
||||
var previous = SceneManager.GetActiveScene();
|
||||
var scene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Additive);
|
||||
try
|
||||
{
|
||||
var bear = (GameObject)PrefabUtility.InstantiatePrefab(AssetDatabase.LoadAssetAtPath<GameObject>(PrefabPath), scene);
|
||||
bear.name = "LittleBear";
|
||||
var floor = GameObject.CreatePrimitive(PrimitiveType.Plane); Place(floor, scene); floor.name = "ReviewGround";
|
||||
floor.transform.localScale = Vector3.one * .5f;
|
||||
string groundPath = FolderPath + "/ReviewGround.mat";
|
||||
var ground = AssetDatabase.LoadAssetAtPath<Material>(groundPath);
|
||||
if (!ground) { ground = new Material(Shader.Find("Universal Render Pipeline/Lit")); ground.color = new Color(.30f, .39f, .35f); AssetDatabase.CreateAsset(ground, groundPath); }
|
||||
floor.GetComponent<Renderer>().sharedMaterial = ground;
|
||||
var light = new GameObject("KeyLight").AddComponent<Light>(); Place(light.gameObject, scene);
|
||||
light.type = LightType.Directional; light.intensity = 1.4f; light.transform.rotation = Quaternion.Euler(35, -140, 0);
|
||||
var fill = new GameObject("FillLight").AddComponent<Light>(); Place(fill.gameObject, scene);
|
||||
fill.type = LightType.Directional; fill.intensity = .6f; fill.transform.rotation = Quaternion.Euler(20, 45, 0);
|
||||
var camera = new GameObject("ReviewCamera").AddComponent<Camera>(); Place(camera.gameObject, scene);
|
||||
camera.orthographic = true; camera.orthographicSize = .85f; camera.transform.position = new Vector3(1.6f, 1.0f, 3.0f);
|
||||
camera.transform.LookAt(new Vector3(0, .6f, 0)); camera.clearFlags = CameraClearFlags.SolidColor; camera.backgroundColor = new Color(.18f, .23f, .26f);
|
||||
EditorSceneManager.SaveScene(scene, ScenePath);
|
||||
}
|
||||
finally { EditorSceneManager.CloseScene(scene, true); if (previous.IsValid()) SceneManager.SetActiveScene(previous); }
|
||||
}
|
||||
public static string Verify()
|
||||
{
|
||||
var scene = EditorSceneManager.NewPreviewScene();
|
||||
Directory.CreateDirectory(Review);
|
||||
try
|
||||
{
|
||||
var go = (GameObject)PrefabUtility.InstantiatePrefab(AssetDatabase.LoadAssetAtPath<GameObject>(PrefabPath), scene);
|
||||
var a = go.GetComponent<Animator>();
|
||||
var skin = go.GetComponentInChildren<SkinnedMeshRenderer>();
|
||||
if (!a || !a.avatar || !a.isHuman || !a.avatar.isValid) throw new Exception("Invalid Humanoid");
|
||||
var mesh = new Mesh(); skin.BakeMesh(mesh);
|
||||
var rest = mesh.vertices.Select(p => go.transform.InverseTransformPoint(skin.transform.TransformPoint(p))).ToArray();
|
||||
var min = rest.Aggregate(Vector3.positiveInfinity, Vector3.Min); var max = rest.Aggregate(Vector3.negativeInfinity, Vector3.Max);
|
||||
if (Mathf.Abs(max.y - min.y - 1.2f) > .015f || Mathf.Abs(min.y) > .005f) throw new Exception("Wrong height or floor");
|
||||
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 (toes[0].z <= ankles[0].z || toes[1].z <= ankles[1].z) throw new Exception("Feet must face +Z");
|
||||
if (skin.sharedMesh.triangles.Length / 3 > 3900 || skin.bones.Length != 41) throw new Exception("Triangle or skeleton budget");
|
||||
if (skin.sharedMaterial.shader.name != "Test/XPbr") throw new Exception("XPbr required");
|
||||
var counts = skin.sharedMesh.GetBonesPerVertex(); int influenceCount = 0; foreach (var n in counts) influenceCount = Math.Max(influenceCount, n); counts.Dispose();
|
||||
if (influenceCount > 4) throw new Exception("More than four influences");
|
||||
var indices = mesh.triangles; var lengths = new float[indices.Length];
|
||||
for (int i = 0; i < indices.Length; i++) lengths[i] = Vector3.Distance(rest[indices[i]], rest[indices[(i / 3) * 3 + (i + 1) % 3]]);
|
||||
var states = new List<object>();
|
||||
a.Rebind(); a.Update(0);
|
||||
foreach (var state in new[] { "Idle", "Walk" })
|
||||
{
|
||||
float minGap = float.MaxValue, maxGap = float.MinValue, minFloor = float.MaxValue, maxFloor = float.MinValue, maxStretch = 0;
|
||||
int crossed = 0; float armSwing = 0, legSwing = 0;
|
||||
int worstEdge = 0; float worstPhase = 0;
|
||||
Vector3 firstArm = Vector3.zero, firstKnee = Vector3.zero;
|
||||
for (int i = 0; i < 125; i++)
|
||||
{
|
||||
a.Play("Base Layer." + state, 0, i / 124f); 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++;
|
||||
skin.BakeMesh(mesh); var vertices = mesh.vertices;
|
||||
var floor = vertices.Min(p => go.transform.InverseTransformPoint(skin.transform.TransformPoint(p)).y);
|
||||
minFloor = Mathf.Min(minFloor, floor); maxFloor = Mathf.Max(maxFloor, floor);
|
||||
if (vertices.Any(p => float.IsNaN(p.x) || float.IsInfinity(p.x))) throw new Exception("Invalid skin positions");
|
||||
for (int j = 0; j < indices.Length; j++) if (lengths[j] > .0024f)
|
||||
{
|
||||
float stretch = Vector3.Distance(vertices[indices[j]], vertices[indices[(j / 3) * 3 + (j + 1) % 3]]) / lengths[j];
|
||||
if (stretch > maxStretch) { maxStretch = stretch; worstEdge = j; worstPhase = i / 124f; }
|
||||
}
|
||||
var hips = a.GetBoneTransform(HumanBodyBones.Hips).position;
|
||||
var hand = a.GetBoneTransform(HumanBodyBones.LeftHand).position - hips;
|
||||
var knee = a.GetBoneTransform(HumanBodyBones.LeftLowerLeg).position - hips;
|
||||
if (i == 0) { firstArm = hand; firstKnee = knee; }
|
||||
armSwing = Mathf.Max(armSwing, Vector3.Distance(firstArm, hand)); legSwing = Mathf.Max(legSwing, Vector3.Distance(firstKnee, knee));
|
||||
}
|
||||
int edgeA = indices[worstEdge], edgeB = indices[(worstEdge / 3) * 3 + (worstEdge + 1) % 3];
|
||||
states.Add(new { state, samples = 125, crossedAnkles = crossed, minGap, maxGap, minFloor, maxFloor, maxEdgeStretch = maxStretch, armSwing, legSwing,
|
||||
worstPhase, worstRestA = V(rest[edgeA]), worstRestB = V(rest[edgeB]) });
|
||||
a.Play("Base Layer." + state, 0, .25f); a.Update(0);
|
||||
Capture(scene, state.ToLowerInvariant() + ".png", new Vector3(.8f, .78f, 2.8f));
|
||||
if (crossed > 0) throw new Exception("Crossed ankles in " + state + ": " + crossed);
|
||||
if (maxStretch > 2.5f) throw new Exception("Excessive skin stretch in " + state + ": " + maxStretch);
|
||||
if (state == "Walk" && legSwing < .025f) throw new Exception("No visible walking motion");
|
||||
}
|
||||
a.Play("Base Layer.Idle", 0, 0); a.Update(0); a.SetInteger("Motion", 1);
|
||||
for (int i = 0; i < 30; i++) a.Update(1f / 60);
|
||||
if (!a.GetCurrentAnimatorStateInfo(0).IsName("Walk")) throw new Exception("Motion=1 transition failed");
|
||||
a.SetInteger("Motion", 0); for (int i = 0; i < 30; i++) a.Update(1f / 60);
|
||||
if (!a.GetCurrentAnimatorStateInfo(0).IsName("Idle")) throw new Exception("Motion=0 transition failed");
|
||||
UnityEngine.Object.DestroyImmediate(mesh);
|
||||
var report = new { prefab = PrefabPath, reviewScene = ScenePath, avatar = a.avatar.name, humanoidValid = true,
|
||||
bones = skin.bones.Length, triangles = skin.sharedMesh.triangles.Length / 3, maxInfluences = influenceCount,
|
||||
rest = new { boundsMin = V(min), boundsMax = V(max), height = max.y - min.y, front = "+Z", rootRotation = V(go.transform.localEulerAngles), rootScale = V(go.transform.localScale) },
|
||||
shader = skin.sharedMaterial.shader.name, motionTransitionsValid = true, states, limitations = "Verified Idle and Walking only; floor range recorded, zero sliding not asserted" };
|
||||
string json = JsonConvert.SerializeObject(report, Formatting.Indented); File.WriteAllText(Path.Combine(Review, "unity_validation.json"), json); return json;
|
||||
}
|
||||
finally { EditorSceneManager.ClosePreviewScene(scene); }
|
||||
}
|
||||
static void Capture(Scene scene, string filename, Vector3 position)
|
||||
{
|
||||
var light = new GameObject("CaptureLight").AddComponent<Light>(); Place(light.gameObject, scene); light.type = LightType.Directional; light.intensity = 1.5f; light.transform.rotation = Quaternion.Euler(30, -140, 0);
|
||||
var fill = new GameObject("CaptureFill").AddComponent<Light>(); Place(fill.gameObject, scene); fill.type = LightType.Directional; fill.intensity = .5f; fill.transform.rotation = Quaternion.Euler(20, 45, 0);
|
||||
var camera = new GameObject("CaptureCamera").AddComponent<Camera>(); Place(camera.gameObject, scene); camera.scene = scene;
|
||||
camera.orthographic = true; camera.orthographicSize = .76f; camera.transform.position = position; camera.transform.LookAt(new Vector3(0, .6f, 0));
|
||||
camera.clearFlags = CameraClearFlags.SolidColor; camera.backgroundColor = new Color(.18f, .23f, .26f); camera.nearClipPlane = .01f; camera.farClipPlane = 20;
|
||||
var rt = new RenderTexture(960, 1000, 24); var texture = new Texture2D(960, 1000, TextureFormat.RGB24, false); var previous = RenderTexture.active;
|
||||
try { camera.targetTexture = rt; camera.Render(); RenderTexture.active = rt; texture.ReadPixels(new Rect(0, 0, 960, 1000), 0, 0); texture.Apply(); File.WriteAllBytes(Path.Combine(Review, filename), texture.EncodeToPNG()); }
|
||||
finally { RenderTexture.active = previous; camera.targetTexture = null; rt.Release(); UnityEngine.Object.DestroyImmediate(rt); UnityEngine.Object.DestroyImmediate(texture); UnityEngine.Object.DestroyImmediate(camera.gameObject); UnityEngine.Object.DestroyImmediate(light.gameObject); UnityEngine.Object.DestroyImmediate(fill.gameObject); }
|
||||
}
|
||||
public static string ValidateRegistration()
|
||||
{
|
||||
var manifest = Newtonsoft.Json.Linq.JObject.Parse(File.ReadAllText(Path.Combine(Repo, "Doc/Design/Character/character_unit_review_manifest.json")));
|
||||
var subjects = manifest["subjects"].Children().Where(s => (string)s["id"] == "LittleBear" || (string)s["id"] == "LittleBear_Static").ToArray();
|
||||
if (subjects.Length != 2) throw new Exception("Expected two LittleBear registrations");
|
||||
foreach (var s in subjects)
|
||||
{
|
||||
if ((string)s["localForward"] != "+Z" || (int)s["maxTriangles"] != 3000) throw new Exception("Wrong LittleBear convention/budget");
|
||||
if (!AssetDatabase.LoadAssetAtPath<GameObject>((string)s["prefab"])) throw new Exception("Missing LittleBear prefab");
|
||||
}
|
||||
if (!AssetDatabase.LoadAssetAtPath<SceneAsset>(ScenePath)) throw new Exception("Missing LittleBear review scene");
|
||||
var controller = AssetDatabase.LoadAssetAtPath<AnimatorController>(ControllerPath);
|
||||
var result = new { scopedSubjects = 2, reviewSceneValid = true,
|
||||
clips = controller.layers[0].stateMachine.states.Select(s => new { state = s.state.name, clip = AssetDatabase.GetAssetPath(s.state.motion), footIK = s.state.iKOnFeet }).ToArray() };
|
||||
string json = JsonConvert.SerializeObject(result, Formatting.Indented); File.WriteAllText(Path.Combine(Review, "registration_validation.json"), json); return json;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -6,7 +6,7 @@ using UnityEngine;
|
||||
using UnityEngine.Rendering;
|
||||
|
||||
/// <summary>
|
||||
/// 把 LmGirl / LmCat 摆进同一观察场景。正面为局部 -Z,相机在 -Z 侧。
|
||||
/// 历史 LmGirl / LmCat 观察场景,保留旧 -Z 资产及相机方向;项目新资源采用 +Z 正面。
|
||||
/// unity command run_script --file AgentScripts/BuildModelView.cs --entry BuildModelView.Run
|
||||
/// </summary>
|
||||
public static class BuildModelView
|
||||
|
||||
@@ -0,0 +1,315 @@
|
||||
#if UNITY_EDITOR
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using AIGame.EditorTools;
|
||||
using UnityEditor;
|
||||
using UnityEditor.SceneManagement;
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
|
||||
/// <summary>
|
||||
/// Pipeline script to render high quality 3D portrait icons for formal companions (0..9).
|
||||
/// Replaces the legacy primitive geometry portraits with actual authored companion models.
|
||||
/// </summary>
|
||||
public static class BuildPetPortraits
|
||||
{
|
||||
const string Pictures = "Assets/Bundles/UI/Texture/Prototype";
|
||||
const string Screenshots = "Assets/Screenshots";
|
||||
const string Animal = "Assets/Bundles/Characters/Animal/";
|
||||
|
||||
public static readonly Dictionary<int, string> FormalPets = new Dictionary<int, string>
|
||||
{
|
||||
[0] = Animal + "RockBallStudio/RockBallStudio_Companion.prefab",
|
||||
[1] = Animal + "FireCatStudio/FireCatStudio_Companion.prefab",
|
||||
[2] = Animal + "BudBudStudio/BudBudStudio_Companion.prefab",
|
||||
[3] = Animal + "SparkBirdStudio/SparkBirdStudio_Companion.prefab",
|
||||
[4] = Animal + "BubbleTurtle/BubbleTurtle_Companion.prefab",
|
||||
[5] = Animal + "RockRhinoStudio/RockRhinoStudio_Companion.prefab",
|
||||
[6] = Animal + "FlameLion/FlameLion_Companion.prefab",
|
||||
[7] = Animal + "VineSpriteStudio/VineSpriteStudio_Companion.prefab",
|
||||
[8] = Animal + "ThunderLeopardStudio/ThunderLeopardStudio_Companion.prefab",
|
||||
[9] = Animal + "TideWhaleStudio/TideWhaleStudio_Companion.prefab",
|
||||
};
|
||||
|
||||
public static readonly string[] PetNames =
|
||||
{
|
||||
"RockBall (岩团团)",
|
||||
"FireCat (火喵)",
|
||||
"BudBud (芽芽)",
|
||||
"SparkBird (闪闪雀)",
|
||||
"BubbleTurtle (泡泡龟)",
|
||||
"RockRhino (岩甲犀)",
|
||||
"FlameLion (烈焰狮)",
|
||||
"VineSprite (藤蔓灵)",
|
||||
"ThunderLeopard (雷鸣豹)",
|
||||
"TideWhale (潮汐鲸)"
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Executes portrait re-rendering and generates a visual comparison sheet.
|
||||
/// </summary>
|
||||
public static string Run()
|
||||
{
|
||||
if (EditorApplication.isPlayingOrWillChangePlaymode)
|
||||
throw new InvalidOperationException("Run in Edit Mode.");
|
||||
|
||||
// 1. Backup old portraits into memory for comparison
|
||||
var oldPortraits = new Texture2D[10];
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
string oldPath = $"{Pictures}/proto_pet_{i}.png";
|
||||
byte[] bytes = File.Exists(oldPath) ? File.ReadAllBytes(oldPath) : null;
|
||||
if (bytes != null)
|
||||
{
|
||||
var tex = new Texture2D(2, 2, TextureFormat.RGBA32, false);
|
||||
tex.LoadImage(bytes);
|
||||
oldPortraits[i] = tex;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Render each formal companion
|
||||
var newPortraits = new Texture2D[10];
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
newPortraits[i] = RenderPortrait(i);
|
||||
}
|
||||
|
||||
// 3. Update sprite atlas
|
||||
UiSpriteAtlasGenerator.GenerateAll();
|
||||
|
||||
// 4. Generate comparison sheet
|
||||
GenerateComparisonSheet(oldPortraits, newPortraits);
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
if (oldPortraits[i] != null) UnityEngine.Object.DestroyImmediate(oldPortraits[i]);
|
||||
if (newPortraits[i] != null) UnityEngine.Object.DestroyImmediate(newPortraits[i]);
|
||||
}
|
||||
|
||||
AssetDatabase.Refresh();
|
||||
return "Pet portraits generated successfully.";
|
||||
}
|
||||
|
||||
private static Texture2D RenderPortrait(int id)
|
||||
{
|
||||
string prefabPath = FormalPets[id];
|
||||
var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(prefabPath);
|
||||
if (prefab == null)
|
||||
throw new FileNotFoundException($"Formal companion prefab not found: {prefabPath}");
|
||||
|
||||
var scene = EditorSceneManager.NewPreviewScene();
|
||||
var previousRT = RenderTexture.active;
|
||||
var rt = new RenderTexture(256, 256, 24, RenderTextureFormat.ARGB32);
|
||||
|
||||
Texture2D resultTex = new Texture2D(256, 256, TextureFormat.RGBA32, false);
|
||||
try
|
||||
{
|
||||
var root = new GameObject("PortraitRoot");
|
||||
SceneManager.MoveGameObjectToScene(root, scene);
|
||||
|
||||
var instance = UnityEngine.Object.Instantiate(prefab, root.transform);
|
||||
instance.name = "Model";
|
||||
instance.transform.localPosition = Vector3.zero;
|
||||
instance.transform.localRotation = Quaternion.identity;
|
||||
|
||||
// Put everything on layer 30 for isolated rendering
|
||||
foreach (var trans in root.GetComponentsInChildren<Transform>(true))
|
||||
{
|
||||
trans.gameObject.layer = 30;
|
||||
}
|
||||
|
||||
// Calculate renderer bounds
|
||||
var renderers = instance.GetComponentsInChildren<Renderer>(true);
|
||||
Bounds bounds = new Bounds(instance.transform.position, Vector3.zero);
|
||||
bool hasBounds = false;
|
||||
foreach (var r in renderers)
|
||||
{
|
||||
if (r is ParticleSystemRenderer) continue;
|
||||
if (!hasBounds)
|
||||
{
|
||||
bounds = r.bounds;
|
||||
hasBounds = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
bounds.Encapsulate(r.bounds);
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasBounds)
|
||||
bounds = new Bounds(instance.transform.position + Vector3.up * 0.3f, Vector3.one * 0.6f);
|
||||
|
||||
// Subtle rotation: companions face -Z by default, turn slightly to show 3/4 front profile
|
||||
instance.transform.localRotation = Quaternion.Euler(0, 18f, 0);
|
||||
|
||||
// Recompute bounds after rotation
|
||||
bounds = new Bounds(instance.transform.position, Vector3.zero);
|
||||
hasBounds = false;
|
||||
foreach (var r in instance.GetComponentsInChildren<Renderer>(true))
|
||||
{
|
||||
if (r is ParticleSystemRenderer) continue;
|
||||
if (!hasBounds)
|
||||
{
|
||||
bounds = r.bounds;
|
||||
hasBounds = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
bounds.Encapsulate(r.bounds);
|
||||
}
|
||||
}
|
||||
|
||||
// Camera setup
|
||||
var camGO = new GameObject("PortraitCamera");
|
||||
camGO.transform.SetParent(root.transform, false);
|
||||
var camera = camGO.AddComponent<Camera>();
|
||||
camera.scene = scene;
|
||||
camera.clearFlags = CameraClearFlags.SolidColor;
|
||||
camera.backgroundColor = Color.clear;
|
||||
camera.cullingMask = 1 << 30;
|
||||
camera.orthographic = true;
|
||||
camera.allowHDR = false;
|
||||
camera.targetTexture = rt;
|
||||
|
||||
// Target roughly upper body/head center of the pet
|
||||
Vector3 target = bounds.center + Vector3.up * (bounds.size.y * 0.04f);
|
||||
float maxExtent = Mathf.Max(bounds.size.x, bounds.size.y);
|
||||
// Orthographic size covers the model with nice framing padding (~12%)
|
||||
camera.orthographicSize = (maxExtent * 0.5f) * 1.15f;
|
||||
|
||||
// Camera placed along -Z looking slightly down (elevation ~10 degrees)
|
||||
Vector3 camPos = target + new Vector3(0, maxExtent * 0.22f, -3.5f);
|
||||
camera.transform.position = camPos;
|
||||
camera.transform.LookAt(target);
|
||||
|
||||
// Lights
|
||||
// 1. Key directional light (warm top-left)
|
||||
var keyLightGO = new GameObject("KeyLight");
|
||||
keyLightGO.transform.SetParent(root.transform, false);
|
||||
var keyLight = keyLightGO.AddComponent<Light>();
|
||||
keyLight.type = LightType.Directional;
|
||||
keyLight.color = new Color(1f, 0.96f, 0.92f);
|
||||
keyLight.intensity = 1.35f;
|
||||
keyLight.cullingMask = 1 << 30;
|
||||
keyLightGO.transform.rotation = Quaternion.Euler(28, -25, 0);
|
||||
|
||||
// 2. Rim/Back light (cool, highlighting contours)
|
||||
var rimLightGO = new GameObject("RimLight");
|
||||
rimLightGO.transform.SetParent(root.transform, false);
|
||||
var rimLight = rimLightGO.AddComponent<Light>();
|
||||
rimLight.type = LightType.Directional;
|
||||
rimLight.color = new Color(0.85f, 0.92f, 1f);
|
||||
rimLight.intensity = 0.85f;
|
||||
rimLight.cullingMask = 1 << 30;
|
||||
rimLightGO.transform.rotation = Quaternion.Euler(-20, 150, 0);
|
||||
|
||||
// Render
|
||||
camera.Render();
|
||||
RenderTexture.active = rt;
|
||||
resultTex.ReadPixels(new Rect(0, 0, 256, 256), 0, 0);
|
||||
resultTex.Apply();
|
||||
|
||||
// Save to disk
|
||||
string destPath = $"{Pictures}/proto_pet_{id}.png";
|
||||
File.WriteAllBytes(destPath, resultTex.EncodeToPNG());
|
||||
}
|
||||
finally
|
||||
{
|
||||
RenderTexture.active = previousRT;
|
||||
EditorSceneManager.ClosePreviewScene(scene);
|
||||
rt.Release();
|
||||
UnityEngine.Object.DestroyImmediate(rt);
|
||||
}
|
||||
|
||||
// Configure TextureImporter
|
||||
string assetPath = $"{Pictures}/proto_pet_{id}.png";
|
||||
AssetDatabase.ImportAsset(assetPath, ImportAssetOptions.ForceUpdate);
|
||||
var importer = (TextureImporter)AssetImporter.GetAtPath(assetPath);
|
||||
importer.textureType = TextureImporterType.Sprite;
|
||||
importer.spriteImportMode = SpriteImportMode.Single;
|
||||
importer.mipmapEnabled = false;
|
||||
importer.alphaIsTransparency = true;
|
||||
importer.textureCompression = TextureImporterCompression.Uncompressed;
|
||||
importer.filterMode = FilterMode.Bilinear;
|
||||
var settings = new TextureImporterSettings();
|
||||
importer.ReadTextureSettings(settings);
|
||||
settings.spriteMeshType = SpriteMeshType.FullRect;
|
||||
importer.SetTextureSettings(settings);
|
||||
importer.SaveAndReimport();
|
||||
|
||||
return resultTex;
|
||||
}
|
||||
|
||||
private static void GenerateComparisonSheet(Texture2D[] oldPortraits, Texture2D[] newPortraits)
|
||||
{
|
||||
// 10 columns, 2 rows (Row 0: Old Prototype, Row 1: New 3D Formal)
|
||||
int cellW = 160;
|
||||
int cellH = 160;
|
||||
int padding = 16;
|
||||
int headerH = 40;
|
||||
int totalW = cellW * 10 + padding * 11;
|
||||
int totalH = cellH * 2 + padding * 3 + headerH;
|
||||
|
||||
var sheet = new Texture2D(totalW, totalH, TextureFormat.RGBA32, false);
|
||||
// Fill subtle dark background
|
||||
Color bg = new Color(0.12f, 0.14f, 0.18f, 1f);
|
||||
Color[] fill = new Color[totalW * totalH];
|
||||
for (int i = 0; i < fill.Length; i++) fill[i] = bg;
|
||||
sheet.SetPixels(fill);
|
||||
|
||||
for (int col = 0; col < 10; col++)
|
||||
{
|
||||
int x = padding + col * (cellW + padding);
|
||||
|
||||
// Row 0: Old (bottom row in texture coordinates)
|
||||
int yOld = padding;
|
||||
DrawCell(sheet, oldPortraits[col], x, yOld, cellW, cellH, new Color(0.2f, 0.22f, 0.26f, 1f));
|
||||
|
||||
// Row 1: New (top row in texture coordinates)
|
||||
int yNew = padding * 2 + cellH;
|
||||
DrawCell(sheet, newPortraits[col], x, yNew, cellW, cellH, new Color(0.18f, 0.26f, 0.35f, 1f));
|
||||
}
|
||||
|
||||
sheet.Apply();
|
||||
if (!Directory.Exists(Screenshots)) Directory.CreateDirectory(Screenshots);
|
||||
string comparisonPath = $"{Screenshots}/pet_portraits_upgrade_comparison.png";
|
||||
File.WriteAllBytes(comparisonPath, sheet.EncodeToPNG());
|
||||
UnityEngine.Object.DestroyImmediate(sheet);
|
||||
AssetDatabase.ImportAsset(comparisonPath, ImportAssetOptions.ForceUpdate);
|
||||
}
|
||||
|
||||
private static void DrawCell(Texture2D dest, Texture2D src, int startX, int startY, int w, int h, Color frameBg)
|
||||
{
|
||||
// Draw frame background
|
||||
for (int y = 0; y < h; y++)
|
||||
{
|
||||
for (int x = 0; x < w; x++)
|
||||
{
|
||||
dest.SetPixel(startX + x, startY + y, frameBg);
|
||||
}
|
||||
}
|
||||
|
||||
if (src == null) return;
|
||||
|
||||
// Bilinear blend source into destination
|
||||
for (int y = 0; y < h; y++)
|
||||
{
|
||||
float v = (float)y / (h - 1);
|
||||
for (int x = 0; x < w; x++)
|
||||
{
|
||||
float u = (float)x / (w - 1);
|
||||
Color c = src.GetPixelBilinear(u, v);
|
||||
if (c.a > 0.001f)
|
||||
{
|
||||
Color current = dest.GetPixel(startX + x, startY + y);
|
||||
Color blended = Color.Lerp(current, c, c.a);
|
||||
blended.a = 1f;
|
||||
dest.SetPixel(startX + x, startY + y, blended);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -323,15 +323,49 @@ public static class BuildPrototype
|
||||
try
|
||||
{
|
||||
var root = new GameObject("PortraitRoot"); SceneManager.MoveGameObjectToScene(root, scene);
|
||||
Pet(id, root.transform, "PortraitPet");
|
||||
var camera = Group("PortraitCamera", root.transform).gameObject.AddComponent<Camera>(); camera.scene = scene;
|
||||
camera.clearFlags = CameraClearFlags.SolidColor; camera.backgroundColor = Color.clear; camera.cullingMask = 1 << 30;
|
||||
bool primitiveBird = id == 3 && !FormalPets.ContainsKey(id);
|
||||
camera.orthographic = true; camera.orthographicSize = primitiveBird ? 1.22f : id >= 9 ? 1.34f : 1.08f; camera.allowHDR = false;
|
||||
camera.transform.position = new Vector3(1.6f, 1.55f, -4); camera.transform.LookAt(new Vector3(0, primitiveBird ? .94f : id >= 5 ? .86f : .82f, 0)); camera.targetTexture = rt;
|
||||
var light = Group("PortraitLight", root.transform).gameObject.AddComponent<Light>(); light.type = LightType.Directional;
|
||||
light.intensity = 1.15f; light.cullingMask = 1 << 30; light.transform.rotation = Quaternion.Euler(35, -35, 0);
|
||||
camera.Render(); RenderTexture.active = rt;
|
||||
bool formal = FormalPets.TryGetValue(id, out var prefabPath) && AssetDatabase.LoadAssetAtPath<GameObject>(prefabPath) != null;
|
||||
if (formal)
|
||||
{
|
||||
var instance = (GameObject)PrefabUtility.InstantiatePrefab(AssetDatabase.LoadAssetAtPath<GameObject>(prefabPath), root.transform);
|
||||
instance.transform.localPosition = Vector3.zero;
|
||||
instance.transform.localRotation = Quaternion.Euler(0, 18f, 0);
|
||||
foreach (var trans in root.GetComponentsInChildren<Transform>(true)) trans.gameObject.layer = 30;
|
||||
var renderers = instance.GetComponentsInChildren<Renderer>(true);
|
||||
Bounds bounds = new Bounds(instance.transform.position, Vector3.zero);
|
||||
bool hasBounds = false;
|
||||
foreach (var r in renderers)
|
||||
{
|
||||
if (r is ParticleSystemRenderer) continue;
|
||||
if (!hasBounds) { bounds = r.bounds; hasBounds = true; } else bounds.Encapsulate(r.bounds);
|
||||
}
|
||||
var camera = Group("PortraitCamera", root.transform).gameObject.AddComponent<Camera>(); camera.scene = scene;
|
||||
camera.clearFlags = CameraClearFlags.SolidColor; camera.backgroundColor = Color.clear; camera.cullingMask = 1 << 30;
|
||||
camera.orthographic = true; camera.allowHDR = false; camera.targetTexture = rt;
|
||||
float maxDim = Mathf.Max(bounds.size.x, bounds.size.y);
|
||||
camera.orthographicSize = (maxDim * 0.5f) * 1.15f;
|
||||
Vector3 target = bounds.center + Vector3.up * (bounds.size.y * 0.04f);
|
||||
camera.transform.position = target + new Vector3(0, maxDim * 0.22f, -3.5f);
|
||||
camera.transform.LookAt(target);
|
||||
|
||||
var keyLight = Group("PortraitLight", root.transform).gameObject.AddComponent<Light>(); keyLight.type = LightType.Directional;
|
||||
keyLight.color = new Color(1f, 0.96f, 0.92f); keyLight.intensity = 1.35f; keyLight.cullingMask = 1 << 30; keyLight.transform.rotation = Quaternion.Euler(28, -25, 0);
|
||||
|
||||
var rimLight = Group("PortraitRim", root.transform).gameObject.AddComponent<Light>(); rimLight.type = LightType.Directional;
|
||||
rimLight.color = new Color(0.85f, 0.92f, 1f); rimLight.intensity = 0.85f; rimLight.cullingMask = 1 << 30; rimLight.transform.rotation = Quaternion.Euler(-20, 150, 0);
|
||||
camera.Render();
|
||||
}
|
||||
else
|
||||
{
|
||||
Pet(id, root.transform, "PortraitPet");
|
||||
var camera = Group("PortraitCamera", root.transform).gameObject.AddComponent<Camera>(); camera.scene = scene;
|
||||
camera.clearFlags = CameraClearFlags.SolidColor; camera.backgroundColor = Color.clear; camera.cullingMask = 1 << 30;
|
||||
bool primitiveBird = id == 3 && !FormalPets.ContainsKey(id);
|
||||
camera.orthographic = true; camera.orthographicSize = primitiveBird ? 1.22f : id >= 9 ? 1.34f : 1.08f; camera.allowHDR = false;
|
||||
camera.transform.position = new Vector3(1.6f, 1.55f, -4); camera.transform.LookAt(new Vector3(0, primitiveBird ? .94f : id >= 5 ? .86f : .82f, 0)); camera.targetTexture = rt;
|
||||
var light = Group("PortraitLight", root.transform).gameObject.AddComponent<Light>(); light.type = LightType.Directional;
|
||||
light.intensity = 1.15f; light.cullingMask = 1 << 30; light.transform.rotation = Quaternion.Euler(35, -35, 0);
|
||||
camera.Render();
|
||||
} RenderTexture.active = rt;
|
||||
var texture = new Texture2D(256, 256, TextureFormat.RGBA32, false);
|
||||
try { texture.ReadPixels(new Rect(0, 0, 256, 256), 0, 0); texture.Apply(); File.WriteAllBytes(Pictures + "/proto_pet_" + id + ".png", texture.EncodeToPNG()); }
|
||||
finally { UnityEngine.Object.DestroyImmediate(texture); }
|
||||
|
||||
@@ -1,398 +0,0 @@
|
||||
#if UNITY_EDITOR
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using UnityEditor;
|
||||
using UnityEditor.SceneManagement;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Rendering;
|
||||
using UnityEngine.SceneManagement;
|
||||
using YooAsset.Editor;
|
||||
|
||||
/// <summary>
|
||||
/// 把 TownSquare 8 类分件做成 Prefab,拼成可加载场景,并登记 Art 收集。
|
||||
/// 由 Unity Pipeline: unity command run_script --file AgentScripts/BuildTownSquare.cs --entry BuildTownSquare.Run
|
||||
/// </summary>
|
||||
public static class BuildTownSquare
|
||||
{
|
||||
const string SceneRoot = "Assets/Bundles/Scenes/Town/TownSquare";
|
||||
const string ScenePath = SceneRoot + "/TownSquare.unity";
|
||||
const string ScenePrefabPath = SceneRoot + "/TownSquare.prefab";
|
||||
const string XPbrShaderName = "Test/XPbr";
|
||||
const string ArtPackage = "Art";
|
||||
|
||||
static readonly string[] Parts =
|
||||
{
|
||||
"ground", "shop", "tree", "lamp", "board", "fountain", "bench", "bush"
|
||||
};
|
||||
|
||||
public static string Run()
|
||||
{
|
||||
AssetDatabase.Refresh();
|
||||
Directory.CreateDirectory(SceneRoot.Replace("Assets/", Application.dataPath + "/../Assets/").Replace('/', Path.DirectorySeparatorChar));
|
||||
|
||||
var reports = new List<string>();
|
||||
foreach (var name in Parts)
|
||||
{
|
||||
reports.Add(EnsurePartPrefab(name));
|
||||
}
|
||||
|
||||
reports.Add(AssembleScenePrefab());
|
||||
reports.Add(EnsurePlayableScene());
|
||||
reports.Add(EnsureArtCollector());
|
||||
AssetDatabase.SaveAssets();
|
||||
AssetDatabase.Refresh();
|
||||
return string.Join(" | ", reports);
|
||||
}
|
||||
|
||||
static string EnsurePartPrefab(string name)
|
||||
{
|
||||
var dir = $"{SceneRoot}/{name}";
|
||||
var fbxPath = $"{dir}/{name}.fbx";
|
||||
var matPath = $"{dir}/{name}.mat";
|
||||
var basePath = $"{dir}/{name}_base.png";
|
||||
var mixPath = $"{dir}/{name}_mix.png";
|
||||
var prefabPath = $"{dir}/{name}.prefab";
|
||||
|
||||
if (!File.Exists(fbxPath))
|
||||
return $"{name}: missing fbx";
|
||||
|
||||
ConfigureTextures(basePath, mixPath);
|
||||
ConfigureFbx(fbxPath, File.Exists(matPath), name);
|
||||
|
||||
var xpbr = Shader.Find(XPbrShaderName);
|
||||
var urpLit = Shader.Find("Universal Render Pipeline/Lit") ?? Shader.Find("Standard");
|
||||
var baseMap = AssetDatabase.LoadAssetAtPath<Texture2D>(basePath);
|
||||
var mixMap = AssetDatabase.LoadAssetAtPath<Texture2D>(mixPath);
|
||||
var useXpbr = xpbr != null && mixMap != null && baseMap != null;
|
||||
var shader = useXpbr ? xpbr : urpLit;
|
||||
|
||||
var mat = AssetDatabase.LoadAssetAtPath<Material>(matPath);
|
||||
if (mat == null)
|
||||
{
|
||||
mat = new Material(shader);
|
||||
AssetDatabase.CreateAsset(mat, matPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
mat.shader = shader;
|
||||
}
|
||||
|
||||
if (useXpbr)
|
||||
{
|
||||
if (mat.HasProperty("_MainTex")) mat.SetTexture("_MainTex", baseMap);
|
||||
if (mat.HasProperty("_MixTex")) mat.SetTexture("_MixTex", mixMap);
|
||||
if (mat.HasProperty("_BaseColor")) mat.SetColor("_BaseColor", Color.white);
|
||||
}
|
||||
else if (baseMap != null && mat.HasProperty("_BaseMap"))
|
||||
{
|
||||
mat.SetTexture("_BaseMap", baseMap);
|
||||
}
|
||||
|
||||
EditorUtility.SetDirty(mat);
|
||||
|
||||
var modelRoot = AssetDatabase.LoadAssetAtPath<GameObject>(fbxPath);
|
||||
if (modelRoot == null)
|
||||
return $"{name}: fbx not loaded";
|
||||
|
||||
var instance = (GameObject)PrefabUtility.InstantiatePrefab(modelRoot);
|
||||
instance.name = name;
|
||||
foreach (var renderer in instance.GetComponentsInChildren<Renderer>())
|
||||
renderer.sharedMaterial = mat;
|
||||
|
||||
UprightAndSit(instance, name);
|
||||
if (name == "ground")
|
||||
EnsureGroundCollider(instance);
|
||||
PrefabUtility.SaveAsPrefabAsset(instance, prefabPath);
|
||||
Object.DestroyImmediate(instance);
|
||||
return name == "ground" ? $"{name}.prefab+MeshCollider" : $"{name}.prefab";
|
||||
}
|
||||
|
||||
static void ConfigureTextures(string basePath, string mixPath)
|
||||
{
|
||||
ConfigureOneTexture(basePath, srgb: true, alpha: true);
|
||||
ConfigureOneTexture(mixPath, srgb: false, alpha: false);
|
||||
}
|
||||
|
||||
static void ConfigureOneTexture(string path, bool srgb, bool alpha)
|
||||
{
|
||||
if (!File.Exists(path)) return;
|
||||
var importer = AssetImporter.GetAtPath(path) as TextureImporter;
|
||||
if (importer == null) return;
|
||||
importer.textureType = TextureImporterType.Default;
|
||||
importer.sRGBTexture = srgb;
|
||||
importer.mipmapEnabled = true;
|
||||
importer.alphaIsTransparency = alpha;
|
||||
importer.SaveAndReimport();
|
||||
}
|
||||
|
||||
static void ConfigureFbx(string fbxPath, bool hasMat, string name)
|
||||
{
|
||||
var model = AssetImporter.GetAtPath(fbxPath) as ModelImporter;
|
||||
if (model == null) return;
|
||||
model.globalScale = 1f;
|
||||
model.useFileScale = false;
|
||||
model.bakeAxisConversion = true;
|
||||
model.materialImportMode = hasMat
|
||||
? ModelImporterMaterialImportMode.ImportViaMaterialDescription
|
||||
: ModelImporterMaterialImportMode.None;
|
||||
model.meshCompression = ModelImporterMeshCompression.Medium;
|
||||
// MeshCollider 烹饪需要可读网格;其它分件保持不可读。
|
||||
model.isReadable = name == "ground";
|
||||
model.addCollider = false;
|
||||
model.SaveAndReimport();
|
||||
}
|
||||
|
||||
static void EnsureGroundCollider(GameObject instance)
|
||||
{
|
||||
var added = 0;
|
||||
foreach (var filter in instance.GetComponentsInChildren<MeshFilter>())
|
||||
{
|
||||
if (filter.sharedMesh == null) continue;
|
||||
var col = filter.GetComponent<MeshCollider>();
|
||||
if (col == null)
|
||||
col = filter.gameObject.AddComponent<MeshCollider>();
|
||||
col.sharedMesh = filter.sharedMesh;
|
||||
col.convex = false;
|
||||
added++;
|
||||
}
|
||||
if (added == 0)
|
||||
throw new System.InvalidOperationException("ground has no mesh for MeshCollider");
|
||||
}
|
||||
|
||||
static void UprightAndSit(GameObject instance, string name)
|
||||
{
|
||||
instance.transform.localPosition = Vector3.zero;
|
||||
instance.transform.localScale = Vector3.one;
|
||||
instance.transform.localRotation = Quaternion.identity;
|
||||
|
||||
var bounds = MeshBounds(instance);
|
||||
var target = TargetSize(name, bounds);
|
||||
var current = CurrentSize(name, bounds);
|
||||
if (target > 0.01f && current > 0.01f)
|
||||
instance.transform.localScale = Vector3.one * (target / current);
|
||||
|
||||
// 游戏 FBX 原点已在底面中点,缩放后仍贴地;position 保持 0。
|
||||
}
|
||||
|
||||
static float TargetSize(string name, Bounds bounds)
|
||||
{
|
||||
switch (name)
|
||||
{
|
||||
case "ground": return 18f;
|
||||
case "shop": return 5.5f;
|
||||
case "tree": return 5.5f;
|
||||
case "lamp": return 3.6f;
|
||||
case "board": return 2.4f;
|
||||
case "fountain": return 3.2f;
|
||||
case "bench": return 1.8f;
|
||||
case "bush": return 0.9f;
|
||||
default: return 1f;
|
||||
}
|
||||
}
|
||||
|
||||
static float CurrentSize(string name, Bounds bounds)
|
||||
{
|
||||
if (name == "ground" || name == "bench")
|
||||
return Mathf.Max(bounds.size.x, bounds.size.z);
|
||||
return bounds.size.y;
|
||||
}
|
||||
|
||||
static Bounds MeshBounds(GameObject root)
|
||||
{
|
||||
Bounds bounds = default;
|
||||
var has = false;
|
||||
foreach (var r in root.GetComponentsInChildren<Renderer>())
|
||||
{
|
||||
if (!has)
|
||||
{
|
||||
bounds = r.bounds;
|
||||
has = true;
|
||||
}
|
||||
else bounds.Encapsulate(r.bounds);
|
||||
}
|
||||
return has ? bounds : new Bounds(root.transform.position, Vector3.one * 0.01f);
|
||||
}
|
||||
|
||||
static string AssembleScenePrefab()
|
||||
{
|
||||
var root = new GameObject("TownSquare");
|
||||
var ground = Place(root.transform, "ground", Vector3.zero, 0f);
|
||||
Physics.SyncTransforms();
|
||||
|
||||
// 总览 L 形;游戏网格正面是局部 -Z,用 YawFrontToward 对准广场中心。
|
||||
// 水平位置按总览,Y 取地表 MeshCollider 碰撞高度。
|
||||
var sits = new List<string>();
|
||||
void Sit(string part, Vector3 xz, float yaw, string instanceName = null)
|
||||
{
|
||||
var y = SampleGroundY(ground, xz.x, xz.z);
|
||||
sits.Add($"{instanceName ?? part}={y:F3}");
|
||||
Place(root.transform, part, new Vector3(xz.x, y, xz.z), yaw, instanceName);
|
||||
}
|
||||
|
||||
var shopLeft = new Vector3(-6.3f, 0f, 3.4f);
|
||||
var shopBack = new Vector3(0.6f, 0f, 6.7f);
|
||||
Sit("shop", shopLeft, YawFrontToward(shopLeft, Vector3.zero), "shop_left");
|
||||
Sit("shop", shopBack, YawFrontToward(shopBack, Vector3.zero), "shop_back");
|
||||
Sit("tree", new Vector3(-8.0f, 0f, 5.2f), 15f, "tree_left");
|
||||
Sit("tree", new Vector3(7.5f, 0f, 6.2f), -15f, "tree_right");
|
||||
var boardPos = new Vector3(5.0f, 0f, 6.5f);
|
||||
Sit("board", boardPos, YawFrontToward(boardPos, Vector3.zero));
|
||||
Sit("fountain", new Vector3(6.1f, 0f, 0.6f), 0f);
|
||||
Sit("lamp", new Vector3(-6.4f, 0f, -2.8f), 0f, "lamp_front_left");
|
||||
Sit("lamp", new Vector3(2.7f, 0f, 6.6f), 10f, "lamp_back");
|
||||
Sit("lamp", new Vector3(8.1f, 0f, 1.3f), -15f, "lamp_right");
|
||||
var benchLeft = new Vector3(-5.2f, 0f, -5.2f);
|
||||
var benchRight = new Vector3(5.6f, 0f, -4.2f);
|
||||
Sit("bench", benchLeft, YawFrontToward(benchLeft, Vector3.zero), "bench_left");
|
||||
Sit("bench", benchRight, YawFrontToward(benchRight, Vector3.zero), "bench_right");
|
||||
Sit("bush", new Vector3(-6.2f, 0f, -5.8f), 0f, "bush_bench_l1");
|
||||
Sit("bush", new Vector3(-4.2f, 0f, -6.0f), 40f, "bush_bench_l2");
|
||||
Sit("bush", new Vector3(6.6f, 0f, -4.8f), 10f, "bush_bench_r");
|
||||
Sit("bush", new Vector3(7.4f, 0f, 2.0f), 25f, "bush_fountain_1");
|
||||
Sit("bush", new Vector3(4.8f, 0f, 1.4f), -10f, "bush_fountain_2");
|
||||
Sit("bush", new Vector3(7.0f, 0f, 0.2f), 20f, "bush_fountain_3");
|
||||
Sit("bush", new Vector3(6.6f, 0f, 6.8f), 0f, "bush_board");
|
||||
|
||||
PrefabUtility.SaveAsPrefabAsset(root, ScenePrefabPath);
|
||||
Object.DestroyImmediate(root);
|
||||
return "TownSquare.prefab sit=[" + string.Join(",", sits) + "]";
|
||||
}
|
||||
|
||||
static GameObject Place(Transform parent, string part, Vector3 pos, float yaw, string instanceName = null)
|
||||
{
|
||||
var prefab = AssetDatabase.LoadAssetAtPath<GameObject>($"{SceneRoot}/{part}/{part}.prefab");
|
||||
if (prefab == null)
|
||||
throw new System.InvalidOperationException($"missing part prefab {part}");
|
||||
var go = (GameObject)PrefabUtility.InstantiatePrefab(prefab, parent);
|
||||
go.name = instanceName ?? part;
|
||||
go.transform.position = pos;
|
||||
go.transform.rotation = Quaternion.Euler(0f, yaw, 0f) * go.transform.rotation;
|
||||
return go;
|
||||
}
|
||||
|
||||
static float SampleGroundY(GameObject ground, float x, float z)
|
||||
{
|
||||
if (ground == null)
|
||||
return 0f;
|
||||
Physics.SyncTransforms();
|
||||
var ray = new Ray(new Vector3(x, 80f, z), Vector3.down);
|
||||
var bestDist = float.MaxValue;
|
||||
var y = 0f;
|
||||
var hitAny = false;
|
||||
foreach (var col in ground.GetComponentsInChildren<MeshCollider>())
|
||||
{
|
||||
if (col == null || !col.enabled) continue;
|
||||
if (!col.Raycast(ray, out var hit, 160f)) continue;
|
||||
if (hit.distance >= bestDist) continue;
|
||||
bestDist = hit.distance;
|
||||
y = hit.point.y;
|
||||
hitAny = true;
|
||||
}
|
||||
if (!hitAny)
|
||||
Debug.LogWarning($"[BuildTownSquare] ground miss at ({x:F2}, {z:F2}), y=0");
|
||||
return y;
|
||||
}
|
||||
|
||||
static float YawFrontToward(Vector3 from, Vector3 to)
|
||||
{
|
||||
// 导出网格正面是局部 -Z(不是 Unity 默认 +Z)。让 +Z 背对目标,则 -Z 朝向目标。
|
||||
var away = from - to;
|
||||
away.y = 0f;
|
||||
if (away.sqrMagnitude < 0.0001f) return 0f;
|
||||
return Quaternion.LookRotation(away).eulerAngles.y;
|
||||
}
|
||||
|
||||
static string EnsurePlayableScene()
|
||||
{
|
||||
var scene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);
|
||||
|
||||
var lightGo = new GameObject("Directional Light");
|
||||
var light = lightGo.AddComponent<Light>();
|
||||
light.type = LightType.Directional;
|
||||
light.color = new Color(1f, 0.96f, 0.88f);
|
||||
light.intensity = 1.15f;
|
||||
light.shadows = LightShadows.Soft;
|
||||
light.shadowStrength = 1f;
|
||||
lightGo.transform.rotation = Quaternion.Euler(50f, -30f, 0f);
|
||||
|
||||
var camGo = new GameObject("Main Camera");
|
||||
camGo.tag = "MainCamera";
|
||||
var cam = camGo.AddComponent<Camera>();
|
||||
cam.clearFlags = CameraClearFlags.SolidColor;
|
||||
cam.backgroundColor = new Color(0.78f, 0.88f, 0.94f);
|
||||
cam.fieldOfView = 40f;
|
||||
cam.nearClipPlane = 0.1f;
|
||||
cam.farClipPlane = 80f;
|
||||
camGo.AddComponent<AudioListener>();
|
||||
camGo.transform.position = new Vector3(0f, 12f, -22f);
|
||||
camGo.transform.rotation = Quaternion.Euler(28f, 0f, 0f);
|
||||
|
||||
var plaza = AssetDatabase.LoadAssetAtPath<GameObject>(ScenePrefabPath);
|
||||
PrefabUtility.InstantiatePrefab(plaza);
|
||||
|
||||
RenderSettings.ambientMode = AmbientMode.Trilight;
|
||||
RenderSettings.ambientSkyColor = new Color(0.32f, 0.40f, 0.50f);
|
||||
RenderSettings.ambientEquatorColor = new Color(0.24f, 0.26f, 0.24f);
|
||||
RenderSettings.ambientGroundColor = new Color(0.16f, 0.15f, 0.13f);
|
||||
|
||||
EditorSceneManager.SaveScene(scene, ScenePath);
|
||||
return ScenePath;
|
||||
}
|
||||
|
||||
static string EnsureArtCollector()
|
||||
{
|
||||
var setting = BundleCollectorSettingData.Setting;
|
||||
setting.ShowPackageView = true;
|
||||
var package = setting.Packages.Find(p => p.PackageName == ArtPackage);
|
||||
if (package == null)
|
||||
package = BundleCollectorSettingData.CreatePackage(ArtPackage);
|
||||
|
||||
package.PackageDesc = "角色/场景美术";
|
||||
package.EnableAddressable = true;
|
||||
package.SupportExtensionless = true;
|
||||
package.AutoCollectShaders = true;
|
||||
package.IgnoreRuleName = nameof(NormalIgnoreRule);
|
||||
BundleCollectorSettingData.ModifyPackage(package);
|
||||
|
||||
var group = package.Groups.Find(g => g.GroupName == "Scenes");
|
||||
if (group == null)
|
||||
group = BundleCollectorSettingData.CreateGroup(package, "Scenes");
|
||||
group.ActiveRuleName = nameof(EnableGroup);
|
||||
group.AssetTags = "art";
|
||||
BundleCollectorSettingData.ModifyGroup(package, group);
|
||||
|
||||
const string broadPath = "Assets/Bundles/Scenes";
|
||||
const string collectPath = "Assets/Bundles/Scenes/Town/TownSquare/TownSquare.prefab";
|
||||
var broad = group.Collectors.Find(c => c.CollectPath == broadPath);
|
||||
if (broad != null)
|
||||
BundleCollectorSettingData.RemoveCollector(group, broad);
|
||||
var collector = group.Collectors.Find(c => c.CollectPath == collectPath);
|
||||
if (collector == null)
|
||||
{
|
||||
collector = new BundleCollector
|
||||
{
|
||||
CollectPath = collectPath,
|
||||
CollectorGUID = AssetDatabase.AssetPathToGUID(collectPath),
|
||||
CollectorType = ECollectorType.MainAssetCollector,
|
||||
AddressRuleName = nameof(AddressByFileName),
|
||||
PackRuleName = nameof(PackSeparately),
|
||||
FilterRuleName = nameof(CollectPrefab),
|
||||
AssetTags = "art",
|
||||
};
|
||||
BundleCollectorSettingData.CreateCollector(group, collector);
|
||||
}
|
||||
else
|
||||
{
|
||||
collector.CollectorGUID = AssetDatabase.AssetPathToGUID(collectPath);
|
||||
collector.AddressRuleName = nameof(AddressByFileName);
|
||||
collector.PackRuleName = nameof(PackSeparately);
|
||||
collector.FilterRuleName = nameof(CollectPrefab);
|
||||
BundleCollectorSettingData.ModifyCollector(group, collector);
|
||||
}
|
||||
|
||||
package.CheckConfigError();
|
||||
BundleCollectorSettingData.SaveFile();
|
||||
return $"art collect={collectPath}";
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,168 @@
|
||||
#if UNITY_EDITOR
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
using UnityEditor;
|
||||
using UnityEditor.Animations;
|
||||
using UnityEditor.SceneManagement;
|
||||
using UnityEngine;
|
||||
|
||||
public static class BuildXBotController
|
||||
{
|
||||
const string Common = "Assets/Bundles/Characters/Human/Common";
|
||||
const string Model = Common + "/XBot.fbx";
|
||||
const string Output = Common + "/XBot.controller";
|
||||
static readonly string[] States = { "Idle", "Walk", "Attack01", "Attack02" };
|
||||
static readonly string[] Files = { "X Bot@BreathingIdle.fbx", "X Bot@Walking.fbx", "X Bot@atk01.fbx", "X Bot@Atk02.fbx" };
|
||||
|
||||
static Avatar AvatarAt(string path)
|
||||
{
|
||||
var avatar = AssetDatabase.LoadAllAssetsAtPath(path).OfType<Avatar>().SingleOrDefault();
|
||||
if (!avatar || !avatar.isValid || !avatar.isHuman) throw new Exception("Invalid Humanoid Avatar: " + path);
|
||||
return avatar;
|
||||
}
|
||||
static Dictionary<string, string> Bones(GameObject model)
|
||||
{
|
||||
return model.GetComponentsInChildren<Transform>(true).Where(t => t.name.StartsWith("mixamorig:"))
|
||||
.ToDictionary(t => t.name, t => t.parent && t.parent.name.StartsWith("mixamorig:") ? t.parent.name : "");
|
||||
}
|
||||
static void Transition(AnimatorState from, AnimatorState to, string parameter, AnimatorConditionMode mode, float value, bool exit = false)
|
||||
{
|
||||
var t = from.AddTransition(to);
|
||||
t.hasExitTime = exit; t.exitTime = .95f; t.hasFixedDuration = true; t.duration = .15f;
|
||||
t.AddCondition(mode, value, parameter);
|
||||
}
|
||||
public static string Build()
|
||||
{
|
||||
if (EditorApplication.isPlayingOrWillChangePlaymode) throw new Exception("Edit Mode required.");
|
||||
if (File.Exists(Output)) throw new Exception("Controller already exists; inspect it before replacing: " + Output);
|
||||
foreach (string f in Files) if (!File.Exists(Common + "/AnimX/" + f)) throw new FileNotFoundException(f);
|
||||
AssetDatabase.ImportAsset(Model, ImportAssetOptions.ForceSynchronousImport);
|
||||
var modelImporter = (ModelImporter)AssetImporter.GetAtPath(Model);
|
||||
modelImporter.animationType = ModelImporterAnimationType.Human;
|
||||
modelImporter.avatarSetup = ModelImporterAvatarSetup.CreateFromThisModel;
|
||||
modelImporter.SaveAndReimport();
|
||||
var avatar = AvatarAt(Model);
|
||||
var bones = Bones(AssetDatabase.LoadAssetAtPath<GameObject>(Model));
|
||||
if (bones.Count != 65) throw new Exception("Expected XBot's 65 bones, got " + bones.Count);
|
||||
var clips = new AnimationClip[Files.Length];
|
||||
for (int i = 0; i < Files.Length; i++)
|
||||
{
|
||||
string path = Common + "/AnimX/" + Files[i];
|
||||
AssetDatabase.ImportAsset(path, ImportAssetOptions.ForceSynchronousImport);
|
||||
var source = AssetDatabase.LoadAssetAtPath<GameObject>(path);
|
||||
var sourceBones = Bones(source);
|
||||
if (sourceBones.Count != 65 || sourceBones.Any(b => !bones.ContainsKey(b.Key) || bones[b.Key] != b.Value))
|
||||
throw new Exception("Source skeleton differs from XBot: " + path);
|
||||
if (source.GetComponentsInChildren<Renderer>(true).Length != 0) throw new Exception("Expected animation without mesh: " + path);
|
||||
var importer = (ModelImporter)AssetImporter.GetAtPath(path);
|
||||
importer.animationType = ModelImporterAnimationType.Human;
|
||||
importer.avatarSetup = ModelImporterAvatarSetup.CopyFromOther;
|
||||
importer.sourceAvatar = avatar;
|
||||
importer.importAnimation = true;
|
||||
importer.importCameras = false; importer.importLights = false;
|
||||
var settings = importer.clipAnimations;
|
||||
if (settings.Length == 0) settings = importer.defaultClipAnimations;
|
||||
if (settings.Length != 1) throw new Exception("Expected one clip: " + path);
|
||||
settings[0].name = States[i];
|
||||
settings[0].loopTime = i < 2; settings[0].loopPose = i < 2;
|
||||
settings[0].lockRootRotation = true; settings[0].lockRootHeightY = true; settings[0].lockRootPositionXZ = true;
|
||||
settings[0].keepOriginalOrientation = true; settings[0].keepOriginalPositionY = true; settings[0].keepOriginalPositionXZ = true;
|
||||
importer.clipAnimations = settings; importer.SaveAndReimport();
|
||||
clips[i] = AssetDatabase.LoadAllAssetsAtPath(path).OfType<AnimationClip>().Single(c => !c.name.StartsWith("__preview__"));
|
||||
if (!clips[i].humanMotion || clips[i].length <= 0) throw new Exception("Invalid Humanoid clip: " + path);
|
||||
}
|
||||
var controller = AnimatorController.CreateAnimatorControllerAtPath(Output);
|
||||
controller.AddParameter("Motion", AnimatorControllerParameterType.Int);
|
||||
controller.AddParameter("Attack01", AnimatorControllerParameterType.Trigger);
|
||||
controller.AddParameter("Attack02", AnimatorControllerParameterType.Trigger);
|
||||
var sm = controller.layers[0].stateMachine;
|
||||
sm.entryPosition = new Vector3(0, 0); sm.anyStatePosition = new Vector3(0, 220);
|
||||
var states = new AnimatorState[4];
|
||||
for (int i = 0; i < states.Length; i++)
|
||||
{
|
||||
states[i] = sm.AddState(States[i], new Vector3(260 + (i % 2) * 280, (i / 2) * 220));
|
||||
states[i].motion = clips[i]; states[i].iKOnFeet = i < 2; states[i].writeDefaultValues = false;
|
||||
}
|
||||
sm.defaultState = states[0];
|
||||
Transition(states[0], states[1], "Motion", AnimatorConditionMode.Equals, 1);
|
||||
Transition(states[1], states[0], "Motion", AnimatorConditionMode.NotEqual, 1);
|
||||
for (int i = 2; i < 4; i++)
|
||||
{
|
||||
var attack = sm.AddAnyStateTransition(states[i]);
|
||||
attack.hasExitTime = false; attack.hasFixedDuration = true; attack.duration = .1f; attack.canTransitionToSelf = false;
|
||||
attack.AddCondition(AnimatorConditionMode.If, 0, States[i]);
|
||||
Transition(states[i], states[1], "Motion", AnimatorConditionMode.Equals, 1, true);
|
||||
Transition(states[i], states[0], "Motion", AnimatorConditionMode.NotEqual, 1, true);
|
||||
}
|
||||
EditorUtility.SetDirty(controller); AssetDatabase.SaveAssetIfDirty(controller);
|
||||
return Verify();
|
||||
}
|
||||
static void Advance(Animator animator, float duration)
|
||||
{
|
||||
int count = Mathf.CeilToInt(duration * 60);
|
||||
for (int i = 0; i < count; i++) animator.Update(1f / 60);
|
||||
}
|
||||
static void Expect(Animator animator, string state)
|
||||
{
|
||||
if (!animator.GetCurrentAnimatorStateInfo(0).IsName("Base Layer." + state) || animator.IsInTransition(0))
|
||||
throw new Exception("Expected settled state " + state);
|
||||
}
|
||||
public static string Verify()
|
||||
{
|
||||
var controller = AssetDatabase.LoadAssetAtPath<AnimatorController>(Output);
|
||||
if (!controller || controller.layers.Length != 1) throw new Exception("Missing controller/layer");
|
||||
var sm = controller.layers[0].stateMachine;
|
||||
if (sm.states.Length != 4 || sm.defaultState.name != "Idle") throw new Exception("Wrong states/default");
|
||||
var scene = EditorSceneManager.NewPreviewScene();
|
||||
var samples = new List<object>(); var checks = new List<string>();
|
||||
try
|
||||
{
|
||||
var go = (GameObject)PrefabUtility.InstantiatePrefab(AssetDatabase.LoadAssetAtPath<GameObject>(Model), scene);
|
||||
var animator = go.GetComponent<Animator>();
|
||||
animator.avatar = AvatarAt(Model); animator.runtimeAnimatorController = controller;
|
||||
animator.applyRootMotion = false; animator.cullingMode = AnimatorCullingMode.AlwaysAnimate;
|
||||
animator.Rebind(); animator.Update(0);
|
||||
foreach (var state in sm.states.Select(s => s.state))
|
||||
{
|
||||
var clip = (AnimationClip)state.motion;
|
||||
Vector3 first = Vector3.zero; float movement = 0;
|
||||
for (int i = 0; i < 25; i++)
|
||||
{
|
||||
animator.Play("Base Layer." + state.name, 0, i / 25f); animator.Update(0);
|
||||
var hip = animator.GetBoneTransform(HumanBodyBones.Hips);
|
||||
var hand = animator.GetBoneTransform(HumanBodyBones.LeftHand).position - hip.position;
|
||||
var foot = animator.GetBoneTransform(HumanBodyBones.LeftFoot).position - hip.position;
|
||||
if (i == 0) first = hand + foot;
|
||||
movement = Mathf.Max(movement, Vector3.Distance(first, hand + foot));
|
||||
if (go.GetComponentsInChildren<Transform>().Any(t => float.IsNaN(t.position.sqrMagnitude) || float.IsInfinity(t.position.sqrMagnitude)))
|
||||
throw new Exception("Invalid bone pose in " + state.name);
|
||||
}
|
||||
if (movement < .00001f) throw new Exception("No sampled movement in " + state.name);
|
||||
samples.Add(new { state = state.name, source = AssetDatabase.GetAssetPath(clip), clip.length, clip.frameRate, clip.humanMotion, clip.isLooping, samples = 25, sampledMovement = movement });
|
||||
}
|
||||
animator.SetInteger("Motion", 0); animator.Play("Base Layer.Idle", 0, 0); animator.Update(0);
|
||||
animator.SetInteger("Motion", 1); Advance(animator, .4f); Expect(animator, "Walk"); checks.Add("Idle -> Walk");
|
||||
animator.SetInteger("Motion", 0); Advance(animator, .4f); Expect(animator, "Idle"); checks.Add("Walk -> Idle");
|
||||
foreach (int motion in new[] { 0, 1 }) foreach (string attack in States.Skip(2))
|
||||
{
|
||||
animator.SetInteger("Motion", motion); animator.Play("Base Layer." + (motion == 1 ? "Walk" : "Idle"), 0, 0); animator.Update(0);
|
||||
animator.SetTrigger(attack); Advance(animator, .2f); Expect(animator, attack);
|
||||
float length = ((AnimationClip)sm.states.Single(s => s.state.name == attack).state.motion).length;
|
||||
Advance(animator, length + .4f); Expect(animator, motion == 1 ? "Walk" : "Idle");
|
||||
checks.Add((motion == 1 ? "Walk" : "Idle") + " -> " + attack + " -> " + (motion == 1 ? "Walk" : "Idle"));
|
||||
}
|
||||
var report = new { controller = Output, model = Model, bones = Bones(go).Count, avatarValid = animator.avatar.isValid, avatarHuman = animator.avatar.isHuman,
|
||||
defaultState = "Idle", parameters = controller.parameters.Select(p => new { p.name, type = p.type.ToString() }), states = samples, transitions = checks,
|
||||
rootMotion = "Baked rotation/Y/XZ; consumer Animator.applyRootMotion=false", verification = "Isolated Editor Animator; no active scene edits" };
|
||||
string json = JsonConvert.SerializeObject(report, Formatting.Indented);
|
||||
string folder = Path.Combine(Directory.GetParent(Application.dataPath).Parent.FullName, "Tools/output/common_bot_controller");
|
||||
Directory.CreateDirectory(folder); File.WriteAllText(Path.Combine(folder, "validation.json"), json);
|
||||
return json;
|
||||
}
|
||||
finally { EditorSceneManager.ClosePreviewScene(scene); }
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,168 @@
|
||||
#if UNITY_EDITOR
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
using UnityEditor;
|
||||
using UnityEditor.Animations;
|
||||
using UnityEditor.SceneManagement;
|
||||
using UnityEngine;
|
||||
|
||||
public static class BuildYBotController
|
||||
{
|
||||
const string Common = "Assets/Bundles/Characters/Human/Common";
|
||||
const string Model = Common + "/YBot.fbx";
|
||||
const string Output = Common + "/YBot.controller";
|
||||
static readonly string[] States = { "Idle", "Walk", "Attack01", "Attack02" };
|
||||
static readonly string[] Files = { "Y Bot@BreathingIdle.fbx", "Y Bot@Walking.fbx", "Y Bot@Atk01.fbx", "Y Bot@atk02.fbx" };
|
||||
|
||||
static Avatar AvatarAt(string path)
|
||||
{
|
||||
var avatar = AssetDatabase.LoadAllAssetsAtPath(path).OfType<Avatar>().SingleOrDefault();
|
||||
if (!avatar || !avatar.isValid || !avatar.isHuman) throw new Exception("Invalid Humanoid Avatar: " + path);
|
||||
return avatar;
|
||||
}
|
||||
static Dictionary<string, string> Bones(GameObject model)
|
||||
{
|
||||
return model.GetComponentsInChildren<Transform>(true).Where(t => t.name.StartsWith("mixamorig:"))
|
||||
.ToDictionary(t => t.name, t => t.parent && t.parent.name.StartsWith("mixamorig:") ? t.parent.name : "");
|
||||
}
|
||||
static void Transition(AnimatorState from, AnimatorState to, string parameter, AnimatorConditionMode mode, float value, bool exit = false)
|
||||
{
|
||||
var t = from.AddTransition(to);
|
||||
t.hasExitTime = exit; t.exitTime = .95f; t.hasFixedDuration = true; t.duration = .15f;
|
||||
t.AddCondition(mode, value, parameter);
|
||||
}
|
||||
public static string Build()
|
||||
{
|
||||
if (EditorApplication.isPlayingOrWillChangePlaymode) throw new Exception("Edit Mode required.");
|
||||
if (File.Exists(Output)) throw new Exception("Controller already exists; inspect it before replacing: " + Output);
|
||||
foreach (string f in Files) if (!File.Exists(Common + "/AnimY/" + f)) throw new FileNotFoundException(f);
|
||||
AssetDatabase.ImportAsset(Model, ImportAssetOptions.ForceSynchronousImport);
|
||||
var modelImporter = (ModelImporter)AssetImporter.GetAtPath(Model);
|
||||
modelImporter.animationType = ModelImporterAnimationType.Human;
|
||||
modelImporter.avatarSetup = ModelImporterAvatarSetup.CreateFromThisModel;
|
||||
modelImporter.SaveAndReimport();
|
||||
var avatar = AvatarAt(Model);
|
||||
var bones = Bones(AssetDatabase.LoadAssetAtPath<GameObject>(Model));
|
||||
if (bones.Count != 65) throw new Exception("Expected YBot's 65 bones, got " + bones.Count);
|
||||
var clips = new AnimationClip[Files.Length];
|
||||
for (int i = 0; i < Files.Length; i++)
|
||||
{
|
||||
string path = Common + "/AnimY/" + Files[i];
|
||||
AssetDatabase.ImportAsset(path, ImportAssetOptions.ForceSynchronousImport);
|
||||
var source = AssetDatabase.LoadAssetAtPath<GameObject>(path);
|
||||
var sourceBones = Bones(source);
|
||||
if (sourceBones.Count != 65 || sourceBones.Any(b => !bones.ContainsKey(b.Key) || bones[b.Key] != b.Value))
|
||||
throw new Exception("Source skeleton differs from YBot: " + path);
|
||||
if (source.GetComponentsInChildren<Renderer>(true).Length != 0) throw new Exception("Expected animation without mesh: " + path);
|
||||
var importer = (ModelImporter)AssetImporter.GetAtPath(path);
|
||||
importer.animationType = ModelImporterAnimationType.Human;
|
||||
importer.avatarSetup = ModelImporterAvatarSetup.CopyFromOther;
|
||||
importer.sourceAvatar = avatar;
|
||||
importer.importAnimation = true;
|
||||
importer.importCameras = false; importer.importLights = false;
|
||||
var settings = importer.clipAnimations;
|
||||
if (settings.Length == 0) settings = importer.defaultClipAnimations;
|
||||
if (settings.Length != 1) throw new Exception("Expected one clip: " + path);
|
||||
settings[0].name = States[i];
|
||||
settings[0].loopTime = i < 2; settings[0].loopPose = i < 2;
|
||||
settings[0].lockRootRotation = true; settings[0].lockRootHeightY = true; settings[0].lockRootPositionXZ = true;
|
||||
settings[0].keepOriginalOrientation = true; settings[0].keepOriginalPositionY = true; settings[0].keepOriginalPositionXZ = true;
|
||||
importer.clipAnimations = settings; importer.SaveAndReimport();
|
||||
clips[i] = AssetDatabase.LoadAllAssetsAtPath(path).OfType<AnimationClip>().Single(c => !c.name.StartsWith("__preview__"));
|
||||
if (!clips[i].humanMotion || clips[i].length <= 0) throw new Exception("Invalid Humanoid clip: " + path);
|
||||
}
|
||||
var controller = AnimatorController.CreateAnimatorControllerAtPath(Output);
|
||||
controller.AddParameter("Motion", AnimatorControllerParameterType.Int);
|
||||
controller.AddParameter("Attack01", AnimatorControllerParameterType.Trigger);
|
||||
controller.AddParameter("Attack02", AnimatorControllerParameterType.Trigger);
|
||||
var sm = controller.layers[0].stateMachine;
|
||||
sm.entryPosition = new Vector3(0, 0); sm.anyStatePosition = new Vector3(0, 220);
|
||||
var states = new AnimatorState[4];
|
||||
for (int i = 0; i < states.Length; i++)
|
||||
{
|
||||
states[i] = sm.AddState(States[i], new Vector3(260 + (i % 2) * 280, (i / 2) * 220));
|
||||
states[i].motion = clips[i]; states[i].iKOnFeet = i < 2; states[i].writeDefaultValues = false;
|
||||
}
|
||||
sm.defaultState = states[0];
|
||||
Transition(states[0], states[1], "Motion", AnimatorConditionMode.Equals, 1);
|
||||
Transition(states[1], states[0], "Motion", AnimatorConditionMode.NotEqual, 1);
|
||||
for (int i = 2; i < 4; i++)
|
||||
{
|
||||
var attack = sm.AddAnyStateTransition(states[i]);
|
||||
attack.hasExitTime = false; attack.hasFixedDuration = true; attack.duration = .1f; attack.canTransitionToSelf = false;
|
||||
attack.AddCondition(AnimatorConditionMode.If, 0, States[i]);
|
||||
Transition(states[i], states[1], "Motion", AnimatorConditionMode.Equals, 1, true);
|
||||
Transition(states[i], states[0], "Motion", AnimatorConditionMode.NotEqual, 1, true);
|
||||
}
|
||||
EditorUtility.SetDirty(controller); AssetDatabase.SaveAssetIfDirty(controller);
|
||||
return Verify();
|
||||
}
|
||||
static void Advance(Animator animator, float duration)
|
||||
{
|
||||
int count = Mathf.CeilToInt(duration * 60);
|
||||
for (int i = 0; i < count; i++) animator.Update(1f / 60);
|
||||
}
|
||||
static void Expect(Animator animator, string state)
|
||||
{
|
||||
if (!animator.GetCurrentAnimatorStateInfo(0).IsName("Base Layer." + state) || animator.IsInTransition(0))
|
||||
throw new Exception("Expected settled state " + state);
|
||||
}
|
||||
public static string Verify()
|
||||
{
|
||||
var controller = AssetDatabase.LoadAssetAtPath<AnimatorController>(Output);
|
||||
if (!controller || controller.layers.Length != 1) throw new Exception("Missing controller/layer");
|
||||
var sm = controller.layers[0].stateMachine;
|
||||
if (sm.states.Length != 4 || sm.defaultState.name != "Idle") throw new Exception("Wrong states/default");
|
||||
var scene = EditorSceneManager.NewPreviewScene();
|
||||
var samples = new List<object>(); var checks = new List<string>();
|
||||
try
|
||||
{
|
||||
var go = (GameObject)PrefabUtility.InstantiatePrefab(AssetDatabase.LoadAssetAtPath<GameObject>(Model), scene);
|
||||
var animator = go.GetComponent<Animator>();
|
||||
animator.avatar = AvatarAt(Model); animator.runtimeAnimatorController = controller;
|
||||
animator.applyRootMotion = false; animator.cullingMode = AnimatorCullingMode.AlwaysAnimate;
|
||||
animator.Rebind(); animator.Update(0);
|
||||
foreach (var state in sm.states.Select(s => s.state))
|
||||
{
|
||||
var clip = (AnimationClip)state.motion;
|
||||
Vector3 first = Vector3.zero; float movement = 0;
|
||||
for (int i = 0; i < 25; i++)
|
||||
{
|
||||
animator.Play("Base Layer." + state.name, 0, i / 25f); animator.Update(0);
|
||||
var hip = animator.GetBoneTransform(HumanBodyBones.Hips);
|
||||
var hand = animator.GetBoneTransform(HumanBodyBones.LeftHand).position - hip.position;
|
||||
var foot = animator.GetBoneTransform(HumanBodyBones.LeftFoot).position - hip.position;
|
||||
if (i == 0) first = hand + foot;
|
||||
movement = Mathf.Max(movement, Vector3.Distance(first, hand + foot));
|
||||
if (go.GetComponentsInChildren<Transform>().Any(t => float.IsNaN(t.position.sqrMagnitude) || float.IsInfinity(t.position.sqrMagnitude)))
|
||||
throw new Exception("Invalid bone pose in " + state.name);
|
||||
}
|
||||
if (movement < .00001f) throw new Exception("No sampled movement in " + state.name);
|
||||
samples.Add(new { state = state.name, source = AssetDatabase.GetAssetPath(clip), clip.length, clip.frameRate, clip.humanMotion, clip.isLooping, samples = 25, sampledMovement = movement });
|
||||
}
|
||||
animator.SetInteger("Motion", 0); animator.Play("Base Layer.Idle", 0, 0); animator.Update(0);
|
||||
animator.SetInteger("Motion", 1); Advance(animator, .4f); Expect(animator, "Walk"); checks.Add("Idle -> Walk");
|
||||
animator.SetInteger("Motion", 0); Advance(animator, .4f); Expect(animator, "Idle"); checks.Add("Walk -> Idle");
|
||||
foreach (int motion in new[] { 0, 1 }) foreach (string attack in States.Skip(2))
|
||||
{
|
||||
animator.SetInteger("Motion", motion); animator.Play("Base Layer." + (motion == 1 ? "Walk" : "Idle"), 0, 0); animator.Update(0);
|
||||
animator.SetTrigger(attack); Advance(animator, .2f); Expect(animator, attack);
|
||||
float length = ((AnimationClip)sm.states.Single(s => s.state.name == attack).state.motion).length;
|
||||
Advance(animator, length + .4f); Expect(animator, motion == 1 ? "Walk" : "Idle");
|
||||
checks.Add((motion == 1 ? "Walk" : "Idle") + " -> " + attack + " -> " + (motion == 1 ? "Walk" : "Idle"));
|
||||
}
|
||||
var report = new { controller = Output, model = Model, bones = Bones(go).Count, avatarValid = animator.avatar.isValid, avatarHuman = animator.avatar.isHuman,
|
||||
defaultState = "Idle", parameters = controller.parameters.Select(p => new { p.name, type = p.type.ToString() }), states = samples, transitions = checks,
|
||||
rootMotion = "Baked rotation/Y/XZ; consumer Animator.applyRootMotion=false", verification = "Isolated Editor Animator; no active scene edits" };
|
||||
string json = JsonConvert.SerializeObject(report, Formatting.Indented);
|
||||
string folder = Path.Combine(Directory.GetParent(Application.dataPath).Parent.FullName, "Tools/output/common_ybot_controller");
|
||||
Directory.CreateDirectory(folder); File.WriteAllText(Path.Combine(folder, "validation.json"), json);
|
||||
return json;
|
||||
}
|
||||
finally { EditorSceneManager.ClosePreviewScene(scene); }
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,100 @@
|
||||
#if UNITY_EDITOR
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
using UnityEditor;
|
||||
using UnityEditor.Animations;
|
||||
using UnityEditor.SceneManagement;
|
||||
using UnityEngine;
|
||||
|
||||
public static class DiagnoseBotWalkTranslation
|
||||
{
|
||||
static float[] V(Vector3 v) => new[] { v.x, v.y, v.z };
|
||||
public static string Run()
|
||||
{
|
||||
const string common = "Assets/Bundles/Characters/Human/Common";
|
||||
var reports = new List<object>();
|
||||
foreach (string name in new[] { "XBot", "YBot", "XBot25", "XBot41" })
|
||||
{
|
||||
var scene = EditorSceneManager.NewPreviewScene();
|
||||
try
|
||||
{
|
||||
var go = (GameObject)PrefabUtility.InstantiatePrefab(AssetDatabase.LoadAssetAtPath<GameObject>(common + "/Prefabs/" + name + ".prefab"), scene);
|
||||
var a = go.GetComponent<Animator>(); a.applyRootMotion = false; a.cullingMode = AnimatorCullingMode.AlwaysAnimate;
|
||||
a.Rebind(); a.SetInteger("Motion",1); a.Update(0);
|
||||
var clip = (AnimationClip)((AnimatorController)a.runtimeAnimatorController).layers[0].stateMachine.states.Single(s=>s.state.name=="Walk").state.motion;
|
||||
var importer = (ModelImporter)AssetImporter.GetAtPath(AssetDatabase.GetAssetPath(clip));
|
||||
var settings = importer.clipAnimations.Single();
|
||||
var points = new List<Vector3>(); var roots = new List<Vector3>(); var samples = new List<object>();
|
||||
for (int i=0;i<=60;i++)
|
||||
{
|
||||
float phase = i/60f;
|
||||
a.Play("Base Layer.Walk",0,Mathf.Min(.99999f,phase)); a.Update(0);
|
||||
Vector3 hip = a.GetBoneTransform(HumanBodyBones.Hips).position-go.transform.position;
|
||||
points.Add(hip); roots.Add(go.transform.position);
|
||||
if (i%15==0) samples.Add(new { phase, hipsFromRoot=V(hip), root=V(go.transform.position) });
|
||||
}
|
||||
var min = points.Aggregate(Vector3.positiveInfinity,Vector3.Min); var max = points.Aggregate(Vector3.negativeInfinity,Vector3.Max);
|
||||
var rootMin=roots.Aggregate(Vector3.positiveInfinity,Vector3.Min);var rootMax=roots.Aggregate(Vector3.negativeInfinity,Vector3.Max);
|
||||
reports.Add(new { name, clip=AssetDatabase.GetAssetPath(clip), clip.length, applyRootMotion=a.applyRootMotion,
|
||||
settings.lockRootPositionXZ, settings.keepOriginalPositionXZ, settings.loopTime,
|
||||
hipRangeWorldMeters=V(max-min), hipNetTravelWorldMeters=V(points.Last()-points.First()), rootRangeWorldMeters=V(rootMax-rootMin), samples });
|
||||
}
|
||||
finally { EditorSceneManager.ClosePreviewScene(scene); }
|
||||
}
|
||||
string json=JsonConvert.SerializeObject(reports,Formatting.Indented);
|
||||
string folder=Path.Combine(Directory.GetParent(Application.dataPath).Parent.FullName,"Tools/output/bot_animation_lab");
|
||||
Directory.CreateDirectory(folder);File.WriteAllText(Path.Combine(folder,"walk_translation_diagnostic.json"),json);
|
||||
return json;
|
||||
}
|
||||
public static string CompareExtraction()
|
||||
{
|
||||
const string common="Assets/Bundles/Characters/Human/Common";
|
||||
const string folder="Assets/Review/BotAnimationLab/TranslationProbe";
|
||||
if (AssetDatabase.IsValidFolder(folder)) throw new Exception("Probe folder already exists");
|
||||
AssetDatabase.CreateFolder("Assets/Review/BotAnimationLab","TranslationProbe");
|
||||
var reports=new List<object>();
|
||||
try
|
||||
{
|
||||
foreach(string name in new[]{"XBot","YBot"})
|
||||
{
|
||||
string source=common+"/Anim"+(name=="XBot"?"X":"Y")+"/"+(name=="XBot"?"X":"Y")+" Bot@Walking.fbx";
|
||||
string path=folder+"/"+name+"_Probe.fbx";
|
||||
if(!AssetDatabase.CopyAsset(source,path))throw new Exception("Copy failed");
|
||||
var importer=(ModelImporter)AssetImporter.GetAtPath(path);
|
||||
var settings=importer.clipAnimations;
|
||||
settings[0].lockRootPositionXZ=false;
|
||||
settings[0].keepOriginalPositionXZ=false;
|
||||
importer.clipAnimations=settings;importer.SaveAndReimport();
|
||||
var clip=AssetDatabase.LoadAllAssetsAtPath(path).OfType<AnimationClip>().Single(c=>!c.name.StartsWith("__preview__"));
|
||||
var scene=EditorSceneManager.NewPreviewScene();
|
||||
AnimatorOverrideController controller=null;
|
||||
try
|
||||
{
|
||||
var go=(GameObject)PrefabUtility.InstantiatePrefab(AssetDatabase.LoadAssetAtPath<GameObject>(common+"/Prefabs/"+name+".prefab"),scene);
|
||||
var a=go.GetComponent<Animator>();
|
||||
controller=new AnimatorOverrideController(a.runtimeAnimatorController);
|
||||
var original=controller.animationClips.Single(c=>c.name=="Walk");controller[original]=clip;
|
||||
a.runtimeAnimatorController=controller;a.applyRootMotion=false;a.Rebind();a.SetInteger("Motion",1);a.Update(0);
|
||||
var points=new List<Vector3>();var roots=new List<Vector3>();
|
||||
for(int i=0;i<=60;i++)
|
||||
{
|
||||
a.Play("Base Layer.Walk",0,Mathf.Min(.99999f,i/60f));a.Update(0);
|
||||
points.Add(a.GetBoneTransform(HumanBodyBones.Hips).position-go.transform.position);roots.Add(go.transform.position);
|
||||
}
|
||||
var range=points.Aggregate(Vector3.negativeInfinity,Vector3.Max)-points.Aggregate(Vector3.positiveInfinity,Vector3.Min);
|
||||
var rootRange=roots.Aggregate(Vector3.negativeInfinity,Vector3.Max)-roots.Aggregate(Vector3.positiveInfinity,Vector3.Min);
|
||||
reports.Add(new{name,lockRootPositionXZ=false,keepOriginalPositionXZ=false,hipRangeWorldMeters=V(range),rootRangeWorldMeters=V(rootRange)});
|
||||
}
|
||||
finally { EditorSceneManager.ClosePreviewScene(scene);if(controller)UnityEngine.Object.DestroyImmediate(controller); }
|
||||
}
|
||||
}
|
||||
finally { AssetDatabase.DeleteAsset(folder); }
|
||||
string json=JsonConvert.SerializeObject(reports,Formatting.Indented);
|
||||
string output=Path.Combine(Directory.GetParent(Application.dataPath).Parent.FullName,"Tools/output/bot_animation_lab/walk_translation_extraction_comparison.json");
|
||||
File.WriteAllText(output,json);return json;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,64 @@
|
||||
#if UNITY_EDITOR
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEditor.SceneManagement;
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
|
||||
/// <summary>White-albedo XTerrain capture to prove lightmap shade is reaching the ground shader.</summary>
|
||||
public static class DiagnoseGroundWhiteGi
|
||||
{
|
||||
const string Root = "Assets/Bundles/Scenes/Town/HomeCamp";
|
||||
const string ScenePath = Root + "/HomeCamp.unity";
|
||||
const string Review = "D:/UD/AI/AIGame/Tools/output/homecamp_review";
|
||||
|
||||
public static string Run()
|
||||
{
|
||||
AssetDatabase.ImportAsset("Assets/Bundles/Shaders/XShader/XTerrain.shader", ImportAssetOptions.ForceSynchronousImport);
|
||||
var scene = EditorSceneManager.OpenScene(ScenePath, OpenSceneMode.Single);
|
||||
var root = scene.GetRootGameObjects().Single(g => g.name == "HomeCamp");
|
||||
var ground = root.transform.Find("Ground")?.GetComponentInChildren<MeshRenderer>();
|
||||
if (!ground) return "FAIL";
|
||||
|
||||
var prev = ground.sharedMaterial;
|
||||
var mat = Object.Instantiate(prev);
|
||||
var white = Texture2D.whiteTexture;
|
||||
mat.SetTexture("_LayerTex0", white); mat.SetTexture("_LayerTex1", white);
|
||||
mat.SetTexture("_LayerTex2", white); mat.SetTexture("_LayerTex3", white);
|
||||
mat.SetFloat("_LightmapContrast", 1f);
|
||||
mat.SetFloat("_BumpScale", 0f); mat.SetFloat("_Roughness", 1.5f); mat.SetFloat("_Metallic", 0f);
|
||||
ground.sharedMaterial = mat;
|
||||
|
||||
ShaderUtil.allowAsyncCompilation = false;
|
||||
var cam = root.GetComponentInChildren<Camera>();
|
||||
Capture(cam, Review + "/diag_xterrain_white_c1.png");
|
||||
|
||||
mat.SetFloat("_LightmapContrast", 2.2f);
|
||||
Capture(cam, Review + "/diag_xterrain_white_c22.png");
|
||||
|
||||
ground.sharedMaterial = prev;
|
||||
Object.DestroyImmediate(mat);
|
||||
return "wrote white-gi captures contrastProp=" + prev.GetFloat("_LightmapContrast");
|
||||
}
|
||||
|
||||
static void Capture(Camera camera, string file)
|
||||
{
|
||||
var rt = RenderTexture.GetTemporary(720, 1280, 24, RenderTextureFormat.ARGB32);
|
||||
var old = RenderTexture.active; var oldT = camera.targetTexture; var oldA = camera.aspect;
|
||||
var tex = new Texture2D(720, 1280, TextureFormat.RGB24, false);
|
||||
try
|
||||
{
|
||||
camera.aspect = 720f / 1280; camera.targetTexture = rt; RenderTexture.active = rt;
|
||||
camera.Render(); camera.Render();
|
||||
tex.ReadPixels(new Rect(0, 0, 720, 1280), 0, 0); tex.Apply();
|
||||
File.WriteAllBytes(file, tex.EncodeToPNG());
|
||||
}
|
||||
finally
|
||||
{
|
||||
camera.targetTexture = oldT; camera.aspect = oldA; RenderTexture.active = old;
|
||||
RenderTexture.ReleaseTemporary(rt); Object.DestroyImmediate(tex);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,93 @@
|
||||
#if UNITY_EDITOR
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEditor.SceneManagement;
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
|
||||
/// <summary>Temporarily force Ground to visualize bakedGI only, to verify lightmap sampling.</summary>
|
||||
public static class DiagnoseHomeCampGiViz
|
||||
{
|
||||
const string ScenePath = "Assets/Bundles/Scenes/Town/HomeCamp/HomeCamp.unity";
|
||||
const string Review = "D:/UD/AI/AIGame/Tools/output/homecamp_review";
|
||||
const string ShaderPath = "Assets/Bundles/Shaders/XShader/XTerrain.shader";
|
||||
|
||||
public static string Run()
|
||||
{
|
||||
Directory.CreateDirectory(Review);
|
||||
var scene = EditorSceneManager.OpenScene(ScenePath, OpenSceneMode.Single);
|
||||
var root = scene.GetRootGameObjects().Single(g => g.name == "HomeCamp");
|
||||
var ground = root.transform.Find("Ground")?.GetComponentInChildren<MeshRenderer>();
|
||||
if (!ground) return "FAIL: no ground";
|
||||
|
||||
var mesh = ground.GetComponent<MeshFilter>()?.sharedMesh;
|
||||
var uv1 = mesh != null && mesh.uv2 != null && mesh.uv2.Length == mesh.vertexCount;
|
||||
var uv0 = mesh != null ? mesh.uv.Length : 0;
|
||||
|
||||
// Inject a debug return of bakedGI into XTerrain fragment temporarily via material replacement:
|
||||
// use Unlit with lightmap — simpler: create temp material using URP/Lit which we know samples LM.
|
||||
var lit = new Material(Shader.Find("Universal Render Pipeline/Lit"));
|
||||
lit.SetColor("_BaseColor", Color.white);
|
||||
lit.SetFloat("_Smoothness", 0f);
|
||||
var prev = ground.sharedMaterial;
|
||||
ground.sharedMaterial = lit;
|
||||
|
||||
var cam = root.GetComponentInChildren<Camera>();
|
||||
Capture(cam, Review + "/diag_ground_urp_lit.png");
|
||||
|
||||
// Also capture original
|
||||
ground.sharedMaterial = prev;
|
||||
Capture(cam, Review + "/diag_ground_xterrain.png");
|
||||
|
||||
// Sample lightmap at ground renderer scaleOffset a few UV points
|
||||
var lm = LightmapSettings.lightmaps[0].lightmapColor;
|
||||
var so = ground.lightmapScaleOffset;
|
||||
string Samples(string label, float u, float v)
|
||||
{
|
||||
// Unity UV: v=0 bottom. Texture2D GetPixel v from bottom if we use GetPixelBilinear with Unity convention...
|
||||
float uu = u * so.x + so.z;
|
||||
float vv = v * so.y + so.w;
|
||||
var rt = RenderTexture.GetTemporary(lm.width, lm.height, 0, RenderTextureFormat.ARGBFloat);
|
||||
Graphics.Blit(lm, rt);
|
||||
var prevRt = RenderTexture.active; RenderTexture.active = rt;
|
||||
var tex = new Texture2D(lm.width, lm.height, TextureFormat.RGBAFloat, false, true);
|
||||
tex.ReadPixels(new Rect(0, 0, lm.width, lm.height), 0, 0); tex.Apply();
|
||||
RenderTexture.active = prevRt; RenderTexture.ReleaseTemporary(rt);
|
||||
// Texture2D: (0,0) bottom-left in GetPixelBilinear? Actually GetPixelBilinear y=0 is bottom.
|
||||
var c = tex.GetPixelBilinear(uu, vv);
|
||||
Object.DestroyImmediate(tex);
|
||||
float l = c.r * .3f + c.g * .59f + c.b * .11f;
|
||||
return label + "=(" + uu.ToString("F2") + "," + vv.ToString("F2") + ")->" + l.ToString("F3");
|
||||
}
|
||||
|
||||
Object.DestroyImmediate(lit);
|
||||
return "uv1=" + uv1 + " uv0Count=" + uv0 + " verts=" + (mesh ? mesh.vertexCount : 0)
|
||||
+ " idx=" + ground.lightmapIndex + " so=" + so
|
||||
+ " " + Samples("center", .5f, .5f)
|
||||
+ " " + Samples("near", .5f, .2f)
|
||||
+ " " + Samples("far", .5f, .8f)
|
||||
+ " " + Samples("left", .2f, .5f)
|
||||
+ " " + Samples("right", .8f, .5f);
|
||||
}
|
||||
|
||||
static void Capture(Camera camera, string file)
|
||||
{
|
||||
var rt = RenderTexture.GetTemporary(720, 1280, 24, RenderTextureFormat.ARGB32);
|
||||
var old = RenderTexture.active; var oldT = camera.targetTexture; var oldA = camera.aspect;
|
||||
var tex = new Texture2D(720, 1280, TextureFormat.RGB24, false);
|
||||
try
|
||||
{
|
||||
camera.aspect = 720f / 1280f; camera.targetTexture = rt; RenderTexture.active = rt;
|
||||
camera.Render();
|
||||
tex.ReadPixels(new Rect(0, 0, 720, 1280), 0, 0); tex.Apply();
|
||||
File.WriteAllBytes(file, tex.EncodeToPNG());
|
||||
}
|
||||
finally
|
||||
{
|
||||
camera.targetTexture = oldT; camera.aspect = oldA; RenderTexture.active = old;
|
||||
RenderTexture.ReleaseTemporary(rt); Object.DestroyImmediate(tex);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,120 @@
|
||||
#if UNITY_EDITOR
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Rendering;
|
||||
using UnityEngine.Rendering.Universal;
|
||||
using UnityEngine.SceneManagement;
|
||||
using UnityEditor.SceneManagement;
|
||||
|
||||
/// <summary>Dump HomeCamp lightmap stats and force a lightmap-only capture for shadow diagnosis.</summary>
|
||||
public static class DiagnoseHomeCampLightmaps
|
||||
{
|
||||
const string ScenePath = "Assets/Bundles/Scenes/Town/HomeCamp/HomeCamp.unity";
|
||||
const string Review = "D:/UD/AI/AIGame/Tools/output/homecamp_review";
|
||||
|
||||
public static string Run()
|
||||
{
|
||||
Directory.CreateDirectory(Review);
|
||||
var scene = EditorSceneManager.OpenScene(ScenePath, OpenSceneMode.Single);
|
||||
var root = scene.GetRootGameObjects().Single(g => g.name == "HomeCamp");
|
||||
var ground = root.transform.Find("Ground")?.GetComponentInChildren<MeshRenderer>();
|
||||
var maps = LightmapSettings.lightmaps;
|
||||
if (maps == null || maps.Length == 0) return "FAIL: no lightmaps loaded";
|
||||
if (!ground) return "FAIL: no ground";
|
||||
|
||||
var lm = maps[0].lightmapColor;
|
||||
var path = Review + "/lightmap0.png";
|
||||
ExportReadable(lm, path);
|
||||
|
||||
// Sample a few texels for contrast (sunlit vs likely shade).
|
||||
var readable = MakeReadable(lm);
|
||||
float minL = 99, maxL = 0, sum = 0;
|
||||
int n = 0;
|
||||
for (int y = 0; y < readable.height; y += 8)
|
||||
for (int x = 0; x < readable.width; x += 8)
|
||||
{
|
||||
var c = readable.GetPixel(x, y);
|
||||
float l = c.r * .3f + c.g * .59f + c.b * .11f;
|
||||
if (l < minL) minL = l; if (l > maxL) maxL = l; sum += l; n++;
|
||||
}
|
||||
Object.DestroyImmediate(readable);
|
||||
|
||||
var lights = Object.FindObjectsByType<Light>(FindObjectsSortMode.None)
|
||||
.Select(l => l.name + " type=" + l.type + " bake=" + l.lightmapBakeType + " inten=" + l.intensity + " en=" + l.enabled)
|
||||
.ToArray();
|
||||
|
||||
var mat = ground.sharedMaterial;
|
||||
var kw = string.Join(",", mat.shaderKeywords ?? System.Array.Empty<string>());
|
||||
var sun = Object.FindObjectsByType<Light>(FindObjectsSortMode.None).FirstOrDefault(l => l.type == LightType.Directional && l.name.Contains("Sun"));
|
||||
|
||||
// Capture A: as-is
|
||||
var cam = root.GetComponentInChildren<Camera>();
|
||||
Capture(cam, Review + "/diag_with_lights.png");
|
||||
|
||||
// Capture B: disable all realtime-contributing lights so only lightmaps/probes remain
|
||||
var prev = Object.FindObjectsByType<Light>(FindObjectsSortMode.None).Select(l => (l, l.enabled)).ToArray();
|
||||
foreach (var (l, _) in prev) l.enabled = false;
|
||||
Capture(cam, Review + "/diag_lightmap_only.png");
|
||||
foreach (var (l, en) in prev) l.enabled = en;
|
||||
|
||||
return "maps=" + maps.Length + " groundLM=" + ground.lightmapIndex
|
||||
+ " scale=" + ground.lightmapScaleOffset
|
||||
+ " lmSize=" + lm.width + "x" + lm.height
|
||||
+ " lumaMin=" + minL.ToString("F3") + " max=" + maxL.ToString("F3") + " avg=" + (sum / n).ToString("F3")
|
||||
+ " exported=" + path
|
||||
+ " keywords=[" + kw + "]"
|
||||
+ " lights=[" + string.Join("; ", lights) + "]"
|
||||
+ " sunBake=" + (sun ? sun.lightmapBakeType.ToString() : "null");
|
||||
}
|
||||
|
||||
static void Capture(Camera camera, string file)
|
||||
{
|
||||
var rt = RenderTexture.GetTemporary(720, 1280, 24, RenderTextureFormat.ARGB32);
|
||||
var old = RenderTexture.active; var oldTarget = camera.targetTexture; var oldAspect = camera.aspect;
|
||||
var tex = new Texture2D(720, 1280, TextureFormat.RGB24, false);
|
||||
try
|
||||
{
|
||||
camera.aspect = 720f / 1280; camera.targetTexture = rt; RenderTexture.active = rt;
|
||||
camera.Render();
|
||||
tex.ReadPixels(new Rect(0, 0, 720, 1280), 0, 0); tex.Apply();
|
||||
File.WriteAllBytes(file, tex.EncodeToPNG());
|
||||
}
|
||||
finally
|
||||
{
|
||||
camera.targetTexture = oldTarget; camera.aspect = oldAspect; RenderTexture.active = old;
|
||||
RenderTexture.ReleaseTemporary(rt); Object.DestroyImmediate(tex);
|
||||
}
|
||||
}
|
||||
|
||||
static Texture2D MakeReadable(Texture2D src)
|
||||
{
|
||||
var rt = RenderTexture.GetTemporary(src.width, src.height, 0, RenderTextureFormat.ARGBFloat);
|
||||
Graphics.Blit(src, rt);
|
||||
var prev = RenderTexture.active; RenderTexture.active = rt;
|
||||
var tex = new Texture2D(src.width, src.height, TextureFormat.RGBAFloat, false);
|
||||
tex.ReadPixels(new Rect(0, 0, src.width, src.height), 0, 0); tex.Apply();
|
||||
RenderTexture.active = prev; RenderTexture.ReleaseTemporary(rt);
|
||||
return tex;
|
||||
}
|
||||
|
||||
static void ExportReadable(Texture2D src, string path)
|
||||
{
|
||||
var tex = MakeReadable(src);
|
||||
// Tone-map HDR lightmap into LDR preview
|
||||
var outTex = new Texture2D(tex.width, tex.height, TextureFormat.RGB24, false);
|
||||
for (int y = 0; y < tex.height; y++)
|
||||
for (int x = 0; x < tex.width; x++)
|
||||
{
|
||||
var c = tex.GetPixel(x, y);
|
||||
// Unity lightmaps are often dLDR / RGBM-ish; simple expose
|
||||
float e = 1.5f;
|
||||
outTex.SetPixel(x, y, new Color(Mathf.Clamp01(c.r * e), Mathf.Clamp01(c.g * e), Mathf.Clamp01(c.b * e), 1));
|
||||
}
|
||||
outTex.Apply();
|
||||
File.WriteAllBytes(path, outTex.EncodeToPNG());
|
||||
Object.DestroyImmediate(tex); Object.DestroyImmediate(outTex);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,146 @@
|
||||
#if UNITY_EDITOR
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
using UnityEditor.SceneManagement;
|
||||
using UnityEditor.Animations;
|
||||
using UnityEngine.Animations;
|
||||
using UnityEngine.Playables;
|
||||
using System.Collections.Generic;
|
||||
|
||||
public static class DiagnoseMixamoPlayback
|
||||
{
|
||||
static readonly string[] Paths={"Assets/Bundles/Characters/Human/test/Boy_mixamo/CampGuideStudio.fbx","Assets/Bundles/Characters/Human/Common/Walking.fbx","Assets/Bundles/Characters/Human/Common/CampGuideStudio@Walking.fbx"};
|
||||
static float[] V(Vector3 v)=>new[]{v.x,v.y,v.z};
|
||||
static string PathOf(UnityEngine.Object o)=>o?AssetDatabase.GetAssetPath(o):null;
|
||||
const string DiagnosticRoot="Assets/Review/CharacterAnimationLab/PlaybackDiagnostics";
|
||||
static string OutputDirectory=>System.IO.Path.Combine(Directory.GetParent(Application.dataPath).Parent.FullName,"Tools/output/campguide_studio/playback_audit");
|
||||
static void Folder(string path){if(AssetDatabase.IsValidFolder(path))return;var parent=System.IO.Path.GetDirectoryName(path).Replace('\\','/');Folder(parent);AssetDatabase.CreateFolder(parent,System.IO.Path.GetFileName(path));}
|
||||
public static string PrepareVariants()
|
||||
{
|
||||
Folder(DiagnosticRoot);
|
||||
var avatar=AssetDatabase.LoadAllAssetsAtPath(Paths[0]).OfType<Avatar>().Single();
|
||||
foreach(var tag in new[]{"CopyBoyAvatar","Generic"})
|
||||
{
|
||||
string path=DiagnosticRoot+"/Walking_"+tag+".fbx";
|
||||
if(!AssetDatabase.LoadMainAssetAtPath(path) && !AssetDatabase.CopyAsset(Paths[1],path))throw new Exception("Copy failed "+path);
|
||||
var importer=(ModelImporter)AssetImporter.GetAtPath(path);
|
||||
importer.animationType=tag=="Generic"?ModelImporterAnimationType.Generic:ModelImporterAnimationType.Human;
|
||||
importer.avatarSetup=tag=="Generic"?ModelImporterAvatarSetup.NoAvatar:ModelImporterAvatarSetup.CopyFromOther;
|
||||
importer.sourceAvatar=tag=="Generic"?null:avatar;
|
||||
if(tag=="Generic")importer.animationCompression=ModelImporterAnimationCompression.Off;
|
||||
importer.SaveAndReimport();
|
||||
}
|
||||
return "Prepared isolated generic and CopyBoyAvatar variants";
|
||||
}
|
||||
public static string FixWalking()
|
||||
{
|
||||
var importer=(ModelImporter)AssetImporter.GetAtPath(Paths[1]);
|
||||
var before=new{path=Paths[1],animationType=importer.animationType.ToString(),avatarSetup=importer.avatarSetup.ToString(),sourceAvatar=PathOf(importer.sourceAvatar)};
|
||||
Directory.CreateDirectory(OutputDirectory);
|
||||
var backup=System.IO.Path.Combine(OutputDirectory,"walking_importer_before.json");
|
||||
if(!File.Exists(backup))File.WriteAllText(backup,JsonConvert.SerializeObject(before,Formatting.Indented));
|
||||
importer.animationType=ModelImporterAnimationType.Human;
|
||||
importer.avatarSetup=ModelImporterAvatarSetup.CopyFromOther;
|
||||
importer.sourceAvatar=AssetDatabase.LoadAllAssetsAtPath(Paths[0]).OfType<Avatar>().Single();
|
||||
importer.SaveAndReimport();
|
||||
var clip=AssetDatabase.LoadAllAssetsAtPath(Paths[1]).OfType<AnimationClip>().First(c=>!c.name.StartsWith("__preview__"));
|
||||
if(!clip.humanMotion || !importer.sourceAvatar.isValid)throw new Exception("Invalid repaired Humanoid clip/avatar");
|
||||
var report=new{path=Paths[1],avatarSetup=importer.avatarSetup.ToString(),sourceAvatar=PathOf(importer.sourceAvatar),clip=clip.name,clip.humanMotion,
|
||||
scenes=Enumerable.Range(0,SceneManager.sceneCount).Select(i=>{var s=SceneManager.GetSceneAt(i);return new{s.path,s.isDirty};}).ToArray()};
|
||||
string json=JsonConvert.SerializeObject(report,Formatting.Indented);File.WriteAllText(System.IO.Path.Combine(OutputDirectory,"fix.json"),json);return json;
|
||||
}
|
||||
public static string VerifyFix()
|
||||
{
|
||||
var importer=(ModelImporter)AssetImporter.GetAtPath(Paths[1]);
|
||||
var avatar=AssetDatabase.LoadAllAssetsAtPath(Paths[0]).OfType<Avatar>().Single();
|
||||
if(importer.avatarSetup!=ModelImporterAvatarSetup.CopyFromOther || importer.sourceAvatar!=avatar)throw new Exception("Wrong source avatar");
|
||||
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(PathOf(state.motion)!=Paths[1] || !(state.motion as AnimationClip).humanMotion)throw new Exception("Controller walk clip lost");
|
||||
var animators=UnityEngine.Object.FindObjectsOfType<Animator>(true).Where(a=>a.runtimeAnimatorController==controller).ToArray();
|
||||
var warnings=animators.Select(a=>new SerializedObject(a).FindProperty("m_WarningMessage")?.stringValue).Where(w=>!string.IsNullOrEmpty(w)).ToArray();
|
||||
if(warnings.Length>0)throw new Exception(string.Join("\n",warnings));
|
||||
var json=JsonConvert.SerializeObject(new{sourceAvatar=PathOf(avatar),controller=PathOf(controller),walk=PathOf(state.motion),footIK=state.iKOnFeet,
|
||||
actorCount=animators.Length,bindingWarnings=warnings.Length,EditorApplication.isPlaying,
|
||||
dirtyScenes=Enumerable.Range(0,SceneManager.sceneCount).Select(i=>{var s=SceneManager.GetSceneAt(i);return new{s.path,s.isDirty};}).ToArray()},Formatting.Indented);
|
||||
File.WriteAllText(System.IO.Path.Combine(OutputDirectory,"validation.json"),json);return json;
|
||||
}
|
||||
class Sampled
|
||||
{
|
||||
public string tag;public GameObject unit;public Animator animator;public PlayableGraph graph;public AnimationClipPlayable playable;
|
||||
public Transform[] bones;public List<Dictionary<string,float[]>> frames=new List<Dictionary<string,float[]>>();
|
||||
}
|
||||
public static string Compare()
|
||||
{
|
||||
var scene=EditorSceneManager.NewPreviewScene();var entries=new List<Sampled>();
|
||||
var source=AssetDatabase.LoadAssetAtPath<GameObject>(Paths[0]);
|
||||
var material=new Material(Shader.Find("Universal Render Pipeline/Lit"));material.color=new Color(.6f,.65f,.72f);
|
||||
try
|
||||
{
|
||||
string[] tags={"Generic","CurrentHumanoid","CopyBoyAvatar"};
|
||||
string[] paths={DiagnosticRoot+"/Walking_Generic.fbx",Paths[1],DiagnosticRoot+"/Walking_CopyBoyAvatar.fbx"};
|
||||
for(int i=0;i<tags.Length;i++)
|
||||
{
|
||||
var unit=(GameObject)PrefabUtility.InstantiatePrefab(source,scene);unit.transform.position=new Vector3((i-1)*1.25f,0,0);
|
||||
var animator=unit.GetComponent<Animator>();animator.runtimeAnimatorController=null;animator.applyRootMotion=false;animator.cullingMode=AnimatorCullingMode.AlwaysAnimate;
|
||||
if(tags[i]=="Generic")animator.avatar=null;
|
||||
var clip=AssetDatabase.LoadAllAssetsAtPath(paths[i]).OfType<AnimationClip>().First(c=>!c.name.StartsWith("__preview__"));
|
||||
var graph=PlayableGraph.Create("MixamoDiagnostic"+tags[i]);graph.SetTimeUpdateMode(DirectorUpdateMode.Manual);
|
||||
var playable=AnimationClipPlayable.Create(graph,clip);playable.SetApplyFootIK(false);playable.SetApplyPlayableIK(false);
|
||||
var output=AnimationPlayableOutput.Create(graph,"Animation",animator);output.SetSourcePlayable(playable);graph.Play();
|
||||
foreach(var r in unit.GetComponentsInChildren<Renderer>())r.sharedMaterial=material;
|
||||
entries.Add(new Sampled{tag=tags[i],unit=unit,animator=animator,graph=graph,playable=playable,bones=unit.GetComponentsInChildren<Transform>().Where(t=>t.name.StartsWith("mixamorig:")).ToArray()});
|
||||
}
|
||||
for(int frame=0;frame<32;frame++)foreach(var e in entries)
|
||||
{
|
||||
e.playable.SetTime(frame/30.0);e.graph.Evaluate(0);
|
||||
e.frames.Add(e.bones.ToDictionary(b=>b.name,b=>V(e.unit.transform.InverseTransformPoint(b.position))));
|
||||
}
|
||||
var reference=entries[0];var results=new List<object>();
|
||||
foreach(var e in entries.Skip(1))
|
||||
{
|
||||
var errors=new Dictionary<string,float>();
|
||||
for(int frame=0;frame<32;frame++)foreach(string bone in e.frames[frame].Keys)
|
||||
{
|
||||
Vector3 P(float[] p)=>new Vector3(p[0],p[1],p[2]);
|
||||
var actual=P(e.frames[frame][bone])-P(e.frames[frame]["mixamorig:Hips"]);
|
||||
var expected=P(reference.frames[frame][bone])-P(reference.frames[frame]["mixamorig:Hips"]);
|
||||
float error=Vector3.Distance(actual,expected);if(!errors.ContainsKey(bone)||errors[bone]<error)errors[bone]=error;
|
||||
}
|
||||
results.Add(new{e.tag,maxJointErrorM=errors.Values.Max(),jointErrors=errors.OrderByDescending(p=>p.Value).ToArray()});
|
||||
}
|
||||
foreach(var e in entries){e.playable.SetTime(10/30.0);e.graph.Evaluate(0);var hips=e.bones.First(b=>b.name=="mixamorig:Hips");var p=hips.position;p.x=e.unit.transform.position.x;p.z=0;hips.position=p;}
|
||||
var light=new GameObject("DiagnosticLight").AddComponent<Light>();SceneManager.MoveGameObjectToScene(light.gameObject,scene);light.type=LightType.Directional;light.intensity=1.4f;light.transform.rotation=Quaternion.Euler(40,140,0);
|
||||
var camera=new GameObject("DiagnosticCamera").AddComponent<Camera>();SceneManager.MoveGameObjectToScene(camera.gameObject,scene);camera.scene=scene;camera.orthographic=true;camera.orthographicSize=.78f;
|
||||
camera.transform.position=new Vector3(0,.62f,5);camera.transform.LookAt(new Vector3(0,.62f,0));camera.clearFlags=CameraClearFlags.SolidColor;camera.backgroundColor=new Color(.2f,.23f,.27f);camera.nearClipPlane=.01f;camera.farClipPlane=20;
|
||||
var rt=new RenderTexture(1440,640,24);var tex=new Texture2D(1440,640,TextureFormat.RGB24,false);var previous=RenderTexture.active;
|
||||
Directory.CreateDirectory(OutputDirectory);
|
||||
try{camera.targetTexture=rt;camera.Render();RenderTexture.active=rt;tex.ReadPixels(new Rect(0,0,1440,640),0,0);tex.Apply();File.WriteAllBytes(System.IO.Path.Combine(OutputDirectory,"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=tags,comparison=results,samples=entries.Select(e=>new{e.tag,e.frames})},Formatting.Indented);File.WriteAllText(System.IO.Path.Combine(OutputDirectory,"comparison.json"),json);
|
||||
return JsonConvert.SerializeObject(results);
|
||||
}
|
||||
finally{foreach(var e in entries)if(e.graph.IsValid())e.graph.Destroy();EditorSceneManager.ClosePreviewScene(scene);UnityEngine.Object.DestroyImmediate(material);}
|
||||
}
|
||||
public static string Inspect()
|
||||
{
|
||||
var result=new{EditorApplication.isPlaying,scenes=Enumerable.Range(0,SceneManager.sceneCount).Select(i=>{var s=SceneManager.GetSceneAt(i);return new{s.path,s.isDirty};}).ToArray(),
|
||||
assets=Paths.Select(path=>{
|
||||
var importer=(ModelImporter)AssetImporter.GetAtPath(path);var hd=importer.humanDescription;var model=AssetDatabase.LoadAssetAtPath<GameObject>(path);
|
||||
return new{path,importer.animationType,importer.avatarSetup,sourceAvatar=PathOf(importer.sourceAvatar),importer.globalScale,importer.useFileScale,importer.bakeAxisConversion,
|
||||
human=new{hd.armStretch,hd.legStretch,hd.upperArmTwist,hd.lowerArmTwist,hd.upperLegTwist,hd.lowerLegTwist,hd.feetSpacing,hd.hasTranslationDoF,
|
||||
mapping=hd.human.Select(b=>new{b.humanName,b.boneName}).ToArray(),skeleton=hd.skeleton.Select(b=>new{b.name,position=V(b.position),rotation=V(b.rotation.eulerAngles),scale=V(b.scale)}).ToArray()},
|
||||
bones=model.GetComponentsInChildren<Transform>(true).Where(t=>t.name.StartsWith("mixamorig:")).Select(t=>new{t.name,parent=t.parent?t.parent.name:null,world=V(model.transform.InverseTransformPoint(t.position)),local=V(t.localPosition),rotation=V(t.localEulerAngles)}).ToArray(),
|
||||
clips=importer.clipAnimations.Select(c=>new{c.name,c.firstFrame,c.lastFrame,c.loopTime,c.loopPose,c.lockRootRotation,c.keepOriginalOrientation,c.rotationOffset,c.lockRootPositionXZ,c.keepOriginalPositionXZ,c.lockRootHeightY,c.heightFromFeet}).ToArray(),
|
||||
animatorClips=AssetDatabase.LoadAllAssetsAtPath(path).OfType<AnimationClip>().Select(c=>new{c.name,c.humanMotion,c.length,settings=AnimationUtility.GetAnimationClipSettings(c)}).ToArray()};
|
||||
}).ToArray()};
|
||||
string json=JsonConvert.SerializeObject(result,Formatting.Indented);
|
||||
string dir=System.IO.Path.Combine(Directory.GetParent(Application.dataPath).Parent.FullName,"Tools/output/campguide_studio/playback_audit");Directory.CreateDirectory(dir);File.WriteAllText(System.IO.Path.Combine(dir,"importers.json"),json);return "Saved "+dir+"/importers.json";
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,178 @@
|
||||
#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
|
||||
@@ -0,0 +1,63 @@
|
||||
#if UNITY_EDITOR
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEditor.SceneManagement;
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
|
||||
/// <summary>Force-reimport XTerrain, punch ground lightmap contrast, recapture HomeCamp review.</summary>
|
||||
public static class FixHomeCampShadowPunch
|
||||
{
|
||||
const string Root = "Assets/Bundles/Scenes/Town/HomeCamp";
|
||||
const string ScenePath = Root + "/HomeCamp.unity";
|
||||
const string Review = "D:/UD/AI/AIGame/Tools/output/homecamp_review";
|
||||
|
||||
public static string Run()
|
||||
{
|
||||
AssetDatabase.ImportAsset("Assets/Bundles/Shaders/XShader/XTerrain.shader", ImportAssetOptions.ForceSynchronousImport);
|
||||
AssetDatabase.ImportAsset("Assets/Bundles/Shaders/XShader/XTerrain_Base.hlsl", ImportAssetOptions.ForceSynchronousImport);
|
||||
|
||||
var mat = AssetDatabase.LoadAssetAtPath<Material>(Root + "/ground_terrain.mat");
|
||||
if (!mat) return "FAIL: no ground mat";
|
||||
// Stronger than 1.45 — busy albedo still hid plaza shade after the CBUFFER fix.
|
||||
mat.SetFloat("_LightmapContrast", 1.85f);
|
||||
EditorUtility.SetDirty(mat);
|
||||
AssetDatabase.SaveAssetIfDirty(mat);
|
||||
|
||||
var scene = EditorSceneManager.OpenScene(ScenePath, OpenSceneMode.Single);
|
||||
var root = scene.GetRootGameObjects().Single(g => g.name == "HomeCamp");
|
||||
var ground = root.transform.Find("Ground")?.GetComponentInChildren<MeshRenderer>();
|
||||
var live = ground ? ground.sharedMaterial.GetFloat("_LightmapContrast") : -1f;
|
||||
|
||||
// Warm shader variants so Capture does not draw the first frame without LIGHTMAP_ON.
|
||||
ShaderUtil.allowAsyncCompilation = false;
|
||||
var cam = root.GetComponentInChildren<Camera>();
|
||||
Capture(cam, Review + "/homecamp_portrait.png", 1080, 1920);
|
||||
Capture(cam, Review + "/homecamp_tall.png", 1080, 2400);
|
||||
|
||||
return "contrast=" + live.ToString("F2") + " groundLM=" + (ground ? ground.lightmapIndex.ToString() : "?")
|
||||
+ " maps=" + LightmapSettings.lightmaps.Length;
|
||||
}
|
||||
|
||||
static void Capture(Camera camera, string file, int w, int h)
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(file));
|
||||
var rt = RenderTexture.GetTemporary(w, h, 24, RenderTextureFormat.ARGB32);
|
||||
var old = RenderTexture.active; var oldT = camera.targetTexture; var oldA = camera.aspect;
|
||||
var tex = new Texture2D(w, h, TextureFormat.RGB24, false);
|
||||
try
|
||||
{
|
||||
camera.aspect = (float)w / h; camera.targetTexture = rt; RenderTexture.active = rt;
|
||||
camera.Render(); camera.Render();
|
||||
tex.ReadPixels(new Rect(0, 0, w, h), 0, 0); tex.Apply();
|
||||
File.WriteAllBytes(file, tex.EncodeToPNG());
|
||||
}
|
||||
finally
|
||||
{
|
||||
camera.targetTexture = oldT; camera.aspect = oldA; RenderTexture.active = old;
|
||||
RenderTexture.ReleaseTemporary(rt); Object.DestroyImmediate(tex);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -1,18 +0,0 @@
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
public static class FrameTownSquare
|
||||
{
|
||||
public static string Run()
|
||||
{
|
||||
SceneView.FocusWindowIfItsOpen<SceneView>();
|
||||
var v = SceneView.lastActiveSceneView;
|
||||
if (v == null && SceneView.sceneViews.Count > 0)
|
||||
v = SceneView.sceneViews[0] as SceneView;
|
||||
if (v == null)
|
||||
return "no scene view";
|
||||
v.LookAt(new Vector3(0f, 1.0f, 0.5f), Quaternion.Euler(38f, 12f, 0f), 22f);
|
||||
v.Repaint();
|
||||
return "framed";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
#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,34 @@
|
||||
#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
|
||||
@@ -1,35 +0,0 @@
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
|
||||
public static class InspectTownSquare
|
||||
{
|
||||
public static string Run()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
var root = GameObject.Find("TownSquare");
|
||||
if (root == null)
|
||||
return "no TownSquare";
|
||||
foreach (Transform t in root.transform)
|
||||
{
|
||||
var b = default(Bounds);
|
||||
var has = false;
|
||||
foreach (var r in t.GetComponentsInChildren<Renderer>())
|
||||
{
|
||||
if (!has) { b = r.bounds; has = true; }
|
||||
else b.Encapsulate(r.bounds);
|
||||
}
|
||||
var cols = t.GetComponentsInChildren<MeshCollider>().Length;
|
||||
sb.AppendFormat(
|
||||
"{0} pos={1} euler={2} scale={3} boundsC={4} size={5} meshCol={6}\n",
|
||||
t.name,
|
||||
t.position,
|
||||
t.eulerAngles,
|
||||
t.localScale,
|
||||
has ? b.center : Vector3.zero,
|
||||
has ? b.size : Vector3.zero,
|
||||
cols);
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
#if UNITY_EDITOR
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// Pipeline script to optimize AudioImporter settings across all project sound assets.
|
||||
/// Converts uncompressed stereo one-shots to mono ADPCM, sets BGM to 2D streaming Vorbis.
|
||||
/// </summary>
|
||||
public static class OptimizeAudioSettings
|
||||
{
|
||||
const string SoundRoot = "Assets/Bundles/Sound";
|
||||
|
||||
public static string Run()
|
||||
{
|
||||
if (EditorApplication.isPlayingOrWillChangePlaymode)
|
||||
throw new InvalidOperationException("Run in Edit Mode.");
|
||||
|
||||
int count = 0;
|
||||
|
||||
var guids = AssetDatabase.FindAssets("t:AudioClip", new[] { SoundRoot });
|
||||
foreach (var guid in guids)
|
||||
{
|
||||
string path = AssetDatabase.GUIDToAssetPath(guid);
|
||||
var importer = AssetImporter.GetAtPath(path) as AudioImporter;
|
||||
if (importer == null) continue;
|
||||
|
||||
bool isBgm = path.Contains("/BGM/");
|
||||
|
||||
// 1. Force to mono for all one-shots (cuts memory in half)
|
||||
importer.forceToMono = !isBgm;
|
||||
|
||||
// 2. Disable 3D spatial calculations for UI, 2D battle feedbacks and BGM
|
||||
var serialized = new SerializedObject(importer);
|
||||
var prop3D = serialized.FindProperty("3D");
|
||||
if (prop3D != null && prop3D.boolValue)
|
||||
{
|
||||
prop3D.boolValue = false;
|
||||
serialized.ApplyModifiedPropertiesWithoutUndo();
|
||||
}
|
||||
|
||||
// 3. Setup platform default settings
|
||||
var settings = importer.defaultSampleSettings;
|
||||
if (isBgm)
|
||||
{
|
||||
// Streaming Vorbis for long background music tracks
|
||||
settings.loadType = AudioClipLoadType.Streaming;
|
||||
settings.compressionFormat = AudioCompressionFormat.Vorbis;
|
||||
settings.quality = 0.75f;
|
||||
settings.preloadAudioData = false;
|
||||
importer.loadInBackground = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fast ADPCM with zero decompression overhead for short gameplay one-shots
|
||||
settings.loadType = AudioClipLoadType.DecompressOnLoad;
|
||||
settings.compressionFormat = AudioCompressionFormat.ADPCM;
|
||||
settings.sampleRateSetting = AudioSampleRateSetting.PreserveSampleRate;
|
||||
settings.preloadAudioData = true;
|
||||
importer.loadInBackground = false;
|
||||
}
|
||||
|
||||
importer.defaultSampleSettings = settings;
|
||||
importer.SaveAndReimport();
|
||||
count++;
|
||||
}
|
||||
|
||||
AssetDatabase.SaveAssets();
|
||||
AssetDatabase.Refresh();
|
||||
|
||||
return $"Optimized {count} audio assets across UI, Combat SFX, and BGM.";
|
||||
}
|
||||
|
||||
public static string Verify()
|
||||
{
|
||||
var guids = AssetDatabase.FindAssets("t:AudioClip", new[] { SoundRoot });
|
||||
int verified = 0;
|
||||
foreach (var guid in guids)
|
||||
{
|
||||
string path = AssetDatabase.GUIDToAssetPath(guid);
|
||||
var importer = AssetImporter.GetAtPath(path) as AudioImporter;
|
||||
if (importer == null) continue;
|
||||
|
||||
bool isBgm = path.Contains("/BGM/");
|
||||
var settings = importer.defaultSampleSettings;
|
||||
|
||||
if (isBgm)
|
||||
{
|
||||
if (settings.loadType != AudioClipLoadType.Streaming || settings.compressionFormat != AudioCompressionFormat.Vorbis)
|
||||
throw new Exception($"BGM not streaming Vorbis: {path}");
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!importer.forceToMono)
|
||||
throw new Exception($"One-shot SFX not forced to mono: {path}");
|
||||
if (settings.compressionFormat != AudioCompressionFormat.ADPCM)
|
||||
throw new Exception($"One-shot SFX not ADPCM: {path}");
|
||||
}
|
||||
verified++;
|
||||
}
|
||||
return $"PASS all {verified} audio assets conform to mobile runtime specifications.";
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
#if UNITY_EDITOR
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// 把 CaoCao Prefab 摆到当前场景广场中心,Y 取地表碰撞高度。不保存进 TownSquare.prefab。
|
||||
/// </summary>
|
||||
public static class PlaceCaoCao
|
||||
{
|
||||
const string PrefabPath = "Assets/Bundles/Characters/Human/CaoCao/CaoCao.prefab";
|
||||
|
||||
public static string Run()
|
||||
{
|
||||
var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(PrefabPath);
|
||||
if (prefab == null)
|
||||
return "missing " + PrefabPath;
|
||||
|
||||
var existing = GameObject.Find("CaoCao_Preview");
|
||||
if (existing != null)
|
||||
Object.DestroyImmediate(existing);
|
||||
|
||||
var go = (GameObject)PrefabUtility.InstantiatePrefab(prefab);
|
||||
go.name = "CaoCao_Preview";
|
||||
|
||||
Physics.SyncTransforms();
|
||||
var y = SampleGroundY(0f, 0f, out var sitInfo);
|
||||
go.transform.position = new Vector3(0f, y, 0f);
|
||||
go.transform.rotation = Quaternion.identity;
|
||||
|
||||
var bounds = default(Bounds);
|
||||
var has = false;
|
||||
foreach (var r in go.GetComponentsInChildren<Renderer>())
|
||||
{
|
||||
if (!has) { bounds = r.bounds; has = true; }
|
||||
else bounds.Encapsulate(r.bounds);
|
||||
}
|
||||
return $"pos={go.transform.position} h={(has ? bounds.size.y : 0f):F3} groundY={y:F3} {sitInfo}";
|
||||
}
|
||||
|
||||
static float SampleGroundY(float x, float z, out string info)
|
||||
{
|
||||
var plaza = GameObject.Find("TownSquare");
|
||||
var cols = plaza != null
|
||||
? plaza.GetComponentsInChildren<MeshCollider>()
|
||||
: Object.FindObjectsByType<MeshCollider>(FindObjectsSortMode.None);
|
||||
var y = 0f;
|
||||
var hitAny = false;
|
||||
var best = float.MaxValue;
|
||||
var meshName = "";
|
||||
var boundsTop = 0f;
|
||||
foreach (var col in cols)
|
||||
{
|
||||
if (col == null || !col.enabled) continue;
|
||||
meshName = col.sharedMesh != null ? col.sharedMesh.name : "null";
|
||||
boundsTop = col.bounds.max.y;
|
||||
var origins = new[]
|
||||
{
|
||||
new Vector3(x, 80f, z),
|
||||
new Vector3(col.bounds.center.x, col.bounds.max.y + 8f, col.bounds.center.z),
|
||||
new Vector3(x + 0.35f, 80f, z + 0.35f),
|
||||
};
|
||||
foreach (var origin in origins)
|
||||
{
|
||||
var ray = new Ray(origin, Vector3.down);
|
||||
if (!col.Raycast(ray, out var hit, 160f)) continue;
|
||||
if (hit.distance >= best) continue;
|
||||
best = hit.distance;
|
||||
y = hit.point.y;
|
||||
hitAny = true;
|
||||
}
|
||||
}
|
||||
if (!hitAny && boundsTop > 0.01f)
|
||||
{
|
||||
y = boundsTop;
|
||||
info = $"fallbackBoundsTop={y:F3} mesh={meshName} cols={cols.Length}";
|
||||
return y;
|
||||
}
|
||||
info = $"plaza={(plaza != null ? plaza.name : "null")} cols={cols.Length} mesh={meshName} meshHit={hitAny}";
|
||||
return hitAny ? y : 0f;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,13 @@
|
||||
/// <summary>Refresh configured Art bundles after scene asset retirement.</summary>
|
||||
public static class RefreshSceneArt
|
||||
{
|
||||
public static string UpdateCollectors()
|
||||
{
|
||||
return AOT.Editor.YooAssetTools.BuildAndStageArt.EnsureLoadableScenePrefabs();
|
||||
}
|
||||
|
||||
public static string Build()
|
||||
{
|
||||
return AOT.Editor.YooAssetTools.BuildAndStageArt.BuildConfiguredAndStage();
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ using UnityEngine;
|
||||
using YooAsset.Editor;
|
||||
|
||||
/// <summary>
|
||||
/// 新角色 Prefab:Y-up、底面原点、局部 -Z 正面,不做 +90X。
|
||||
/// 新角色 Prefab:Y-up、底面原点、局部 +Z 正面,不做 +90X。旧资产保留原绑定空间。
|
||||
/// unity command run_script --file AgentScripts/SetupCharacter.cs --entry SetupCharacter.Run --args "[\"Human\",\"CaoCao\"]"
|
||||
/// </summary>
|
||||
public static class SetupCharacter
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
#if UNITY_EDITOR
|
||||
using System.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
public static class TuneHomeCampShadeLift
|
||||
{
|
||||
const string Root = "Assets/Bundles/Scenes/Town/HomeCamp";
|
||||
|
||||
public static string Run()
|
||||
{
|
||||
AssetDatabase.ImportAsset("Assets/Bundles/Shaders/XShader/HomeCampXPbr.shader", ImportAssetOptions.ForceSynchronousImport);
|
||||
AssetDatabase.ImportAsset("Assets/Bundles/Shaders/XShader/XTerrain.shader", ImportAssetOptions.ForceSynchronousImport);
|
||||
|
||||
var report = new System.Collections.Generic.List<string>();
|
||||
foreach (var path in AssetDatabase.FindAssets("t:Material", new[] { Root }).Select(AssetDatabase.GUIDToAssetPath))
|
||||
{
|
||||
var mat = AssetDatabase.LoadAssetAtPath<Material>(path);
|
||||
if (!mat || mat.shader == null) continue;
|
||||
if (mat.shader.name == "Test/HomeCampXPbr")
|
||||
{
|
||||
// Stronger open: gamma 0.4 ≈ visibly half the crushed shade.
|
||||
mat.SetFloat("_LightmapShadeGamma", .4f);
|
||||
mat.SetFloat("_AO", .5f);
|
||||
EditorUtility.SetDirty(mat);
|
||||
report.Add(mat.name + " gamma=" + mat.GetFloat("_LightmapShadeGamma"));
|
||||
}
|
||||
else if (mat.shader.name == "XWorld/XTerrain")
|
||||
{
|
||||
mat.SetFloat("_LightmapContrast", .4f);
|
||||
mat.SetFloat("_LightmapShadeMul", 1f);
|
||||
mat.SetColor("_LightmapShadeTint", new Color(.9f, .92f, .96f, 1f));
|
||||
EditorUtility.SetDirty(mat);
|
||||
report.Add(mat.name + " contrast=" + mat.GetFloat("_LightmapContrast"));
|
||||
}
|
||||
}
|
||||
AssetDatabase.SaveAssets();
|
||||
return string.Join("; ", report);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -111,7 +111,7 @@ public static class VerifyCollection
|
||||
PrototypeReply response=null;string error=null;
|
||||
Flow.StartCoroutine(api.Send(command,(r,e)=>{response=r;error=e;}));await Until(()=>response!=null||error!=null);Check(response?.ok==true,"Capture failed before simulated crash");
|
||||
var session=Get<Func<SessionData>>("_session")();string key="AIGame.G1.pending."+Uri.EscapeDataString("http://127.0.0.1:18081")+"."+session.Pid;
|
||||
PlayerPrefs.SetString(key,JsonUtility.ToJson(command));PlayerPrefs.Save();Set("_needsSync",true);Flow.Handle("Home");
|
||||
UnityEngine.PlayerPrefs.SetString(key,JsonUtility.ToJson(command));UnityEngine.PlayerPrefs.Save();Set("_needsSync",true);Flow.Handle("Home");
|
||||
await Until(()=>!Flow.IsBusy&&Get<PrototypeReply>("_reply").replayed);
|
||||
Check(Flow.Game.State.tickets==tickets-1&&Flow.Game.State.captureCount==count+1&&Flow.Game.State.shards.Sum()==shards+20&&!api.HasPending,"Replayed capture redrew or double-debited");
|
||||
var dialog=UiManager.Instance.Get(ConfirmDialogView.PrefabName).GetComponent<ConfirmDialogView>().CaptureState();Check(dialog.Visible&&dialog.Title=="获得进化碎片","Recovered draw result not surfaced");Capture("recovery");Click("Ok");
|
||||
|
||||
@@ -117,9 +117,9 @@ public static class VerifyHomeMobileUi
|
||||
{
|
||||
var hud = UiManager.Instance?.Get("UI_MainHUD");
|
||||
Check(hud != null && hud.GetComponentInChildren<HomeExplorerPresenter>().Ready, "UI preview is still loading");
|
||||
Check(Object.FindObjectsOfType<HotUpdate.TownSquareSpawner>().Length == 0 && Object.FindObjectsOfType<HotUpdate.CharacterSpawner>().Length == 0,
|
||||
Check(Object.FindObjectsOfType<HotUpdate.CharacterSpawner>().Length == 0,
|
||||
"Startup still creates 3D world spawners");
|
||||
Check(GameObject.Find("TownSquare_Runtime") == null && GameObject.Find("CaoCao_Runtime") == null, "Startup world was loaded");
|
||||
Check(GameObject.Find("CaoCao_Runtime") == null, "Startup world was loaded");
|
||||
var raw = hud.GetComponentInChildren<RawImage>();
|
||||
Check(raw.enabled && raw.texture is RenderTexture && raw.material.shader.name == "AIGame/HomeExplorerCutout", "Live preview material missing");
|
||||
var presenter = hud.GetComponentInChildren<HomeExplorerPresenter>();
|
||||
|
||||
Reference in New Issue
Block a user