#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; /// /// 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. /// 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 FormalPets = new Dictionary { [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 (潮汐鲸)" }; /// /// Executes portrait re-rendering and generates a visual comparison sheet. /// 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(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(true)) { trans.gameObject.layer = 30; } // Calculate renderer bounds var renderers = instance.GetComponentsInChildren(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(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.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(); 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(); 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