Files

316 lines
12 KiB
C#

#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