๐Ÿ“”Generative Art๋ฅผ ์œ„ํ•œ Boilerplate (ํŒŒ์ผ๋ถ„๋ฆฌ)

BamgasiJMยท2026๋…„ 4์›” 1์ผ

Unity GenArt

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

์ ์šฉ ๋ฐฉ๋ฒ•

GameObject์— BoilerplateGPUInstancing.cs ํ•˜๋‚˜๋งŒ ๋ถ™์ž…๋‹ˆ๋‹ค.

Hierarchy
โ””โ”€โ”€ GameObject (๋นˆ ์˜ค๋ธŒ์ ํŠธ)
        โ””โ”€โ”€ BoilerplateGPUInstancing  โ† ์ด๊ฒƒ๋งŒ Add Component

GPUInstanceRenderer์™€ GenArtResources๋Š” MonoBehaviour๊ฐ€ ์•„๋‹Œ ์ผ๋ฐ˜ C# ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. Unity ์ปดํฌ๋„ŒํŠธ ์‹œ์Šคํ…œ ๋ฐ”๊นฅ์— ์žˆ์œผ๋ฏ€๋กœ Inspector์— ๋ถ™์ผ ์ˆ˜ ์—†๊ณ , ๋ถ™์ผ ํ•„์š”๋„ ์—†์Šต๋‹ˆ๋‹ค.

BoilerplateGPUInstancing   โ† MonoBehaviour โ†’ GameObject์— ๋ถ™๋Š” ๊ฒƒ
        โ”‚ ์ƒ์†
GenArtBase                 โ† MonoBehaviour โ†’ ์ง์ ‘ ๋ถ™์ด์ง€ ์•Š์Œ (์ถ”์ƒ ํด๋ž˜์Šค)
        โ”‚ ์†Œ์œ  (new)
GPUInstanceRenderer        โ† ์ผ๋ฐ˜ ํด๋ž˜์Šค โ†’ ๋ถ™์ด๋Š” ๊ฐœ๋… ์ž์ฒด๊ฐ€ ์—†์Œ
        โ”‚ ํ˜ธ์ถœ (static)
GenArtResources            โ† static ํด๋ž˜์Šค โ†’ ๋ถ™์ด๋Š” ๊ฐœ๋… ์ž์ฒด๊ฐ€ ์—†์Œ

GenArtBase๋„ MonoBehaviour์ด๊ธด ํ•˜์ง€๋งŒ abstract์ด๊ธฐ ๋•Œ๋ฌธ์— Inspector์˜ Add Component ๋ชฉ๋ก์— ์•„์˜ˆ ๋‚˜ํƒ€๋‚˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. BoilerplateGPUInstancing์„ ๋ถ™์ด๋Š” ์ˆœ๊ฐ„ GenArtBase์˜ OnEnable, Update ๋“ฑ์ด ์ž๋™์œผ๋กœ ํ•จ๊ป˜ ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค.


์ฐธ์กฐ ๋ฐฉ์‹

Assets/
โ”œโ”€โ”€ BoilerplateGPUInstancing.cs     
โ”œโ”€โ”€ GenArtBase.cs           
โ”œโ”€โ”€ GenArtResources.cs
โ”œโ”€โ”€ GPUInstanceRenderer.cs 
โ””โ”€โ”€ GPUInstancingLit.shader

C#์€ ๊ฐ™์€ ์–ด์…ˆ๋ธ”๋ฆฌ(Assembly) ์•ˆ์— ์žˆ์œผ๋ฉด ์ž๋™์œผ๋กœ ์„œ๋กœ๋ฅผ ์ธ์‹ํ•ฉ๋‹ˆ๋‹ค.
Unity ํ”„๋กœ์ ํŠธ์—์„œ Assets/ ํด๋” ์•ˆ์˜ ๋ชจ๋“  .cs ํŒŒ์ผ์€ ๊ธฐ๋ณธ์ ์œผ๋กœ Assembly-CSharp ๋ผ๋Š” ํ•˜๋‚˜์˜ ์–ด์…ˆ๋ธ”๋ฆฌ๋กœ ์ปดํŒŒ์ผ๋ฉ๋‹ˆ๋‹ค. ๊ฐ™์€ ์–ด์…ˆ๋ธ”๋ฆฌ ์•ˆ์ด๋ฉด ๋ณ„๋„์˜ import ๊ฐœ๋… ์—†์ด ํด๋ž˜์Šค ์ด๋ฆ„๋งŒ์œผ๋กœ ๋ฐ”๋กœ ์ฐธ์กฐํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

GPUInstanceRenderer๋ฅผ MonoBehaviour๊ฐ€ ์•„๋‹Œ ์ผ๋ฐ˜ ํด๋ž˜์Šค๋กœ ๋งŒ๋“  ์ด์œ 

