๐ŸซงArt_020 Pixel Luminance Flip

BamgasiJMยท2026๋…„ 3์›” 30์ผ

Unity GenArt

๋ชฉ๋ก ๋ณด๊ธฐ
31/41
post-thumbnail

๋ธ”๋ Œ๋” ์•„ํŠธ์›Œํฌ ๋ธ”๋กœ๊ทธ

ํ•ต์‹ฌ ๋กœ์ง

  • ์ด๋ฏธ์ง€ ํ”ฝ์…€ โ†’ luminance ๊ณ„์‚ฐ (ITU-R BT.709)
  • luminance โ†’ ํšŒ์ „๊ฐ ๋งคํ•‘ (์–ด๋‘์šธ์ˆ˜๋ก ํฐ ๊ฐ๋„)
  • ํŒ๋“ค์ด ๋žœ๋ค ๋”œ๋ ˆ์ด๋กœ ๋’ค์ง‘ํ˜”๋‹ค ๋Œ์•„์˜ค๋Š” ์‚ฌ์ดํด ๋ฐ˜๋ณต
  • ์ธ์ŠคํŽ™ํ„ฐ์—์„œ ํ• ๋‹นํ•  ์ด๋ฏธ์ง€์˜ Import Settings์—์„œ Read/Write Enabled๋ฅผ ์ฒดํฌํ•ด์•ผ GetPixel()์ด ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค.

  • ํ”ผ๋ฒ— ์˜คํ”„์…‹ ์ฒ˜๋ฆฌ: ๋ธ”๋กœ๊ทธ ์›๋ณธ์ฒ˜๋Ÿผ ํŒ์ด ํ•œ์ชฝ ๋ชจ์„œ๋ฆฌ๋ฅผ ์ถ•์œผ๋กœ ๋’ค์ง‘ํžˆ๋„๋ก pivot ๋ณด์ • ์˜คํ”„์…‹์„ Animate()์—์„œ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. plateSize * 0.5f ๊ฐ’์ด ๊ทธ ๊ธฐ์ค€์ž…๋‹ˆ๋‹ค.

์ฝ”๋“œ

๐Ÿ“’PixelLuminanceFlip.cs

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
}

profile
Coding Art with Blender / oF / Processing / p5.js / nannou

0๊ฐœ์˜ ๋Œ“๊ธ€