


๋ธ๋ ๋ ์ํธ์ํฌ ๋ธ๋ก๊ทธ
์ธ์คํํฐ์์ ํ ๋นํ ์ด๋ฏธ์ง์ Import Settings์์ Read/Write Enabled๋ฅผ ์ฒดํฌํด์ผ GetPixel()์ด ๋์ํฉ๋๋ค.
ํผ๋ฒ ์คํ์
์ฒ๋ฆฌ: ๋ธ๋ก๊ทธ ์๋ณธ์ฒ๋ผ ํ์ด ํ์ชฝ ๋ชจ์๋ฆฌ๋ฅผ ์ถ์ผ๋ก ๋ค์งํ๋๋ก pivot ๋ณด์ ์คํ์
์ Animate()์์ ๊ณ์ฐํฉ๋๋ค. plateSize * 0.5f ๊ฐ์ด ๊ทธ ๊ธฐ์ค์
๋๋ค.
using System.Collections.Generic;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif
[ExecuteAlways]
public class PixelLuminanceFlip : MonoBehaviour
{
enum RenderPipeline { BuiltIn, URP, HDRP }
struct PlateData
{
public float targetAngle; // luminance์์ ๊ณ์ฐํ ๋ชฉํ ํ์ ๊ฐ (degrees)
public float startDelay; // ์ฌ์ดํด ๋ด ๋๋ค ์์ ์ง์ฐ
public Color color; // ์ด๋ฏธ์ง์์ ์ํ๋งํ ํฝ์
์์
public Vector3 localPos; // ๊ทธ๋ฆฌ๋ ๋ก์ปฌ ์์น
}
// =========================================================
[Header("Image")]
[SerializeField, Tooltip("ํฝ์
luminance๋ฅผ ๋ถ์ํ ์์ค ์ด๋ฏธ์ง. Import Settings > Read/Write Enabled ํ์.")]
Texture2D sourceImage;
[SerializeField, Tooltip("true: ์ด๋์ธ์๋ก ํฐ ๊ฐ๋ / false: ๋ฐ์์๋ก ํฐ ๊ฐ๋")]
bool invertLuminance = true;
[Header("Grid")]
[SerializeField] int gridSize = 30;
[SerializeField] float spacing = 0.52f;
[SerializeField] float plateSize = 0.5f;
[Header("Flip Animation")]
[SerializeField, Range(1f, 180f), Tooltip("์์ ํ ์ด๋์ด ํฝ์
์ ์ต๋ ํ์ ๊ฐ๋")]
float maxAngleDeg = 90f;
[SerializeField, Range(0.05f, 2f), Tooltip("๋ค์งํ๋ ๋ฐ ๊ฑธ๋ฆฌ๋ ์๊ฐ (์ด)")]
float flipDuration = 0.25f;
[SerializeField, Range(0f, 5f), Tooltip("๋ค์งํ ์ฑ ์ ์ง๋๋ ์๊ฐ (์ด)")]
float holdDuration = 0.8f;
[SerializeField, Range(0.5f, 10f), Tooltip("ํ ์ฌ์ดํด์ ์ ์ฒด ๊ธธ์ด (์ด)")]
float cycleInterval = 4f;
[SerializeField, Range(0f, 5f), Tooltip("ํ๋ค์ ๋๋ค ์์ ์ง์ฐ ์ต๋๊ฐ (์ด)")]
float delaySpread = 1.5f;
[SerializeField] int randomSeed = 42;
[Header("Color")]
[SerializeField, Tooltip("true: ์ด๋ฏธ์ง ํฝ์
์์ / false: ๋จ์")]
bool useImageColor = true;
[SerializeField] Color overrideColor = Color.white;
[Header("Rendering")]
[SerializeField] Mesh instanceMesh;
[SerializeField] Material instanceMaterial;
// =========================================================
const int BATCH_SIZE = 1023;
readonly List<PlateData> _plates = new List<PlateData>();
Matrix4x4[] _matrices;
Vector4[] _colors;
bool _generated;
float _simTime;
RenderPipeline _pipeline;
// --- ๊ตฌ์กฐ ์บ์ ---
int _cachedGridSize;
float _cachedSpacing, _cachedPlateSize, _cachedMaxAngle, _cachedDelaySpread;
int _cachedSeed;
Texture2D _cachedImage;
#if UNITY_EDITOR
bool _generateQueued;
#endif
// =========================================================
void OnEnable()
{
_pipeline = DetectPipeline();
EnsureResources();
Generate();
CacheStructureParams();
}
void OnValidate()
{
gridSize = Mathf.Max(2, gridSize);
spacing = Mathf.Max(0.01f, spacing);
plateSize = Mathf.Max(0.01f, plateSize);
flipDuration = Mathf.Max(0.01f, flipDuration);
holdDuration = Mathf.Max(0f, holdDuration);
delaySpread = Mathf.Max(0f, delaySpread);
// cycleInterval์ flipOut + hold + flipBack + ์ฌ์ ๋ณด๋ค ์ปค์ผ ํจ
cycleInterval = Mathf.Max(flipDuration * 2f + holdDuration + 0.1f, cycleInterval);
#if UNITY_EDITOR
_pipeline = DetectPipeline();
EnsureResources();
if (StructureParamsChanged())
{
CacheStructureParams();
QueueGenerate();
}
else
{
Animate(Application.isPlaying ? _simTime : 0f);
}
#endif
}
void Update()
{
if (!_generated) return;
if (!Application.isPlaying)
{
Animate(0f);
RenderInstances();
return;
}
_simTime += Time.deltaTime;
Animate(_simTime);
RenderInstances();
}
// =========================================================
// ๊ธฐ๋ณธ ๋ฉ์๋ฅผ Cube๋ก ์์ฑ. ์ปค์คํ
์
ฐ์ด๋ ์์ผ๋ฉด URP Unlit ํด๋ฐฑ.
void EnsureResources()
{
if (instanceMesh == null)
{
GameObject temp = GameObject.CreatePrimitive(PrimitiveType.Cube);
instanceMesh = temp.GetComponent<MeshFilter>().sharedMesh;
DestroyImmediate(temp);
}
if (instanceMaterial == null)
{
Shader shader =
Shader.Find("Custom/SpiralVortexUnlit") ??
Shader.Find("Universal Render Pipeline/Unlit") ??
Shader.Find("Standard");
instanceMaterial = new Material(shader);
instanceMaterial.enableInstancing = true;
}
}
// ์ด๋ฏธ์ง luminance ๋ถ์ ํ ํ๋ ์ดํธ ๋ฐฐ์ด ์์ฑ
void Generate()
{
_plates.Clear();
_generated = false;
_simTime = 0f;
Random.InitState(randomSeed);
float[] luminances = SampleLuminances();
NormalizeLuminances(luminances, out float[] normalized);
float totalW = (gridSize - 1) * spacing;
float totalH = (gridSize - 1) * spacing;
for (int y = 0; y < gridSize; y++)
{
for (int x = 0; x < gridSize; x++)
{
int idx = y * gridSize + x;
float t = invertLuminance ? 1f - normalized[idx] : normalized[idx];
_plates.Add(new PlateData
{
targetAngle = t * maxAngleDeg,
startDelay = Random.Range(0f, delaySpread),
color = useImageColor ? SampleColor(x, y) : overrideColor,
localPos = new Vector3(
x * spacing - totalW * 0.5f,
0f,
y * spacing - totalH * 0.5f)
});
}
}
_matrices = new Matrix4x4[_plates.Count];
_colors = new Vector4[_plates.Count];
_generated = true;
Animate(0f);
}
// =========================================================
// ๋งค ํ๋ ์ ๊ฐ ํ๋ ์ดํธ์ ํ์ ๊ฐ์ SmoothStep์ผ๋ก ๋ณด๊ฐ
void Animate(float t)
{
if (_matrices == null || _colors == null) return;
float cycleT = t % cycleInterval; // ์ ์ฒด ์ฌ์ดํด ๋ด ํ์ฌ ์์น
for (int i = 0; i < _plates.Count; i++)
{
PlateData plate = _plates[i];
float localT = cycleT - plate.startDelay; // ํ๋ ์ดํธ ๊ฐ๋ณ ํ์ด๋จธ
float angle = 0f;
if (localT > 0f)
{
float tFlipOut = flipDuration;
float tHold = tFlipOut + holdDuration;
float tFlipBack = tHold + flipDuration;
if (localT < tFlipOut) angle = Mathf.SmoothStep(0f, plate.targetAngle, localT / flipDuration);
else if (localT < tHold) angle = plate.targetAngle;
else if (localT < tFlipBack) angle = Mathf.SmoothStep(plate.targetAngle, 0f, (localT - tHold) / flipDuration);
// tFlipBack ์ดํ: angle = 0 (ํ๋ฉด ์ ์ง)
}
// ํผ๋ฒ์ ์ ๋ชจ์๋ฆฌ๋ก ์ด๋ํด์ ํ์ (๋ฌธ์ฒ๋ผ ๋ค์งํ๋ ๋๋)
// ๋ก์ปฌ: ํ์ ์ ์ค์ฌ์ Z ๋ฐฉํฅ์ผ๋ก plateSize/2 ์์ผ๋ก ์ด๋ ํ ํ์ , ๋ค์ ๋ณต์
Quaternion rot = Quaternion.Euler(angle, 0f, 0f);
Vector3 pivot = new Vector3(0f, 0f, plateSize * 0.5f);
Vector3 offset = rot * -pivot + pivot; // ํผ๋ฒ ๋ณด์ ์คํ์
Vector3 worldPos = transform.TransformPoint(plate.localPos + offset);
Quaternion worldRot = transform.rotation * rot;
Vector3 scale = new Vector3(plateSize, plateSize * 0.1f, plateSize);
_matrices[i] = Matrix4x4.TRS(worldPos, worldRot, scale);
Color c = plate.color;
_colors[i] = new Vector4(c.r, c.g, c.b, c.a);
}
}
// 1023๊ฐ ๋จ์ ๋ฐฐ์น ๋๋ก์ฐ
void RenderInstances()
{
if (instanceMesh == null || instanceMaterial == null) return;
string colorProp = GetColorPropertyName();
int total = _plates.Count;
int index = 0;
while (total > 0)
{
int batch = Mathf.Min(BATCH_SIZE, total);
Matrix4x4[] batchMatrices = new Matrix4x4[batch];
Vector4[] batchColors = new Vector4[batch];
System.Array.Copy(_matrices, index, batchMatrices, 0, batch);
System.Array.Copy(_colors, index, batchColors, 0, batch);
MaterialPropertyBlock block = new MaterialPropertyBlock();
block.SetVectorArray(colorProp, batchColors);
Graphics.DrawMeshInstanced(
instanceMesh, 0, instanceMaterial,
batchMatrices, batch, block
);
index += batch;
total -= batch;
}
}
// =========================================================
// ๊ทธ๋ฆฌ๋ ์ ์ฒด ์ขํ์ luminance๋ฅผ ์ด๋ฏธ์ง์์ ์ํ๋ง (ITU-R BT.709)
float[] SampleLuminances()
{
float[] result = new float[gridSize * gridSize];
if (sourceImage == null)
{
for (int i = 0; i < result.Length; i++) result[i] = 0.5f;
return result;
}
int w = sourceImage.width;
int h = sourceImage.height;
for (int y = 0; y < gridSize; y++)
{
for (int x = 0; x < gridSize; x++)
{
int px = Mathf.RoundToInt((float)x / (gridSize - 1) * (w - 1));
int py = Mathf.RoundToInt((float)y / (gridSize - 1) * (h - 1));
Color c = sourceImage.GetPixel(px, py);
result[y * gridSize + x] = 0.2126f * c.r + 0.7152f * c.g + 0.0722f * c.b;
}
}
return result;
}
// luminance ๋ฐฐ์ด์ 0~1๋ก ์ ๊ทํ
void NormalizeLuminances(float[] src, out float[] dst)
{
dst = new float[src.Length];
float minL = float.MaxValue, maxL = float.MinValue;
foreach (float l in src) { minL = Mathf.Min(minL, l); maxL = Mathf.Max(maxL, l); }
float range = Mathf.Max(maxL - minL, 0.0001f);
for (int i = 0; i < src.Length; i++) dst[i] = (src[i] - minL) / range;
}
// ๊ทธ๋ฆฌ๋ ์ขํ์ ๋์ํ๋ ์ด๋ฏธ์ง ํฝ์
์์ ๋ฐํ
Color SampleColor(int gx, int gy)
{
if (sourceImage == null) return Color.white;
int px = Mathf.RoundToInt((float)gx / (gridSize - 1) * (sourceImage.width - 1));
int py = Mathf.RoundToInt((float)gy / (gridSize - 1) * (sourceImage.height - 1));
return sourceImage.GetPixel(px, py);
}
// =========================================================
RenderPipeline DetectPipeline()
{
var rp = UnityEngine.Rendering.GraphicsSettings.currentRenderPipeline;
if (rp == null) return RenderPipeline.BuiltIn;
string name = rp.GetType().Name;
if (name.Contains("Universal")) return RenderPipeline.URP;
if (name.Contains("HighDefinition") || name.Contains("HDRP")) return RenderPipeline.HDRP;
return RenderPipeline.BuiltIn;
}
string GetColorPropertyName() => _pipeline switch
{
RenderPipeline.URP => "_BaseColor",
RenderPipeline.HDRP => "_BaseColor",
_ => "_Color"
};
bool StructureParamsChanged() =>
_cachedGridSize != gridSize ||
_cachedImage != sourceImage ||
!Mathf.Approximately(_cachedSpacing, spacing) ||
!Mathf.Approximately(_cachedPlateSize, plateSize) ||
!Mathf.Approximately(_cachedMaxAngle, maxAngleDeg) ||
!Mathf.Approximately(_cachedDelaySpread, delaySpread) ||
_cachedSeed != randomSeed;
void CacheStructureParams()
{
_cachedGridSize = gridSize;
_cachedSpacing = spacing;
_cachedPlateSize = plateSize;
_cachedMaxAngle = maxAngleDeg;
_cachedDelaySpread = delaySpread;
_cachedSeed = randomSeed;
_cachedImage = sourceImage;
}
#if UNITY_EDITOR
void QueueGenerate()
{
if (_generateQueued) return;
_generateQueued = true;
EditorApplication.delayCall += () =>
{
_generateQueued = false;
if (this == null) return;
Generate();
};
}
#endif
}