๋“œ๋กœ์šฐ ์ฝœ ์ „๋‹ด ๊ฐ์ฒด๋Š” Unity ์ด๋ฒคํŠธ๊ฐ€ ํ•„์š” ์—†์Šต๋‹ˆ๋‹ค. GenArtBase๊ฐ€ new GPUInstanceRenderer(mesh, mat)๋กœ ์†Œ์œ ํ•˜๋Š” ํ˜•ํƒœ๊ฐ€ ์ธ์ŠคํŽ™ํ„ฐ ๋…ธ์ถœ ์—†์ด ๊น”๋”ํ•ฉ๋‹ˆ๋‹ค. Inspector์—์„œ mesh/material์ด ๊ต์ฒด๋  ๋•Œ EnsureResources()์—์„œ ์ž๋™์œผ๋กœ ์žฌ๊ตฌ์„ฑ๋ฉ๋‹ˆ๋‹ค.

GenArtBase๊ฐ€ Time.time์„ ๊ทธ๋Œ€๋กœ ๋„˜๊ธฐ๋Š” ์ด์œ 

waveSpeed ๊ฐ™์€ ์Šค์ผ€์ผ ํŒŒ๋ผ๋ฏธํ„ฐ๋Š” ์„œ๋ธŒํด๋ž˜์Šค ๊ณ ์œ  ๊ฐ’์ž…๋‹ˆ๋‹ค. ๋ฒ ์ด์Šค๊ฐ€ ์•Œ ์ˆ˜ ์—†์œผ๋ฏ€๋กœ Time.time์„ ๋‚ ๊ฒƒ์œผ๋กœ ์ „๋‹ฌํ•˜๊ณ  Animate() ๋‚ด๋ถ€์—์„œ t * waveSpeed๋กœ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค.


์ƒˆ ์•„ํŠธ์›Œํฌ๋ฅผ ๋งŒ๋“ค ๋•Œ

BoilerplateGPUInstancing.cs๋ฅผ ๋ณต์‚ฌ โ†’ ํด๋ž˜์Šค๋ช… ๋ณ€๊ฒฝ โ†’ Generate() ๋ฐฐ์น˜ ๊ณต์‹, Animate() ์ˆ˜์‹, ์ธ์ŠคํŽ™ํ„ฐ ํŒŒ๋ผ๋ฏธํ„ฐ, ์บ์‹œ 4๊ณณ๋งŒ ์ˆ˜์ •ํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค. GenArtBase, GPUInstanceRenderer, GenArtResources๋Š” ์†๋Œˆ ํ•„์š”๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.


Codes

๐Ÿ“„BoilerplateGPUInstancing.cs

using UnityEngine;
using System.Collections.Generic;

// ============================================================
// ๋ณด์ผ๋Ÿฌํ”Œ๋ ˆ์ดํŠธ ์•„ํŠธ์›Œํฌ โ€” GenArtBase ์„œ๋ธŒํด๋ž˜์Šค
//
// ์ˆ˜์ •ํ•ด์•ผ ํ•  ๊ณณ์€ ๋‘ ๊ตฐ๋ฐ๋ฟ์ž…๋‹ˆ๋‹ค.
//   Generate() : ์ธ์Šคํ„ด์Šค๋ฅผ ๋ช‡ ๊ฐœ, ์–ด๋–ค ํŒจํ„ด์œผ๋กœ ๋ฐฐ์น˜ํ•  ๊ฒƒ์ธ๊ฐ€
//   Animate(t) : ๋งค ํ”„๋ ˆ์ž„ ์œ„์น˜ยท์ƒ‰์ƒ์„ ์–ด๋–ค ์ˆ˜์‹์œผ๋กœ ๊ณ„์‚ฐํ•  ๊ฒƒ์ธ๊ฐ€
//
// ๊ตฌ์กฐ ํŒŒ๋ผ๋ฏธํ„ฐ(์žฌ์ƒ์„ฑ์ด ํ•„์š”ํ•œ ๊ฒƒ) vs ์‹œ๊ฐ ํŒŒ๋ผ๋ฏธํ„ฐ(์‹ค์‹œ๊ฐ„ ๋ฐ˜์˜)๋ฅผ
// ๊ตฌ๋ถ„ํ•ด์„œ ์ธ์ŠคํŽ™ํ„ฐ ํ—ค๋”๋ฅผ ๋‚˜๋ˆ„๊ณ ,
// StructureParamsChanged() / CacheStructureParams()๋ฅผ ํ•จ๊ป˜ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค.
// ============================================================
public class BoilerplateGPUInstancing : GenArtBase
{
    // ============================================================
    // ์ธ์ŠคํŽ™ํ„ฐ ํŒŒ๋ผ๋ฏธํ„ฐ
    // ============================================================

    // ----- ๊ตฌ์กฐ ํŒŒ๋ผ๋ฏธํ„ฐ (๋ณ€๊ฒฝ ์‹œ Generate ์žฌ์‹คํ–‰) ---------------
    [Header("๊ตฌ์กฐ ์„ค์ • (๋ณ€๊ฒฝ ์‹œ ์˜ค๋ธŒ์ ํŠธ ์žฌ์ƒ์„ฑ)")]
    public int   gridSize  = 100;
    public float spacing   = 0.38f;
    public float cubeSize  = 0.30f;
    public int   colorSeed = 0;

