Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Unity Automatic 3D Icons Rendering
using System.Collections.Generic;
using DiceKingdoms.Game;
using DiceKingdoms.Interface;
using Unity.Mathematics;
using UnityEditor;
using UnityEditor.U2D;
using UnityEngine;
using UnityEngine.Experimental.Rendering;
using UnityEngine.U2D;
using Utility;
namespace DiceKingdoms.Meta {
public class IconRenderer : MonoBehaviour {
[Header("References")]
[SerializeField]
private new Camera camera;
[SerializeField]
private Transform subjectHolder;
[SerializeField]
private PlayerColors playerColors;
[SerializeField]
private SpriteAtlas buildingIconAtlas;
[Header("Constants")]
[SerializeField]
private float distanceFactor;
[SerializeField]
private float translationAdjustment;
[SerializeField]
private float zoomAdjustment;
[SerializeField]
private float marginRatio;
[SerializeField]
private int iterations;
[SerializeField]
private int adjustSize;
[SerializeField]
private int renderSize;
[SerializeField]
private int renderMultiSample;
[Header("Data")]
[SerializeField]
private string buildingIconsFolder;
#if UNITY_EDITOR
[ContextMenu("Generate Building Icons")]
private void GenerateBuildingIcons() {
playerColors.Init();
List<Object> icons = new();
foreach (string guid in AssetDatabase.FindAssets("t:Prefab")) {
string assetPath = AssetDatabase.GUIDToAssetPath(guid);
GameObject prefab = PrefabUtility.LoadPrefabContents(assetPath);
if (prefab.GetComponent<BuildingVisual>() is { } buildingVisual &&
buildingVisual.GetComponent<SkinVariant>() is { }) {
buildingVisual.icons = RenderSubjectPrefab(buildingVisual.transform,
$"Assets/{buildingIconsFolder}/building-icon-{buildingVisual.name.Replace(' ', '-').ToLower()}");
icons.AddRange(buildingVisual.icons);
PrefabUtility.SaveAsPrefabAsset(prefab, assetPath);
}
PrefabUtility.UnloadPrefabContents(prefab);
}
buildingIconAtlas.Remove(buildingIconAtlas.GetPackables());
buildingIconAtlas.Add(icons.ToArray());
AssetDatabase.SaveAssetIfDirty(buildingIconAtlas);
}
[ContextMenu("Test Camera Adjust")]
private void TestCameraAdjust() {
playerColors.Init();
RenderSubject(subjectHolder, true);
}
private static Texture2D DownSample(Texture2D input, int factor) {
Texture2D output = new(input.width / factor, input.height / factor, input.graphicsFormat,
TextureCreationFlags.None);
for (int outputY = 0; outputY < output.height; outputY++)
for (int outputX = 0; outputX < output.width; outputX++) {
Color sum = Color.clear;
for (int offsetY = 0; offsetY < factor; offsetY++)
for (int offsetX = 0; offsetX < factor; offsetX++)
sum += input.GetPixel(factor * outputX + offsetX, factor * outputY + offsetY);
Color average = sum / IntMath.Sq(factor);
Color corrected = average;
if (average.a > 0) {
corrected /= average.a;
corrected.a = average.a;
}
output.SetPixel(outputX, outputY, corrected);
}
return output;
}
private static bool IsApproximatelyEqual(Texture2D lhs, Texture2D rhs) {
if (lhs == null || rhs == null) return false;
if (lhs.width != rhs.width || lhs.height != rhs.height) return false;
float4 Cast(Color c) => new(c.r, c.g, c.b, c.a);
const float colorThreshold = .1f;
const int pixelCountThreshold = 16;
Color[] lhsPixels = lhs.GetPixels();
Color[] rhsPixels = rhs.GetPixels();
int count = 0;
for (int i = 0; i < lhsPixels.Length; i++) {
if (lhsPixels[i].a == 0 && rhsPixels[i].a == 0)
continue;
float difference = math.cmax(math.abs(Cast(lhsPixels[i]) - Cast(rhsPixels[i])));
if (difference >= colorThreshold
&& ++count >= pixelCountThreshold)
return false;
}
return true;
}
private Texture2D[] RenderSubject(Transform subject, bool debug = false) {
Texture2D Render(int resolution) {
RenderTextureDescriptor rtd = new(resolution, resolution) {
depthBufferBits = 24,
useMipMap = false,
sRGB = true,
colorFormat = RenderTextureFormat.ARGB32
};
RenderTexture rt = new(rtd);
rt.Create();
camera.targetTexture = rt;
camera.Render();
camera.targetTexture = null;
RenderTexture.active = rt;
Texture2D texture = new(rt.width, rt.height, TextureFormat.RGBA32, false, false);
texture.ReadPixels(new Rect(0, 0, rt.width, rt.height), 0, 0);
RenderTexture.active = null;
DestroyImmediate(rt);
return texture;
}
(int2, int2) Bounds(Texture2D texture) {
int2 min = new(texture.width, texture.height);
int2 max = new(-1, -1);
for (int y = 0; y < texture.height; y++)
for (int x = 0; x < texture.width; x++) {
if (texture.GetPixel(x, y).a > 0) {
min = math.min(min, new int2(x, y));
max = math.max(max, new int2(x, y));
}
}
return (min, max);
}
Bounds bounds = new(subject.position, Vector3.zero);
foreach (Renderer renderer in subject.GetComponentsInChildren<Renderer>())
bounds.Encapsulate(renderer.bounds);
float radius = math.cmax(bounds.size);
float distance = distanceFactor * radius / math.sin(math.radians(camera.fieldOfView) / 2);
camera.transform.position = bounds.center - distance * camera.transform.forward;
for (int i = 0; i < iterations; i++) {
Texture2D texture = Render(adjustSize);
float2 textureSize = new(texture.width, texture.height);
(int2 min, int2 max) = Bounds(texture);
float2 size = max - min;
float2 center = (min + max) / 2;
float2 offset = center - textureSize / 2;
camera.transform.position += translationAdjustment * radius *
(offset.x * camera.transform.right + offset.y * camera.transform.up);
float zoomOffset = 1 - math.cmax(size / (textureSize * (1 - marginRatio)));
camera.transform.position += zoomAdjustment * distance * (zoomOffset * camera.transform.forward);
if (debug)
Debug.Log(
$"Iteration {i}, Translation {math.length(offset):F1}, Zoom {math.length(zoomOffset):F2}");
}
bool hasPlayerColor;
{
ColoredPaperUtils.SetPlayerColor(subject, playerColors.Colors[0]);
Texture2D firstColor = Render(adjustSize);
ColoredPaperUtils.SetPlayerColor(subject, playerColors.Colors[1]);
Texture2D secondColor = Render(adjustSize);
hasPlayerColor = !IsApproximatelyEqual(firstColor, secondColor);
}
Texture2D[] textures = new Texture2D[hasPlayerColor ? playerColors.Colors.Length : 1];
for (int i = 0; i < textures.Length; i++) {
ColoredPaperUtils.SetPlayerColor(subject, playerColors.Colors[i]);
textures[i] = DownSample(Render(renderSize * renderMultiSample), renderMultiSample);
}
return textures;
}
private Sprite[] RenderSubjectPrefab(Transform subjectPrefab, string outputPathPrefix) {
Transform subject = Instantiate(subjectPrefab, subjectHolder);
Texture2D[] rendered = RenderSubject(subject);
Sprite[] sprites = new Sprite[playerColors.Colors.Length];
for (int i = 0; i < rendered.Length; i++) {
PlayerColor color = playerColors.Colors[i];
string outputPath = $"{outputPathPrefix}-{color.Name.Replace(' ', '-').ToLower()}.png";
if (!IsApproximatelyEqual(rendered[i], AssetDatabase.LoadAssetAtPath<Texture2D>(outputPath))) {
System.IO.File.WriteAllBytes(outputPath, rendered[i].EncodeToPNG());
AssetDatabase.ImportAsset(outputPath);
Debug.Log($"{outputPath} updated.");
}
sprites[i] = AssetDatabase.LoadAssetAtPath<Sprite>(outputPath);
}
for (int i = rendered.Length; i < sprites.Length; i++) sprites[i] = sprites[i - 1];
DestroyImmediate(subject.gameObject);
return sprites;
}
#endif
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment