316 lines
12 KiB
C#
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
|
|
|