    // ----- ์‹œ๊ฐ ํŒŒ๋ผ๋ฏธํ„ฐ (๋ณ€๊ฒฝ ์‹œ Animate(0f)๋งŒ ์žฌ์‹คํ–‰) ----------
    [Header("์• ๋‹ˆ๋ฉ”์ด์…˜ ์„ค์ • (์‹ค์‹œ๊ฐ„ ๋ฐ˜์˜)")]
    public float waveHeight    = 0.60f;
    public float waveSpeed     = 1.2f;
    public float waveFrequency = 0.6f;

    [Header("์ƒ‰์ƒ ์„ค์ • (์‹ค์‹œ๊ฐ„ ๋ฐ˜์˜)")]
    [Range(0f, 1f)] public float hueBase    = 0.58f;
    [Range(0f, 1f)] public float hueRange   = 0.04f;
    [Range(0f, 1f)] public float saturation = 0.80f;
    public float emissionIntensity = 1.0f;

    // ============================================================
    // ์ธ์Šคํ„ด์Šค ๋ฐ์ดํ„ฐ
    // Generate์—์„œ ํ•œ ๋ฒˆ๋งŒ ๊ณ„์‚ฐ๋˜๋Š” ์ •์  ๋ฐ์ดํ„ฐ์ž…๋‹ˆ๋‹ค.
    // Animate์—์„œ ๋งค ํ”„๋ ˆ์ž„ ์ฐธ์กฐํ•˜์ง€๋งŒ ๊ฐ’์„ ๋ณ€๊ฒฝํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
    // ============================================================
    struct InstanceData
    {
        public Vector3 basePos;    // ๊ธฐ์ค€ ์œ„์น˜ (Y = 0 ํ‰๋ฉด)
        public float   hue;        // ๊ณ ์œ  ์ƒ‰์ƒ
        public float   brightness; // ๊ธฐ๋ณธ ๋ฐ๊ธฐ
    }

    readonly List<InstanceData> _instances = new List<InstanceData>();

    // ๊ตฌ์กฐ ํŒŒ๋ผ๋ฏธํ„ฐ ์บ์‹œ โ€” OnValidate์—์„œ ๋ณ€๊ฒฝ ๊ฐ์ง€์— ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
    int   _cachedGridSize;
    float _cachedSpacing;
    float _cachedCubeSize;
    int   _cachedColorSeed;

    // ============================================================
    // Generate โ€” [์•„ํŠธ์›Œํฌ ๋กœ์ง] ๋ฐฐ์น˜ ๊ณต์‹์„ ์—ฌ๊ธฐ์„œ ์ˆ˜์ •ํ•ฉ๋‹ˆ๋‹ค.
    // ============================================================
    protected override void Generate()
    {
        _instances.Clear();
        _generated = false;

        Random.InitState(colorSeed); // ๊ฐ™์€ ์‹œ๋“œ = ํ•ญ์ƒ ๋™์ผํ•œ ์ƒ‰์ƒ ๋ฐฐ์น˜

        // ๊ทธ๋ฆฌ๋“œ ์ค‘์‹ฌ์„ ๋กœ์ปฌ ์›์ (0,0,0)์— ๋งž์ถฅ๋‹ˆ๋‹ค.
        float offset = (gridSize - 1) * spacing * 0.5f;

        for (int x = 0; x < gridSize; x++)
        {
            for (int z = 0; z < gridSize; z++)
            {
                _instances.Add(new InstanceData
                {
                    basePos    = new Vector3(x * spacing - offset, 0f, z * spacing - offset),
                    hue        = (hueBase + Random.value * hueRange) % 1f,
                    brightness = Random.Range(0.5f, 1.0f)
                });
            }
        }

        // GPU ์ „์†ก ๋ฐฐ์—ด์„ ์ธ์Šคํ„ด์Šค ์ˆ˜์— ๋งž๊ฒŒ ์‚ฌ์ „ ํ• ๋‹นํ•ฉ๋‹ˆ๋‹ค.
        int count  = _instances.Count;
        _matrices  = new Matrix4x4[count];
        _colors    = new Vector4[count];
        _emissions = new Vector4[count];

        _generated = true;
        Animate(0f); // ์ƒ์„ฑ ์งํ›„ ์ดˆ๊ธฐ ์ƒํƒœ ๊ณ„์‚ฐ
    }

