Skip to content

Instantly share code, notes, and snippets.

@jean-moreno
Last active April 20, 2024 12:42
Show Gist options
  • Star 24 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save jean-moreno/724c5d04d619c55f0bcda433b053df5d to your computer and use it in GitHub Desktop.
Save jean-moreno/724c5d04d619c55f0bcda433b053df5d to your computer and use it in GitHub Desktop.
Texture Combiner utility for Unity: create an RGBA texture from 4 different input textures (note: place both files in an Editor folder)
using UnityEngine;
using UnityEditor;
using System.Collections.Generic;
public class TextureCombiner : EditorWindow
{
[MenuItem("Tools/Texture Combiner...")]
static void Open()
{
var w = GetWindow<TextureCombiner>(true, "Texture Combiner");
w.minSize = new Vector2(870, 420);
w.maxSize = new Vector2(870, 420);
}
public enum SaveFormat { PNG, EXR }
public enum Channel { R,G,B,A, RGBLuminance }
Texture2D textureR, textureG, textureB, textureA;
Channel sourceR, sourceG, sourceB, sourceA;
RenderTexture textureCombined;
RenderTexture textureCombinedAlpha;
Material blitMaterial;
Material blitMaterialAlpha;
SaveFormat saveFormat;
bool removeCompression = true;
bool removeCompressionPreview = false;
int textureSize;
string[] textureSizes = new string[] { "128", "256", "512", "1024", "2048", "4096", "Custom" };
int textureWidth = 128;
int textureHeight = 128;
Texture2D textureSaved;
void OnGUI()
{
GUILayout.Label("TEXTURE COMBINER", EditorStyles.boldLabel);
GUILayout.Space(8);
EditorGUI.BeginChangeCheck();
GUILayout.BeginHorizontal();
GUILayout.Label("Combined texture: ", GUILayout.ExpandWidth(false));
var newTexture = (Texture2D)EditorGUILayout.ObjectField(textureSaved, typeof(Texture2D), false);
GUILayout.EndHorizontal();
if(EditorGUI.EndChangeCheck() && newTexture != textureSaved)
{
if(Load(newTexture))
{
textureSaved = newTexture;
}
}
GUILayout.Space(8f);
float space = 8f;
var r = EditorGUILayout.GetControlRect(false, (64f + space) * 4f);
var texRect = r;
texRect.x += 24f;
texRect.height = 64f;
texRect.width = 64f;
textureR = (Texture2D)EditorGUI.ObjectField(texRect, textureR, typeof(Texture2D), false);
texRect.y += 64f + space;
textureG = (Texture2D)EditorGUI.ObjectField(texRect, textureG, typeof(Texture2D), false);
texRect.y += 64f + space;
textureB = (Texture2D)EditorGUI.ObjectField(texRect, textureB, typeof(Texture2D), false);
texRect.y += 64f + space;
textureA = (Texture2D)EditorGUI.ObjectField(texRect, textureA, typeof(Texture2D), false);
var lblRect = r;
lblRect.width = 20f;
lblRect.x += 4f;
lblRect.y += 22f;
GUI.Label(lblRect, "R", EditorStyles.largeLabel);
lblRect.y += 64f + space;
GUI.Label(lblRect, "G", EditorStyles.largeLabel);
lblRect.y += 64f + space;
GUI.Label(lblRect, "B", EditorStyles.largeLabel);
lblRect.y += 64f + space;
GUI.Label(lblRect, "A", EditorStyles.largeLabel);
var chanRect = r;
chanRect.width = 120f;
chanRect.x += texRect.x + texRect.width + space;
chanRect.height = 64f;
sourceR = (Channel)GUI_SourceChannel(chanRect, sourceR);
chanRect.y += 64f + space;
sourceG = (Channel)GUI_SourceChannel(chanRect, sourceG);
chanRect.y += 64f + space;
sourceB = (Channel)GUI_SourceChannel(chanRect, sourceB);
chanRect.y += 64f + space;
sourceA = (Channel)GUI_SourceChannel(chanRect, sourceA);
var resultRect = r;
resultRect.height = (64f + space) * 4f;
resultRect.x += lblRect.x + lblRect.width + texRect.width + chanRect.width + 64f;
resultRect.width = resultRect.height;
if(textureCombined != null || textureSaved != null)
{
var alphaRect = resultRect;
alphaRect.x += resultRect.width + space;
//handy way to highlight the saved texture when clicking on the big preview
if(textureSaved != null)
{
EditorGUI.ObjectField(resultRect, textureSaved, typeof(Texture2D), false);
EditorGUI.ObjectField(alphaRect, textureSaved, typeof(Texture2D), false);
}
if(textureCombined != null)
{
GUI.Box(resultRect, GUIContent.none);
GUI.Box(alphaRect, GUIContent.none);
//rgb
GUI.DrawTexture(resultRect, textureCombined, ScaleMode.StretchToFill, false, 0);
//alpha
GUI.DrawTexture(alphaRect, textureCombinedAlpha, ScaleMode.StretchToFill, false, 0);
}
}
else
{
resultRect.width += resultRect.width + space;
EditorGUI.HelpBox(resultRect, "texture not generated yet", MessageType.Warning);
}
GUILayout.Space(8f);
GUILayout.BeginHorizontal();
//Texture size
EditorGUI.BeginChangeCheck();
textureSize = EditorGUILayout.Popup(textureSize, textureSizes, GUILayout.Width(60f));
using(new EditorGUI.DisabledScope(textureSize != textureSizes.Length-1))
textureWidth = EditorGUILayout.IntField(textureWidth, GUILayout.Width(60f));
GUILayout.Label("x");
using(new EditorGUI.DisabledScope(textureSize != textureSizes.Length-1))
textureHeight = EditorGUILayout.IntField(textureHeight, GUILayout.Width(60f));
if(EditorGUI.EndChangeCheck())
{
textureWidth = Mathf.Clamp(textureWidth, 1, 16384);
textureHeight = Mathf.Clamp(textureHeight, 1, 16384);
TextureSizeUpdated();
}
GUILayout.FlexibleSpace();
//Save button
if(GUILayout.Button("SAVE AS...", GUILayout.Width(120f)))
{
SaveAs(saveFormat);
}
GUILayout.EndHorizontal();
//Options
GUILayout.BeginHorizontal();
saveFormat = (SaveFormat)EditorGUILayout.EnumPopup(saveFormat, GUILayout.Width(60f));
removeCompression = GUILayout.Toggle(removeCompression, new GUIContent("Remove compression (saved texture)", "Remove compression from input textures for the saved texture"), EditorStyles.miniButton);
removeCompressionPreview = GUILayout.Toggle(removeCompressionPreview, new GUIContent("Remove compression (preview)", "Remove compression from input textures for the preview image.\n\nThis is a separate setting because disabling/enabling back compression takes a few seconds and that can be annoying when regularly changing the inputs."), EditorStyles.miniButton);
GUILayout.FlexibleSpace();
//Reset button
if(GUILayout.Button("RESET", GUILayout.Width(120f)))
{
Reset();
}
GUILayout.EndHorizontal();
if(GUI.changed)
{
RefreshCombinedTexture(true);
}
}
void TextureSizeUpdated()
{
if(textureSize != textureSizes.Length-1)
{
textureWidth = int.Parse(textureSizes[textureSize]);
textureHeight = textureWidth;
}
UpdateRenderTextures(true);
}
void Reset()
{
OnDestroy();
sourceR = Channel.R;
sourceG = Channel.R;
sourceB = Channel.R;
sourceA = Channel.R;
textureR = null;
textureG = null;
textureB = null;
textureA = null;
textureSaved = null;
}
void UpdateRenderTextures(bool delete)
{
if(delete && textureCombined != null)
ClearRenderTexture(textureCombined);
if(delete && textureCombinedAlpha != null)
ClearRenderTexture(textureCombinedAlpha);
if(textureCombined == null)
{
textureCombined = new RenderTexture(textureWidth, textureHeight, 0, RenderTextureFormat.ARGB32, RenderTextureReadWrite.Default);
textureCombined.hideFlags = HideFlags.HideAndDontSave;
}
if(textureCombinedAlpha == null || (textureCombinedAlpha.width != textureCombined.width || textureCombinedAlpha.height != textureCombined.height))
{
textureCombinedAlpha = new RenderTexture(textureCombined);
textureCombinedAlpha.hideFlags = HideFlags.HideAndDontSave;
}
}
void RefreshCombinedTexture(bool preview)
{
UpdateRenderTextures(false);
if(blitMaterial == null)
{
blitMaterial = new Material(Shader.Find("Hidden/TextureCombiner"));
blitMaterial.name = "Texture Combine";
blitMaterial.hideFlags = HideFlags.HideAndDontSave;
}
if((!preview && removeCompression) || (preview && removeCompressionPreview))
{
//remove compression of source textures
RemoveCompression();
}
blitMaterial.SetTexture("_TexR", textureR);
blitMaterial.SetTexture("_TexG", textureG);
blitMaterial.SetTexture("_TexB", textureB);
blitMaterial.SetTexture("_TexA", textureA);
blitMaterial.SetFloat("_SrcR", (int)sourceR);
blitMaterial.SetFloat("_SrcG", (int)sourceG);
blitMaterial.SetFloat("_SrcB", (int)sourceB);
blitMaterial.SetFloat("_SrcA", (int)sourceA);
Graphics.Blit(null, textureCombined, blitMaterial, 0);
Graphics.Blit(textureCombined, textureCombinedAlpha, blitMaterial, 1);
//restore compression if necessary
RestoreCompression();
}
void OnDestroy()
{
if(textureCombined != null)
ClearRenderTexture(textureCombined);
if(textureCombinedAlpha != null)
ClearRenderTexture(textureCombinedAlpha);
if(blitMaterial != null)
DestroyImmediate(blitMaterial);
}
void ClearRenderTexture(RenderTexture rt)
{
rt.Release();
DestroyImmediate(rt);
}
int GUI_SourceChannel(Rect position, Channel channel)
{
var names = System.Enum.GetNames(typeof(Channel));
var r = position;
r.height /= names.Length;
for(int i = 0; i < names.Length; i++)
{
if(GUI.Toggle(r, (int)channel == i, names[i], EditorStyles.miniButton)) channel = (Channel)i;
r.y += r.height;
}
return (int)channel;
}
void SaveAs(SaveFormat format)
{
var path = EditorUtility.SaveFilePanelInProject("Save combined texture", "CombinedTexture", (format == SaveFormat.PNG) ? "png" : "exr", "Save combined texture as...");
if(!string.IsNullOrEmpty(path))
{
//save to file
var osPath = (Application.dataPath + path.Substring(6)).Replace('/', System.IO.Path.DirectorySeparatorChar);
//blit to render texture
RefreshCombinedTexture(false);
//set active render texture and read pixels
RenderTexture.active = textureCombined;
Texture2D texture2D = new Texture2D(textureCombined.width, textureCombined.height, (format == SaveFormat.PNG) ? TextureFormat.ARGB32 : TextureFormat.RGBAHalf, false);
texture2D.ReadPixels(new Rect(0, 0, textureCombined.width, textureCombined.height), 0, 0);
RenderTexture.active = null;
//save file to disk
byte[] data = (format == SaveFormat.PNG) ? texture2D.EncodeToPNG() : texture2D.EncodeToEXR(Texture2D.EXRFlags.CompressZIP);
System.IO.File.WriteAllBytes(osPath, data);
//import new file in Unity
AssetDatabase.ImportAsset(path);
//set metadata
var importer = AssetImporter.GetAtPath(path);
importer.userData = GetUserData();
importer.SaveAndReimport();
//load in UI and select in Project view
textureSaved = AssetDatabase.LoadAssetAtPath<Texture2D>(path);
Selection.objects = new Object[] { textureSaved };
}
}
Dictionary<TextureImporter, TextureImporterCompression> compressionSettings;
void RemoveCompression()
{
compressionSettings = new Dictionary<TextureImporter, TextureImporterCompression>();
CheckTextureCompression(textureR);
CheckTextureCompression(textureG);
CheckTextureCompression(textureB);
CheckTextureCompression(textureA);
}
void CheckTextureCompression(Texture2D texture)
{
if(texture != null)
{
var importer = AssetImporter.GetAtPath(AssetDatabase.GetAssetPath(texture)) as TextureImporter;
if(importer != null && importer.textureCompression != TextureImporterCompression.Uncompressed)
{
compressionSettings.Add(importer, importer.textureCompression);
importer.textureCompression = TextureImporterCompression.Uncompressed;
importer.SaveAndReimport();
}
}
}
void RestoreCompression()
{
if(compressionSettings != null && compressionSettings.Count > 0)
{
foreach(var kvp in compressionSettings)
{
kvp.Key.textureCompression = kvp.Value;
kvp.Key.SaveAndReimport();
}
}
compressionSettings = null;
}
bool Load(Texture2D texture)
{
if(texture == null)
return true;
var importer = AssetImporter.GetAtPath(AssetDatabase.GetAssetPath(texture));
if(importer != null)
{
if(importer.userData.StartsWith("texture_combiner"))
{
//no error check here!
//may break with different userData
var userDataSplit = importer.userData.Split(' ');
var rGuid = userDataSplit[1].Split(':')[1];
var gGuid = userDataSplit[2].Split(':')[1];
var bGuid = userDataSplit[3].Split(':')[1];
var aGuid = userDataSplit[4].Split(':')[1];
textureR = AssetDatabase.LoadAssetAtPath<Texture2D>(AssetDatabase.GUIDToAssetPath(rGuid));
textureG = AssetDatabase.LoadAssetAtPath<Texture2D>(AssetDatabase.GUIDToAssetPath(gGuid));
textureB = AssetDatabase.LoadAssetAtPath<Texture2D>(AssetDatabase.GUIDToAssetPath(bGuid));
textureA = AssetDatabase.LoadAssetAtPath<Texture2D>(AssetDatabase.GUIDToAssetPath(aGuid));
string errorGUID = "";
if(!string.IsNullOrEmpty(rGuid) && textureR == null)
{
errorGUID += "Red ";
}
if(!string.IsNullOrEmpty(gGuid) && textureG == null)
{
errorGUID += "Green ";
}
if(!string.IsNullOrEmpty(bGuid) && textureB == null)
{
errorGUID += "Blue ";
}
if(!string.IsNullOrEmpty(aGuid) && textureA == null)
{
errorGUID += "Alpha";
}
sourceR = (Channel)System.Enum.Parse(typeof(Channel), userDataSplit[5].Split(':')[1]);
sourceG = (Channel)System.Enum.Parse(typeof(Channel), userDataSplit[6].Split(':')[1]);
sourceB = (Channel)System.Enum.Parse(typeof(Channel), userDataSplit[7].Split(':')[1]);
sourceA = (Channel)System.Enum.Parse(typeof(Channel), userDataSplit[8].Split(':')[1]);
textureSaved = texture;
if(textureCombined != null)
{
textureCombined.Release();
DestroyImmediate(textureCombined);
}
if(textureCombinedAlpha != null)
{
textureCombinedAlpha.Release();
DestroyImmediate(textureCombinedAlpha);
}
if(!string.IsNullOrEmpty(errorGUID))
{
EditorUtility.DisplayDialog("Error", "Source texture(s) couldn't be found in the project:\n\n" + errorGUID + "\n\nMaybe they have been deleted, or they GUID has been updated?", "Ok");
}
return true;
}
else
{
ShowNotification(new GUIContent("This texture doesn't seem to have been generated with the Texture Combiner"));
}
}
return false;
}
string GetUserData()
{
return string.Format("texture_combiner r:{0} g:{1} b:{2} a:{3} rc:{4} gc:{5} bc:{6} ac:{7}",
AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(textureR)),
AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(textureG)),
AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(textureB)),
AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(textureA)),
sourceR,
sourceG,
sourceB,
sourceA
);
}
}
Shader "Hidden/TextureCombiner"
{
Properties
{
_MainTex ("source", 2D) = "black" {}
_TexR ("R", 2D) = "black" {}
_TexG ("G", 2D) = "black" {}
_TexB ("B", 2D) = "black" {}
_TexA ("A", 2D) = "white" {}
_SrcR ("R Source", Float) = 0
_SrcG ("G Source", Float) = 0
_SrcB ("B Source", Float) = 0
_SrcA ("A Source", Float) = 0
}
SubShader
{
Cull Off
ZWrite Off
ZTest Always
//PASS 0: Combine
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
return o;
}
sampler2D _TexR;
sampler2D _TexG;
sampler2D _TexB;
sampler2D _TexA;
float _SrcR;
float _SrcG;
float _SrcB;
float _SrcA;
inline float GetChannel(in float4 source, in float sourceChannel)
{
float r = 1;
switch(sourceChannel)
{
case 0: r = source.r; break;
case 1: r = source.g; break;
case 2: r = source.b; break;
case 3: r = source.a; break;
case 4: r = Luminance(source.rgb); break;
}
return r;
}
fixed4 frag (v2f i) : SV_Target
{
float4 r4 = tex2D(_TexR, i.uv);
float4 g4 = tex2D(_TexG, i.uv);
float4 b4 = tex2D(_TexB, i.uv);
float4 a4 = tex2D(_TexA, i.uv);
float r = GetChannel(r4, _SrcR);
float g = GetChannel(g4, _SrcG);
float b = GetChannel(b4, _SrcB);
float a = GetChannel(a4, _SrcA);
return float4(r,g,b,a);
}
ENDCG
}
//PASS 1: Get Alpha
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
return o;
}
sampler2D _MainTex;
fixed4 frag (v2f i) : SV_Target
{
float4 main = tex2D(_MainTex, i.uv);
return main.aaaa;
}
ENDCG
}
}
}
@jean-moreno
Copy link
Author

Screenshot

screenshot

@FleshMobProductions
Copy link

Works incredibly well! Thanks for sharing

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment