#if UNITY_EDITOR using System.IO; using UnityEditor; using UnityEngine; using UnityEngine.Rendering; using YooAsset.Editor; /// /// M3:角色目录约定 Assets/Bundles/Characters/{ActorName}/ + Art 包收集。 /// public static class SetupM3Character { public const string ActorName = "MaleExplorer"; public const string ActorRoot = "Assets/Bundles/Characters/" + ActorName; const string FbxPath = ActorRoot + "/" + ActorName + ".fbx"; const string BasePath = ActorRoot + "/" + ActorName + "_base.png"; const string MixPath = ActorRoot + "/" + ActorName + "_mix.png"; const string ColorPath = ActorRoot + "/" + ActorName + "_color.png"; const string NormalPath = ActorRoot + "/" + ActorName + "_normal.png"; const string AoPath = ActorRoot + "/" + ActorName + "_ao.png"; const string MetallicPath = ActorRoot + "/" + ActorName + "_metallic.png"; const string RoughnessPath = ActorRoot + "/" + ActorName + "_roughness.png"; const string ConceptPath = ActorRoot + "/concept.png"; const string MatPath = ActorRoot + "/" + ActorName + ".mat"; const string PrefabPath = ActorRoot + "/" + ActorName + ".prefab"; const string ArtPackage = "Art"; const string XPbrShaderName = "Test/XPbr"; [MenuItem("AIGame/M3/Setup Character Prefab + Art Collector")] public static string SetupAll() { AssetDatabase.Refresh(); var a = EnsureTextureImports(); var b = EnsureMaterialAndPrefab(); var c = EnsureArtCollector(); CleanupLegacyLayout(); AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); return $"ok textures={a} prefab={b} art={c}"; } public static string EnsureTextureImports() { var paths = new[] { BasePath, MixPath, ColorPath, NormalPath, AoPath, MetallicPath, RoughnessPath, ConceptPath }; var ok = 0; foreach (var path in paths) { if (!File.Exists(path)) continue; var importer = AssetImporter.GetAtPath(path) as TextureImporter; if (importer == null) continue; var isMix = path == MixPath; var isNormal = path == NormalPath; var isData = path == MetallicPath || path == RoughnessPath || path == AoPath || isMix; importer.textureType = isNormal ? TextureImporterType.NormalMap : TextureImporterType.Default; importer.sRGBTexture = !isNormal && !isData; importer.mipmapEnabled = true; importer.alphaIsTransparency = path == ConceptPath || path == BasePath; importer.SaveAndReimport(); ok++; } if (File.Exists(FbxPath)) { var model = AssetImporter.GetAtPath(FbxPath) as ModelImporter; if (model != null) { // XPbr:保留 externalObjects 材质重映射;无 .mat 时再 None model.materialImportMode = File.Exists(MatPath) ? ModelImporterMaterialImportMode.ImportViaMaterialDescription : ModelImporterMaterialImportMode.None; model.meshCompression = ModelImporterMeshCompression.Medium; model.isReadable = false; model.SaveAndReimport(); } } return $"imported={ok} fbx={(File.Exists(FbxPath) ? "yes" : "no")}"; } public static string EnsureMaterialAndPrefab() { if (File.Exists(FbxPath) && (File.Exists(BasePath) || File.Exists(ColorPath))) return EnsureMeshPrefab(); if (File.Exists(ConceptPath)) return EnsureBillboardPrefab(); return "missing MaleExplorer mesh/concept under " + ActorRoot; } static string EnsureMeshPrefab() { var baseMap = AssetDatabase.LoadAssetAtPath(BasePath); var mixMap = AssetDatabase.LoadAssetAtPath(MixPath); var color = baseMap != null ? baseMap : AssetDatabase.LoadAssetAtPath(ColorPath); if (color == null) return "base/color texture missing"; var xpbr = Shader.Find(XPbrShaderName); var urpLit = Shader.Find("Universal Render Pipeline/Lit") ?? Shader.Find("Standard"); var useXpbr = xpbr != null && mixMap != null; var shader = useXpbr ? xpbr : urpLit; var mat = AssetDatabase.LoadAssetAtPath(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", color); if (mat.HasProperty("_MixTex")) mat.SetTexture("_MixTex", mixMap); if (mat.HasProperty("_BaseColor")) mat.SetColor("_BaseColor", Color.white); if (mat.HasProperty("_Metallic")) mat.SetFloat("_Metallic", 1f); if (mat.HasProperty("_Roughness")) mat.SetFloat("_Roughness", 1f); if (mat.HasProperty("_AO")) mat.SetFloat("_AO", 1f); if (mat.HasProperty("_AlphaTest")) mat.SetFloat("_AlphaTest", 0f); mat.DisableKeyword("_ALPHATEST_ON"); } else { var normal = AssetDatabase.LoadAssetAtPath(NormalPath); var ao = AssetDatabase.LoadAssetAtPath(AoPath); var metallic = AssetDatabase.LoadAssetAtPath(MetallicPath); var roughness = AssetDatabase.LoadAssetAtPath(RoughnessPath); if (mat.HasProperty("_BaseMap")) mat.SetTexture("_BaseMap", color); if (mat.HasProperty("_MainTex")) mat.SetTexture("_MainTex", color); if (mat.HasProperty("_BumpMap") && normal != null) { mat.SetTexture("_BumpMap", normal); mat.EnableKeyword("_NORMALMAP"); } if (mat.HasProperty("_OcclusionMap") && ao != null) mat.SetTexture("_OcclusionMap", ao); if (mat.HasProperty("_MetallicGlossMap") && metallic != null) { mat.SetTexture("_MetallicGlossMap", metallic); mat.EnableKeyword("_METALLICSPECGLOSSMAP"); } if (mat.HasProperty("_Smoothness") && roughness != null) mat.SetFloat("_Smoothness", 0.45f); if (mat.HasProperty("_BaseColor")) mat.SetColor("_BaseColor", Color.white); } EditorUtility.SetDirty(mat); var modelRoot = AssetDatabase.LoadAssetAtPath(FbxPath); if (modelRoot == null) return "fbx not loaded"; var instance = (GameObject)PrefabUtility.InstantiatePrefab(modelRoot); instance.name = ActorName; foreach (var renderer in instance.GetComponentsInChildren()) renderer.sharedMaterial = mat; // 落地参考盘 var ground = GameObject.CreatePrimitive(PrimitiveType.Cylinder); ground.name = "ShadowDisc"; ground.transform.SetParent(instance.transform, false); ground.transform.localPosition = new Vector3(0f, 0.02f, 0f); ground.transform.localScale = new Vector3(0.7f, 0.015f, 0.7f); Object.DestroyImmediate(ground.GetComponent()); var gMat = new Material(urpLit != null ? urpLit : shader); if (gMat.HasProperty("_BaseColor")) gMat.SetColor("_BaseColor", new Color(0.12f, 0.12f, 0.14f, 1f)); if (gMat.HasProperty("_Color")) gMat.color = new Color(0.12f, 0.12f, 0.14f, 1f); ground.GetComponent().sharedMaterial = gMat; ground.GetComponent().shadowCastingMode = ShadowCastingMode.Off; // 粗略归一化身高约 1.8m var bounds = new Bounds(instance.transform.position, Vector3.zero); var renderers = instance.GetComponentsInChildren(); foreach (var r in renderers) { if (r.name == "ShadowDisc") continue; bounds.Encapsulate(r.bounds); } if (bounds.size.y > 0.01f) { var scale = 1.8f / bounds.size.y; instance.transform.localScale = Vector3.one * scale; } PrefabUtility.SaveAsPrefabAsset(instance, PrefabPath); Object.DestroyImmediate(instance); return PrefabPath; } static string EnsureBillboardPrefab() { var tex = AssetDatabase.LoadAssetAtPath(ConceptPath); if (tex == null) return "concept not loaded"; var shader = Shader.Find("Universal Render Pipeline/Unlit"); if (shader == null) shader = Shader.Find("Unlit/Transparent"); if (shader == null) shader = Shader.Find("Sprites/Default"); var mat = AssetDatabase.LoadAssetAtPath(MatPath); if (mat == null) { mat = new Material(shader); AssetDatabase.CreateAsset(mat, MatPath); } else mat.shader = shader; if (mat.HasProperty("_BaseMap")) mat.SetTexture("_BaseMap", tex); if (mat.HasProperty("_MainTex")) mat.SetTexture("_MainTex", tex); if (mat.HasProperty("_BaseColor")) mat.SetColor("_BaseColor", Color.white); if (mat.HasProperty("_Surface")) mat.SetFloat("_Surface", 1f); mat.EnableKeyword("_SURFACE_TYPE_TRANSPARENT"); mat.renderQueue = (int)RenderQueue.Transparent; EditorUtility.SetDirty(mat); var root = new GameObject(ActorName); root.transform.localScale = new Vector3(1.2f, 2.0f, 1f); var quad = GameObject.CreatePrimitive(PrimitiveType.Quad); quad.name = "Billboard"; quad.transform.SetParent(root.transform, false); quad.transform.localPosition = new Vector3(0f, 1.0f, 0f); Object.DestroyImmediate(quad.GetComponent()); quad.GetComponent().sharedMaterial = mat; PrefabUtility.SaveAsPrefabAsset(root, PrefabPath); Object.DestroyImmediate(root); return PrefabPath + " (billboard fallback)"; } static void CleanupLegacyLayout() { // 旧 M3 扁平目录,迁移后删除空壳 var legacy = new[] { "Assets/Bundles/Characters/Prefabs/MaleExplorer.prefab", "Assets/Bundles/Characters/Textures/male_explorer.png", "Assets/Bundles/Characters/Textures/MaleExplorer_Billboard.mat", }; foreach (var path in legacy) { if (File.Exists(path) || AssetDatabase.LoadMainAssetAtPath(path) != null) AssetDatabase.DeleteAsset(path); } } public 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 == "Characters"); if (group == null) group = BundleCollectorSettingData.CreateGroup(package, "Characters"); group.ActiveRuleName = nameof(EnableGroup); group.AssetTags = "art"; BundleCollectorSettingData.ModifyGroup(package, group); const string collectPath = "Assets/Bundles/Characters"; 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 $"package={ArtPackage} collect={collectPath} pack=PackDirectory"; } } #endif