    // ============================================================
    // Animate โ€” [์•„ํŠธ์›Œํฌ ๋กœ์ง] ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ˆ˜์‹์„ ์—ฌ๊ธฐ์„œ ์ˆ˜์ •ํ•ฉ๋‹ˆ๋‹ค.
    // t : GenArtBase.Update()์—์„œ Time.time์„ ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค.
    //     ์—๋””ํ„ฐ ์”ฌ ๋ทฐ / ์ดˆ๊ธฐํ™” ์‹œ์—๋Š” 0f๊ฐ€ ์ „๋‹ฌ๋ฉ๋‹ˆ๋‹ค.
    // ============================================================
    protected override void Animate(float t)
    {
        if (!_generated) return;

        float animT = t * waveSpeed; // ์‹œ๊ฐ„ ์Šค์ผ€์ผ ์ ์šฉ

        for (int i = 0; i < _instances.Count; i++)
        {
            InstanceData inst = _instances[i];

            // X + Z ์ขŒํ‘œ ํ•ฉ์œผ๋กœ ๋Œ€๊ฐ์„  ๋ฐฉํ–ฅ ์‚ฌ์ธํŒŒ๋ฅผ ๋งŒ๋“ญ๋‹ˆ๋‹ค.
            float wave = Mathf.Sin(
                (inst.basePos.x + inst.basePos.z) * waveFrequency + animT
            ) * waveHeight;

            // -1~+1 ๋ฒ”์œ„๋ฅผ 0~1๋กœ ์ •๊ทœํ™”ํ•ฉ๋‹ˆ๋‹ค. ์ƒ‰์ƒยท๋ฐ๊ธฐ ๋ณ€์กฐ์— ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
            float normalized = (wave / waveHeight + 1f) * 0.5f;

            // ๋กœ์ปฌ โ†’ ์›”๋“œ ์ขŒํ‘œ ๋ณ€ํ™˜ (๋ถ€๋ชจ GameObject์˜ ์œ„์น˜ยทํšŒ์ „ ๋ฐ˜์˜)
            Vector3 worldPos = transform.TransformPoint(
                new Vector3(inst.basePos.x, wave, inst.basePos.z)
            );

            _matrices[i] = Matrix4x4.TRS(
                worldPos,
                transform.rotation,
                Vector3.one * cubeSize
            );

            float animBrightness = inst.brightness * Mathf.Lerp(0.2f, 1.0f, normalized);
            Color albedo   = Color.HSVToRGB(inst.hue, saturation, animBrightness);
            Color emission = Color.HSVToRGB(inst.hue, saturation, normalized) * emissionIntensity;

            _colors[i]    = new Vector4(albedo.r,   albedo.g,   albedo.b,   1f);
            _emissions[i] = new Vector4(emission.r, emission.g, emission.b, 1f);
        }
    }

    // ============================================================
    // ๊ตฌ์กฐ ํŒŒ๋ผ๋ฏธํ„ฐ ์บ์‹œ โ€” ๊ตฌ์กฐ ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์ถ”๊ฐ€/์ œ๊ฑฐํ•˜๋ฉด ์—ฌ๊ธฐ๋„ ํ•จ๊ป˜ ์ˆ˜์ •ํ•ฉ๋‹ˆ๋‹ค.
    // ============================================================

    protected override bool StructureParamsChanged()
    {
        return gridSize  != _cachedGridSize
            || spacing   != _cachedSpacing
            || cubeSize  != _cachedCubeSize
            || colorSeed != _cachedColorSeed;
    }

    protected override void CacheStructureParams()
    {
        _cachedGridSize  = gridSize;
        _cachedSpacing   = spacing;
        _cachedCubeSize  = cubeSize;
        _cachedColorSeed = colorSeed;
    }
}

๐Ÿ“„GenArtBase.cs

using UnityEngine;

#if UNITY_EDITOR
using UnityEditor;
#endif

// ============================================================
// ์ƒ์„ฑ ์•„ํŠธ ๋ฒ ์ด์Šค ํด๋ž˜์Šค
// Unity ๋ผ์ดํ”„์‚ฌ์ดํด(OnEnable / OnValidate / Update)๊ณผ
// ์—๋””ํ„ฐ ์•ˆ์ „ ํŒจํ„ด(QueueGenerate)์„ ์ „๋‹ดํ•ฉ๋‹ˆ๋‹ค.
//
// ์„œ๋ธŒํด๋ž˜์Šค๋Š” 4๊ฐœ์˜ abstract ๋ฉ”์„œ๋“œ๋งŒ ๊ตฌํ˜„ํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค.
//   Generate()             โ€” ๋ฐฐ์น˜ ๊ณต์‹
//   Animate(float t)       โ€” ์• ๋‹ˆ๋ฉ”์ด์…˜ ๊ณต์‹
//   StructureParamsChanged() โ€” ๊ตฌ์กฐ ํŒŒ๋ผ๋ฏธํ„ฐ ๋ณ€๊ฒฝ ๊ฐ์ง€
//   CacheStructureParams() โ€” ๊ตฌ์กฐ ํŒŒ๋ผ๋ฏธํ„ฐ ์บ์‹œ ์ €์žฅ
// ============================================================
[ExecuteAlways]
public abstract class GenArtBase : MonoBehaviour
{
    [Header("๋ Œ๋” ๋ฆฌ์†Œ์Šค (๋น„์›Œ๋‘๋ฉด ์ž๋™ ์ƒ์„ฑ)")]
    public Mesh     instanceMesh;
    public Material instanceMaterial;

    // ์„œ๋ธŒํด๋ž˜์Šค์—์„œ Animate()๊ฐ€ ์ฑ„์šฐ๊ณ  RenderInstances()๊ฐ€ ์ฝ์Šต๋‹ˆ๋‹ค.
    protected Matrix4x4[] _matrices;
    protected Vector4[]   _colors;
    protected Vector4[]   _emissions;

