Created
May 28, 2022 11:22
-
-
Save Marc-Ducret/e9d3497645302944df18566891a69fe7 to your computer and use it in GitHub Desktop.
Unity Automatic 3D Icons Rendering
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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