Add TownSquare scene assets, resource production docs, and model pipeline updates.
Bring in plaza props/prefabs, ZhaoYun source GLB, design originals, and Hunyuan/Blender tooling used to produce them. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,393 @@
|
||||
#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.05f;
|
||||
light.shadows = LightShadows.Soft;
|
||||
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.55f, 0.7f, 0.85f);
|
||||
RenderSettings.ambientEquatorColor = new Color(0.5f, 0.52f, 0.48f);
|
||||
RenderSettings.ambientGroundColor = new Color(0.35f, 0.32f, 0.28f);
|
||||
|
||||
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 collectPath = "Assets/Bundles/Scenes";
|
||||
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(PackDirectory),
|
||||
FilterRuleName = nameof(CollectAll),
|
||||
AssetTags = "art",
|
||||
};
|
||||
BundleCollectorSettingData.CreateCollector(group, collector);
|
||||
}
|
||||
else
|
||||
{
|
||||
collector.CollectorGUID = AssetDatabase.AssetPathToGUID(collectPath);
|
||||
collector.AddressRuleName = nameof(AddressByFileName);
|
||||
collector.PackRuleName = nameof(PackDirectory);
|
||||
collector.FilterRuleName = nameof(CollectAll);
|
||||
BundleCollectorSettingData.ModifyCollector(group, collector);
|
||||
}
|
||||
|
||||
package.CheckConfigError();
|
||||
BundleCollectorSettingData.SaveFile();
|
||||
return $"art collect={collectPath}";
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,18 @@
|
||||
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,35 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user