    // false์ด๋ฉด Update์—์„œ ๋ Œ๋”๋ง์„ ๊ฑด๋„ˆ๋œ๋‹ˆ๋‹ค.
    protected bool _generated;

    GPUInstanceRenderer _renderer;

#if UNITY_EDITOR
    bool _generateQueued;
#endif

    // ============================================================
    // Unity ์ด๋ฒคํŠธ โ€” ์„œ๋ธŒํด๋ž˜์Šค์—์„œ override ๋ถˆํ•„์š”
    // ============================================================

    void OnEnable()
    {
        EnsureResources();
        Generate();
        CacheStructureParams();
    }

    void OnValidate()
    {
#if UNITY_EDITOR
        EnsureResources();

        if (StructureParamsChanged())
        {
            // ๊ตฌ์กฐ ํŒŒ๋ผ๋ฏธํ„ฐ ๋ณ€๊ฒฝ โ†’ ์žฌ์ƒ์„ฑ ์˜ˆ์•ฝ
            CacheStructureParams();
            QueueGenerate();
        }
        else
        {
            // ์‹œ๊ฐ ํŒŒ๋ผ๋ฏธํ„ฐ ๋ณ€๊ฒฝ โ†’ ์žฌ์ƒ์„ฑ ์—†์ด ์ฆ‰์‹œ ๊ฐฑ์‹ 
            Animate(0f);
        }
#endif
    }

    void Update()
    {
        if (!_generated || _renderer == null) return;

        if (!Application.isPlaying)
        {
            // ์”ฌ ๋ทฐ: ์ •์ง€ ์ƒํƒœ๋กœ ๋ Œ๋”๋งํ•ฉ๋‹ˆ๋‹ค.
            Animate(0f);
            _renderer.Render(_matrices, _colors, _emissions, _matrices.Length);
            return;
        }

        // Play ๋ชจ๋“œ: ์‹œ๊ฐ„ ๊ธฐ๋ฐ˜ ์• ๋‹ˆ๋ฉ”์ด์…˜
        Animate(Time.time);
        _renderer.Render(_matrices, _colors, _emissions, _matrices.Length);
    }

    // ============================================================
    // ๋ฆฌ์†Œ์Šค ์ดˆ๊ธฐํ™”
    // ============================================================

    // ๋ฉ”์‹œยท๋จธํ‹ฐ๋ฆฌ์–ผ์ด ์—†์œผ๋ฉด ์ž๋™ ์ƒ์„ฑํ•˜๊ณ  renderer๋ฅผ (์žฌ)๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.
    void EnsureResources()
    {
        if (instanceMesh     == null) instanceMesh     = GenArtResources.CreateDefaultMesh();
        if (instanceMaterial == null) instanceMaterial = GenArtResources.CreateDefaultMaterial();

        // ์ธ์ŠคํŽ™ํ„ฐ์—์„œ mesh/material์ด ๊ต์ฒด๋  ๊ฒฝ์šฐ๋ฅผ ์œ„ํ•ด ํ•ญ์ƒ ์žฌ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.
        _renderer = new GPUInstanceRenderer(instanceMesh, instanceMaterial);
    }

    // ============================================================
    // ์—๋””ํ„ฐ ์•ˆ์ „ Generate ์˜ˆ์•ฝ
    // ============================================================

    // OnValidate ์ปจํ…์ŠคํŠธ์—์„œ ์ง์ ‘ ์”ฌ์„ ์ˆ˜์ •ํ•˜๋ฉด Unity ์ง๋ ฌํ™”์™€ ์ถฉ๋Œํ•ฉ๋‹ˆ๋‹ค.
    // delayCall๋กœ ์—๋””ํ„ฐ ๋ฃจํ”„ ๋‹ค์Œ ํ‹ฑ์— ์•ˆ์ „ํ•˜๊ฒŒ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค.
#if UNITY_EDITOR
    void QueueGenerate()
    {
        if (_generateQueued) return;
        _generateQueued = true;

        EditorApplication.delayCall += () =>
        {
            _generateQueued = false;
            if (this == null) return; // ์˜ค๋ธŒ์ ํŠธ๊ฐ€ ์‚ญ์ œ๋œ ๊ฒฝ์šฐ ๋ฐฉ์–ด
            Generate();
        };
    }
#endif

    // ============================================================
    // ์„œ๋ธŒํด๋ž˜์Šค ๊ตฌํ˜„ ์š”๊ตฌ โ€” ์•„ํŠธ์›Œํฌ ๋กœ์ง
    // ============================================================

    // ์ธ์Šคํ„ด์Šค ๋ฐฐ์น˜๋ฅผ ๊ณ„์‚ฐํ•˜๊ณ  _matrices, _colors, _emissions ๋ฐฐ์—ด์„ ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.
    // ๋งˆ์ง€๋ง‰์— _generated = true ๋ฅผ ์„ธํŒ…ํ•˜๊ณ  Animate(0f)๋ฅผ ํ˜ธ์ถœํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
    protected abstract void Generate();

