Greetings everyone, and thank you for joining me in this exploration of optimizing sprite generation!
I’ve encountered a frustrating issue where my program freezes during the creation of a layered sprite object. The bottleneck appears to be within the nested loop responsible for “Creating & Blending,” which, according to timestamps, takes a whopping 9983ms.
My current understanding is that the process of copying and applying pixels is computationally expensive. Therefore, I’m eager to explore ways to optimize the operations within this nested loop.
However, I’m also curious about the limitations of certain approaches. Specifically, is it true that even utilizing techniques like coroutines or threading wouldn’t effectively address the program freeze?
Thank you for your time reading this!
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
using System.Diagnostics;
using System.IO;
using System.Text;
// This class, SpriteMaker, is used to generate a sprite from multiple texture layers
[RequireComponent(typeof(SpriteRenderer))]
public class SpriteMaker : MonoBehaviour
{
[System.Serializable]
public struct LayerData
{
public Texture2D texture;
public int offsetX;
public int offsetY;
public float scale;
}
public string m_SourceFolderName = "folder";
public LayerData[] layers;
public string m_SaveFolderName = "FinalPhoto";
//float scaleFactor = 10.0f;
public SceneLoad scene;
public void StartHere()
{
// Show the settings
ShowSettings();
genSprite();
}
public void genSprite()
{
// Start a timer
System.Diagnostics.Stopwatch stopwatch = System.Diagnostics.Stopwatch.StartNew();
long startTime;
// --- Section 1: Loading Textures ---
startTime = stopwatch.ElapsedMilliseconds;
string streamingAssetPath = string.IsNullOrEmpty(m_SourceFolderName) ? Application.streamingAssetsPath : Path.Combine(Application.streamingAssetsPath, m_SourceFolderName);
string[] fileNames = { "background.jpg", "A.jpg", "B.jpg", "C.jpg", "D.png" }; // Replace with your actual file names
for (int i = 0; i < Mathf.Min(layers.Length, fileNames.Length); i++)
{
string filePathSrc = Path.Combine(streamingAssetPath, fileNames[i]);
if (File.Exists(filePathSrc))
{
byte[] bytes = File.ReadAllBytes(filePathSrc);
Texture2D layerTexture = new Texture2D(2, 2);
layerTexture.LoadImage(bytes);
layers[i].texture = layerTexture;
}
else
{
UnityEngine.Debug.LogError($"File not found: {filePathSrc}");
// Consider handling the missing file case (e.g., using a default texture)
}
}
UnityEngine.Debug.Log($"Loading Textures: {stopwatch.ElapsedMilliseconds - startTime} ms");
// --- Section 2: Calculating Combined Texture Size ---
startTime = stopwatch.ElapsedMilliseconds;
// Calculate the size of the combined texture (taking offsets into account)
int maxWidth = 0;
int maxHeight = 0;
foreach (LayerData layerData in layers)
{
maxWidth = Mathf.Max(maxWidth, layerData.texture.width + layerData.offsetX);
maxHeight = Mathf.Max(maxHeight, layerData.texture.height + layerData.offsetY);
//UnityEngine.Debug.Log("W:" + layerData.texture.width + "H:" + layerData.texture.height);
}
UnityEngine.Debug.Log($"Calculating Size: {stopwatch.ElapsedMilliseconds - startTime} ms");
// --- Section 3: Creating and Blending Textures ---
startTime = stopwatch.ElapsedMilliseconds;
// Create a new texture
Texture2D combinedTexture = new Texture2D(maxWidth , maxHeight);
// Array to store the accumulated alpha values for each pixel
float[] accumulatedAlpha = new float[maxWidth * maxHeight];
// Iterate through each layer
for (int i = 0; i < layers.Length; i++)
{
Color[] layerPixels = layers[i].texture.GetPixels();
float layerScale = layers[i].scale;
for (int y = 0; y < layers[i].texture.height; y++)
{
for (int x = 0; x < layers[i].texture.width; x++)
{
int pixelIndex = (y * layers[i].texture.width) + x;
Color pixel = layerPixels[pixelIndex];
// Calculate the target position in the combined texture with offset
int targetX = Mathf.RoundToInt(x * layerScale) + layers[i].offsetX;
int targetY = Mathf.RoundToInt(y * layerScale) + layers[i].offsetY;
int targetIndex = (targetY * maxWidth) + targetX;
// Check if target position is within bounds
if (targetX >= 0 && targetX < maxWidth && targetY >= 0 && targetY < maxHeight)
{
// Calculate the blended color using accumulated alpha
float alpha = pixel.a;
float newAlpha = alpha + accumulatedAlpha[targetIndex] * (1 - alpha);
Color blendedColor = (pixel * alpha + combinedTexture.GetPixel(targetX, targetY) * accumulatedAlpha[targetIndex] * (1 - alpha)) / newAlpha;
combinedTexture.SetPixel(targetX, targetY, blendedColor);
accumulatedAlpha[targetIndex] = newAlpha;
}
}
}
}
combinedTexture.Apply();
UnityEngine.Debug.Log($"Creating & Blending: {stopwatch.ElapsedMilliseconds - startTime} ms");
// --- Section 4: Saving the Sprite ---
startTime = stopwatch.ElapsedMilliseconds;
// Create a new sprite from the combined texture
Sprite combinedSprite = Sprite.Create(combinedTexture, new Rect(0, 0, maxWidth, maxHeight), new Vector2(0.5f, 0.5f));
// Encode the combined sprite to JPG
byte[] jpgData = EncodeSpriteToJPG(combinedSprite);
// Save the JPG data to a file (example)
string dirPath = System.IO.Path.Combine(Application.streamingAssetsPath, m_SaveFolderName);
if (!System.IO.Directory.Exists(dirPath))
{
System.IO.Directory.CreateDirectory(dirPath);
}
string filePath = System.IO.Path.Combine(dirPath, "Result" + ".jpg");
System.IO.File.WriteAllBytes(filePath, jpgData);
UnityEngine.Debug.Log(jpgData.Length / 1024 + "Kb was saved as: " + filePath);
// Assign the combined sprite to a SpriteRenderer (assuming you have one in your scene)
GetComponent<SpriteRenderer>().sprite = combinedSprite;
// Scale down the GameObject
transform.localScale = new Vector3(1f, 1f, 1.0f);
UnityEngine.Debug.Log($"Saving Sprite: {stopwatch.ElapsedMilliseconds - startTime} ms");
// --- Total Processing Time ---
stopwatch.Stop();
TimeSpan elapsedTime = stopwatch.Elapsed;
UnityEngine.Debug.Log($"genSprite processing time: {elapsedTime.TotalMilliseconds} ms");
}
public static byte[] EncodeSpriteToJPG(Sprite sprite, int quality = 75)
{
Texture2D texture = sprite.texture;
Rect rect = sprite.rect;
// Create a temporary RenderTexture
RenderTexture renderTexture = RenderTexture.GetTemporary(texture.width, texture.height, 0, RenderTextureFormat.ARGB32);
Graphics.Blit(texture, renderTexture);
// Activate the RenderTexture
RenderTexture.active = renderTexture;
// Create a new Texture2D with the content of the RenderTexture
Texture2D tempTexture = new Texture2D(texture.width, texture.height, TextureFormat.RGB24, false);
tempTexture.ReadPixels(new Rect(0, 0, texture.width, texture.height), 0, 0);
tempTexture.Apply();
// Deactivate the RenderTexture
RenderTexture.active = null;
// Release the temporary RenderTexture
RenderTexture.ReleaseTemporary(renderTexture);
// Crop the texture based on the sprite's rect
Color[] pixels = tempTexture.GetPixels((int)rect.x, (int)rect.y, (int)rect.width, (int)rect.height);
Texture2D croppedTexture = new Texture2D((int)rect.width, (int)rect.height, TextureFormat.RGB24, false);
croppedTexture.SetPixels(pixels);
croppedTexture.Apply();
// Encode the cropped texture to JPG
byte[] jpgData = croppedTexture.EncodeToJPG(quality);
// Destroy the temporary texture
Destroy(tempTexture);
return jpgData;
}
}