    // ๋งค ํ”„๋ ˆ์ž„(๋˜๋Š” t=0 ์ •์ง€ ์ƒํƒœ) ๊ธฐ์ค€์œผ๋กœ _matrices, _colors, _emissions๋ฅผ ๊ฐฑ์‹ ํ•ฉ๋‹ˆ๋‹ค.
    // t ๊ฐ’: Update์—์„œ๋Š” Time.time, ์—๋””ํ„ฐ/์ดˆ๊ธฐํ™”์—์„œ๋Š” 0f
    protected abstract void Animate(float t);

    // ๊ตฌ์กฐ ํŒŒ๋ผ๋ฏธํ„ฐ(์˜ค๋ธŒ์ ํŠธ ์ˆ˜ยท๋ฐฐ์น˜ ํ˜•ํƒœ๋ฅผ ๊ฒฐ์ •ํ•˜๋Š” ํŒŒ๋ผ๋ฏธํ„ฐ)๊ฐ€ ๋ฐ”๋€Œ์—ˆ์œผ๋ฉด true๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
    protected abstract bool StructureParamsChanged();

    // ํ˜„์žฌ ๊ตฌ์กฐ ํŒŒ๋ผ๋ฏธํ„ฐ ๊ฐ’์„ ์บ์‹œ์— ์ €์žฅํ•ฉ๋‹ˆ๋‹ค.
    protected abstract void CacheStructureParams();
}

๐Ÿ“„GenArtResources.cs

using UnityEngine;
using UnityEngine.Rendering;

// ============================================================
// ๋ฉ”์‹œยท๋จธํ‹ฐ๋ฆฌ์–ผ ์ž๋™ ์ƒ์„ฑ ์œ ํ‹ธ๋ฆฌํ‹ฐ
// Shader.Find() ๋Œ€์‹  ์”ฌ์˜ ๊ธฐ์กด Renderer ๋˜๋Š” GraphicsSettings์—์„œ
// ์…ฐ์ด๋”๋ฅผ ์ถ”์ถœํ•ฉ๋‹ˆ๋‹ค. Shader.Find()๋Š” Always Included Shaders์—
// ๋“ฑ๋ก๋˜์ง€ ์•Š์€ ์…ฐ์ด๋”๋ฅผ ๋Ÿฐํƒ€์ž„์— ์ฐพ์ง€ ๋ชปํ•˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.
// ============================================================
public static class GenArtResources
{
    // ์ž„์‹œ Primitive์—์„œ ํ๋ธŒ ๋ฉ”์‹œ๋ฅผ ์ถ”์ถœ ํ›„ ์ฆ‰์‹œ ์‚ญ์ œํ•ฉ๋‹ˆ๋‹ค.
    // ์”ฌ ๊ณ„์ธต(Hierarchy)์— ์ž”๋ฅ˜ ์˜ค๋ธŒ์ ํŠธ๋ฅผ ๋‚จ๊ธฐ์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
    public static Mesh CreateDefaultMesh()
    {
        GameObject temp = GameObject.CreatePrimitive(PrimitiveType.Cube);
        Mesh mesh = temp.GetComponent<MeshFilter>().sharedMesh;
        Object.DestroyImmediate(temp);
        return mesh;
    }

    public static Material CreateDefaultMaterial()
    {
        Shader shader = ResolveShader();

        if (shader == null)
        {
            Debug.LogError("[GenArtResources] ์œ ํšจํ•œ ์…ฐ์ด๋”๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. " +
                           "instanceMaterial์„ ์ธ์ŠคํŽ™ํ„ฐ์—์„œ ์ง์ ‘ ํ• ๋‹นํ•˜์„ธ์š”.");
            return null;
        }

        Debug.Log($"[GenArtResources] ์‚ฌ์šฉ ์…ฐ์ด๋”: {shader.name}");

        Material mat = new Material(shader);
        mat.enableInstancing = true;
        mat.EnableKeyword("_EMISSION");
        mat.globalIlluminationFlags = MaterialGlobalIlluminationFlags.RealtimeEmissive;

        return mat;
    }

    // ============================================================
    // ์…ฐ์ด๋” ํ•ด๊ฒฐ ์ˆœ์„œ
    // 1์ˆœ์œ„: ์ปค์Šคํ…€ ์ธ์Šคํ„ด์‹ฑ ์…ฐ์ด๋” (SRP Batcher ๊ฐ„์„ญ ์—†์Œ)
    // 2์ˆœ์œ„: Shader.Find() ํด๋ฐฑ
    // ============================================================
    static Shader ResolveShader()
    {
        // 1์ˆœ์œ„: UNITY_INSTANCING_BUFFER ๊ธฐ๋ฐ˜ ์ปค์Šคํ…€ ์…ฐ์ด๋”
        // SRP Batcher๊ฐ€ ํ™œ์„ฑํ™”๋œ URP์—์„œ per-instance ์ƒ‰์ƒ์„ ์ •ํ™•ํžˆ ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค.
        Shader custom = Shader.Find("Custom/GPUInstancingLit");
        if (custom != null) return custom;

        // 2์ˆœ์œ„: ํด๋ฐฑ (์ปค์Šคํ…€ ์…ฐ์ด๋” ํŒŒ์ผ์ด ์—†๋Š” ๊ฒฝ์šฐ)
        Debug.LogWarning("[GenArtResources] Custom/GPUInstancingLit ์…ฐ์ด๋”๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. " +
                         "GPUInstancingLit.shader๋ฅผ Assets ํด๋”์— ์ถ”๊ฐ€ํ•˜์„ธ์š”.");
        return Shader.Find("Universal Render Pipeline/Lit")
            ?? Shader.Find("Standard");
    }
}

๐Ÿ“„GPUInstanceRenderer.cs

using UnityEngine;

// ============================================================
// GPU Instancing ๋“œ๋กœ์šฐ ์ฝœ ์ „๋‹ด ํด๋ž˜์Šค
// MonoBehaviour๊ฐ€ ์•„๋‹Œ ์ˆœ์ˆ˜ C# ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค.
// GenArtBase๊ฐ€ ์†Œ์œ ํ•˜๋ฉฐ, ์•„ํŠธ์›Œํฌ ํด๋ž˜์Šค์—์„œ ์ง์ ‘ ๊ฑด๋“œ๋ฆฌ์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
// ============================================================
public class GPUInstanceRenderer
{
    const int BATCH_SIZE = 1023;

    readonly Mesh     _mesh;
    readonly Material _material;

    public GPUInstanceRenderer(Mesh mesh, Material material)
    {
        _mesh     = mesh;
        _material = material;
    }

    // matrices / colors / emissions ๋ฐฐ์—ด์„ 1023๊ฐœ ๋‹จ์œ„ ๋ฐฐ์น˜๋กœ ๋‚˜๋ˆ  GPU์— ์ „์†กํ•ฉ๋‹ˆ๋‹ค.
    // count๋Š” ๋ฐฐ์—ด์˜ ์‹ค์ œ ์‚ฌ์šฉ ๊ธธ์ด์ž…๋‹ˆ๋‹ค (๋ฐฐ์—ด ํฌ๊ธฐ์™€ ๋‹ค๋ฅผ ์ˆ˜ ์žˆ์Œ).
    public void Render(Matrix4x4[] matrices, Vector4[] colors, Vector4[] emissions, int count)
    {
        if (_mesh == null || _material == null || count == 0) return;

        int index     = 0;
        int remaining = count;

        while (remaining > 0)
        {
            int batch = Mathf.Min(BATCH_SIZE, remaining);

            // ๋ฐฐ์น˜ ํฌ๊ธฐ๋งŒํผ ์Šฌ๋ผ์ด์Šค ๋ณต์‚ฌํ•ฉ๋‹ˆ๋‹ค.
            Matrix4x4[] batchMatrices  = new Matrix4x4[batch];
            Vector4[]   batchColors    = new Vector4[batch];
            Vector4[]   batchEmissions = new Vector4[batch];

            System.Array.Copy(matrices,  index, batchMatrices,  0, batch);
            System.Array.Copy(colors,    index, batchColors,    0, batch);
            System.Array.Copy(emissions, index, batchEmissions, 0, batch);

            // ๋ฐฐ์น˜๋งˆ๋‹ค new๋กœ ์ƒ์„ฑํ•ด์•ผ ์ด์ „ ๋ฐฐ์น˜ ๋ฐ์ดํ„ฐ์™€ ๊ฐ„์„ญ์ด ์—†์Šต๋‹ˆ๋‹ค.
            MaterialPropertyBlock block = new MaterialPropertyBlock();
            block.SetVectorArray("_BaseColor",     batchColors);
            block.SetVectorArray("_EmissionColor", batchEmissions);

            Graphics.DrawMeshInstanced(
                _mesh, 0, _material,
                batchMatrices, batch, block
            );

            index     += batch;
            remaining -= batch;
        }
    }
}

๐Ÿ“„GPUInstancingLit.shader

Shader "Custom/GPUInstancingLit"
{
    Properties
    {
        _BaseColor   ("Base Color",   Color) = (1,1,1,1)
        _EmissionColor ("Emission Color", Color) = (0,0,0,1)
    }

    SubShader
    {
        Tags
        {
            "RenderType"      = "Opaque"
            "RenderPipeline"  = "UniversalPipeline"
            "Queue"           = "Geometry"
        }

        Pass
        {
            Name "ForwardLit"
            Tags { "LightMode" = "UniversalForward" }

            HLSLPROGRAM
            #pragma vertex   vert
            #pragma fragment frag

            // GPU Instancing ํ™œ์„ฑํ™”
            #pragma multi_compile_instancing

            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"

            // --------------------------------------------------------
            // ์ธ์Šคํ„ด์Šค๋ณ„ ํ”„๋กœํผํ‹ฐ ๋ฒ„ํผ
            // UNITY_INSTANCING_BUFFER ์•ˆ์— ์„ ์–ธ๋œ ๋ณ€์ˆ˜๋Š”
            // ์ธ์Šคํ„ด์Šค๋งˆ๋‹ค ๋‹ค๋ฅธ ๊ฐ’์„ GPU์—์„œ ์ง์ ‘ ์ฝ์Šต๋‹ˆ๋‹ค.
            // MaterialPropertyBlock.SetVectorArray()๋กœ ์ฑ„์šด ๊ฐ’์ด ์—ฌ๊ธฐ๋กœ ๋“ค์–ด์˜ต๋‹ˆ๋‹ค.
            // --------------------------------------------------------
            UNITY_INSTANCING_BUFFER_START(Props)
                UNITY_DEFINE_INSTANCED_PROP(float4, _BaseColor)
                UNITY_DEFINE_INSTANCED_PROP(float4, _EmissionColor)
            UNITY_INSTANCING_BUFFER_END(Props)

            struct Attributes
            {
                float4 positionOS : POSITION;
                float3 normalOS   : NORMAL;
                UNITY_VERTEX_INPUT_INSTANCE_ID  // ์ธ์Šคํ„ด์Šค ID ์ž…๋ ฅ
            };

            struct Varyings
            {
                float4 positionHCS : SV_POSITION;
                float3 normalWS    : TEXCOORD0;
                UNITY_VERTEX_INPUT_INSTANCE_ID  // fragment๋กœ ์ „๋‹ฌ์šฉ
            };

            Varyings vert(Attributes IN)
            {
                Varyings OUT;

                // ์ธ์Šคํ„ด์Šค ID๋ฅผ ํ˜„์žฌ ์ปจํ…์ŠคํŠธ์— ์„ธํŒ…ํ•ฉ๋‹ˆ๋‹ค.
                // UNITY_ACCESS_INSTANCED_PROP ์‚ฌ์šฉ ์ „ ๋ฐ˜๋“œ์‹œ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.
                UNITY_SETUP_INSTANCE_ID(IN);
                UNITY_TRANSFER_INSTANCE_ID(IN, OUT);

                OUT.positionHCS = TransformObjectToHClip(IN.positionOS.xyz);
                OUT.normalWS    = TransformObjectToWorldNormal(IN.normalOS);
                return OUT;
            }

            half4 frag(Varyings IN) : SV_Target
            {
                UNITY_SETUP_INSTANCE_ID(IN);

                // ์ธ์Šคํ„ด์Šค๋ณ„ ๋ฒ„ํผ์—์„œ ์ด ์ธ์Šคํ„ด์Šค์˜ ์ƒ‰์ƒ์„ ์ฝ์Šต๋‹ˆ๋‹ค.
                float4 baseColor     = UNITY_ACCESS_INSTANCED_PROP(Props, _BaseColor);
                float4 emissionColor = UNITY_ACCESS_INSTANCED_PROP(Props, _EmissionColor);

                // ๊ฐ„๋‹จํ•œ Lambert diffuse ์กฐ๋ช…
                Light mainLight = GetMainLight();
                float3 normalWS = normalize(IN.normalWS);
                float  NdotL    = saturate(dot(normalWS, mainLight.direction));
                float3 diffuse  = baseColor.rgb * mainLight.color * NdotL;

                // ambient + diffuse + emission
                float3 ambient = baseColor.rgb * 0.2;
                float3 color   = ambient + diffuse + emissionColor.rgb;

                return half4(color, baseColor.a);
            }
            ENDHLSL
        }

        // ShadowCaster Pass โ€” ๊ทธ๋ฆผ์ž ํˆฌ์˜์„ ์œ„ํ•ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.
        Pass
        {
            Name "ShadowCaster"
            Tags { "LightMode" = "ShadowCaster" }

            ZWrite On
            ZTest LEqual
            ColorMask 0

            HLSLPROGRAM
            #pragma vertex   vertShadow
            #pragma fragment fragShadow
            #pragma multi_compile_instancing

            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Shadows.hlsl"

            UNITY_INSTANCING_BUFFER_START(Props)
                UNITY_DEFINE_INSTANCED_PROP(float4, _BaseColor)
                UNITY_DEFINE_INSTANCED_PROP(float4, _EmissionColor)
            UNITY_INSTANCING_BUFFER_END(Props)

            struct Attributes
            {
                float4 positionOS : POSITION;
                float3 normalOS   : NORMAL;
                UNITY_VERTEX_INPUT_INSTANCE_ID
            };

            struct Varyings
            {
                float4 positionHCS : SV_POSITION;
                UNITY_VERTEX_INPUT_INSTANCE_ID
            };

            Varyings vertShadow(Attributes IN)
            {
                Varyings OUT;
                UNITY_SETUP_INSTANCE_ID(IN);
                UNITY_TRANSFER_INSTANCE_ID(IN, OUT);

                float3 posWS      = TransformObjectToWorld(IN.positionOS.xyz);
                float3 normalWS   = TransformObjectToWorldNormal(IN.normalOS);
                float4 posHCS     = TransformWorldToHClip(
                                        ApplyShadowBias(posWS, normalWS, _MainLightPosition.xyz)
                                    );
                OUT.positionHCS = posHCS;
                return OUT;
            }

            half4 fragShadow(Varyings IN) : SV_Target
            {
                return 0;
            }
            ENDHLSL
        }
    }
}

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

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