๐ŸซงArt_021 Mesh Volume Artwork

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

Unity GenArt

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

MeshVolumeArtwork โ€” ์•„ํŠธ์›Œํฌ ๊ฐœ์š” ๋ฐ ๊ธฐ์ˆ  ๋ฌธ์„œ

1. ์•„ํŠธ์›Œํฌ ๊ฐœ์š”

FBX๋กœ ์ œ์ž‘ํ•œ 3D ๋ชจ๋ธ๋ง์˜ ๋‚ด๋ถ€ ๋ถ€ํ”ผ๋ฅผ ์ˆ˜์ฒœ ๊ฐœ์˜ ์ž‘์€ ํŒŒํ‹ฐํด๋กœ ์ฑ„์šฐ๊ณ , ๊ฐ ํŒŒํ‹ฐํด์ด Perlin Noise๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ๋ฉ”์‹œ ๋‚ด๋ถ€์—์„œ ์ฒœ์ฒœํžˆ ๋ถ€์œ ํ•˜๋Š” ์ƒ์„ฑ ์•„ํŠธ์›Œํฌ์ž…๋‹ˆ๋‹ค.
ํŒŒํ‹ฐํด์€ ๋ชจ๋ธ๋ง์˜ ๊ฒ‰ ํ‘œ๋ฉด์ด ์•„๋‹Œ ๋‹ซํžŒ ๋ฉ”์‹œ์˜ ๋‚ด๋ถ€ ๊ณต๊ฐ„์— ๋ฌด์ž‘์œ„๋กœ ๋ฐฐ์น˜๋ฉ๋‹ˆ๋‹ค. ํŒŒํ‹ฐํด ์ž์ฒด๋Š” Unity์˜ Particle System์„ ์‚ฌ์šฉํ•˜์ง€ ์•Š์œผ๋ฉฐ, GPU Instancing์œผ๋กœ ์ˆ˜์ฒœ ๊ฐœ๋ฅผ ๋‹จ์ผ ๋“œ๋กœ์šฐ ์ฝœ์— ๊ฐ€๊นŒ์šด ๋น„์šฉ์œผ๋กœ ๋ Œ๋”๋งํ•ฉ๋‹ˆ๋‹ค.

2. ์ „์ฒด ์‹œ์Šคํ…œ ๊ตฌ์กฐ

์ด ์•„ํŠธ์›Œํฌ๋Š” 4๊ฐœ์˜ ํŒŒ์ผ๋กœ ๊ตฌ์„ฑ๋œ ๋ชจ๋“ˆ ์‹œ์Šคํ…œ ์œ„์—์„œ ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค.

GenArtResources	        โ€” ๋ฉ”์‹œยท๋จธํ‹ฐ๋ฆฌ์–ผ ์ž๋™ ์ƒ์„ฑ (static ์œ ํ‹ธ)
GPUInstanceRenderer     โ€” DrawMeshInstanced 1023 ๋ฐฐ์น˜ ๋ถ„ํ•  (์ผ๋ฐ˜ ํด๋ž˜์Šค)
GenArtBase              โ€” Unity ๋ผ์ดํ”„์‚ฌ์ดํด ์ „๋‹ด (abstract MonoBehaviour)
  โ””โ”€โ”€ MeshVolumeArtwork โ€” ์•„ํŠธ์›Œํฌ ๋กœ์ง (concrete ์„œ๋ธŒํด๋ž˜์Šค)

MeshVolumeArtwork๋Š” GenArtBase๋ฅผ ์ƒ์†ํ•˜๊ณ  4๊ฐœ์˜ abstract ๋ฉ”์„œ๋“œ๋งŒ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค. Unity ์ด๋ฒคํŠธ(OnEnable, OnValidate, Update), ์—๋””ํ„ฐ ์•ˆ์ „ ํŒจํ„ด(QueueGenerate), GPU ๋“œ๋กœ์šฐ ํ˜ธ์ถœ์€ ๋ชจ๋‘ ์ƒ์œ„ ํด๋ž˜์Šค๊ฐ€ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

3. ์‹คํ–‰ ํ๋ฆ„

OnEnable()
  โ””โ”€โ”€ Generate()
        โ”œโ”€โ”€ BuildWorldTriangleCache()   ์‚ผ๊ฐํ˜• ๋ฐ์ดํ„ฐ๋ฅผ ์›”๋“œ ๊ณต๊ฐ„์œผ๋กœ ๋ณ€ํ™˜ยท์บ์‹ฑ
        โ”œโ”€โ”€ CalcWorldBounds()           ์›”๋“œ ๋ฐ”์šด๋”ฉ ๋ฐ•์Šค ๊ณ„์‚ฐ
        โ”œโ”€โ”€ Rejection Sampling ๋ฃจํ”„     ๋‚ด๋ถ€ ํŒ์ • ํ†ต๊ณผํ•œ ์ ๋งŒ _instances์— ์ถ”๊ฐ€
        โ”‚     โ””โ”€โ”€ IsInsideWorld()
        โ”‚           โ””โ”€โ”€ RayIntersectsTriangle() (์‚ผ๊ฐํ˜• ์ˆ˜๋งŒํผ ๋ฐ˜๋ณต)
        โ””โ”€โ”€ Animate(0f)                 ์ดˆ๊ธฐ ์ •์ง€ ์ƒํƒœ ๊ณ„์‚ฐ

Update() โ†’ ๋งค ํ”„๋ ˆ์ž„
  โ”œโ”€โ”€ Animate(Time.time)               _matrices, _colors ๊ฐฑ์‹ 
  โ””โ”€โ”€ GPUInstanceRenderer.Render()     DrawMeshInstanced ํ˜ธ์ถœ

ํŒŒ์ผ ๊ตฌ์กฐ

Assets/
โ”œโ”€โ”€ GPUInstancingLit.shader โ† ์…ฐ์ด๋”
โ”œโ”€โ”€ GenArtResources.cs โ† ์œ ํ‹ธ
โ”œโ”€โ”€ GenArtBase.cs โ† ๋ฒ ์ด์Šค ํด๋ž˜์Šค
โ”œโ”€โ”€ GPUInstanceRenderer.cs โ† ๋“œ๋กœ์šฐ ํด๋ž˜์Šค
โ””โ”€โ”€ MeshVolumeArtwork.cs โ† ๋ฉ”์‹œ ๋‚ด๋ถ€ ํŒŒํ‹ฐํด ์•„ํŠธ์›Œํฌ


์ฝ”๋“œ

๐Ÿ“’ MeshVolumeArtwork.cs

using UnityEngine;
using System.Collections.Generic;

// ============================================================
// MeshVolumeArtwork โ€” GenArtBase ์„œ๋ธŒํด๋ž˜์Šค
//
// FBX ๋ฉ”์‹œ์˜ ๋‚ด๋ถ€ ๋ถ€ํ”ผ๋ฅผ ํŒŒํ‹ฐํด๋กœ ์ฑ„์šฐ๊ณ  Perlin Noise๋กœ ๋ถ€์œ ์‹œํ‚ต๋‹ˆ๋‹ค.
//
// [๋‚ด๋ถ€ ํŒ์ • ์ „๋žต]
// ์‚ผ๊ฐํ˜•์„ ์›”๋“œ ๊ณต๊ฐ„์œผ๋กœ ๋ณ€ํ™˜ํ•ด์„œ ์บ์‹ฑํ•ฉ๋‹ˆ๋‹ค.
// FBX Import ์‹œ Unity๊ฐ€ ์ ์šฉํ•˜๋Š” ์Šค์ผ€์ผ/ํšŒ์ „์ด ์ž๋™์œผ๋กœ ๋ฐ˜์˜๋ฉ๋‹ˆ๋‹ค.
// ์ƒ˜ํ”Œ ํ›„๋ณด ์ ์— ๋ฏธ์„ธ ์ง€ํ„ฐ๋ฅผ ์ถ”๊ฐ€ํ•ด ์—ฃ์ง€ ํ†ต๊ณผ ์ˆ˜์น˜ ์˜ค๋ฅ˜๋ฅผ ํšŒํ”ผํ•ฉ๋‹ˆ๋‹ค.
// ============================================================
public class MeshVolumeArtwork : GenArtBase
{
    // ============================================================
    // ์ธ์ŠคํŽ™ํ„ฐ ํŒŒ๋ผ๋ฏธํ„ฐ
    // ============================================================

    [Header("๊ตฌ์กฐ ์„ค์ • (๋ณ€๊ฒฝ ์‹œ ์žฌ์ƒ์„ฑ)")]
    public MeshFilter targetMeshFilter;
    public int        particleCount = 3000;
    public int        colorSeed     = 0;

    [Header("ํŒŒํ‹ฐํด ์„ค์ • (์‹ค์‹œ๊ฐ„ ๋ฐ˜์˜)")]
    [Range(0.005f, 0.3f)] public float particleSize = 0.04f;

    [Header("๋ถ€์œ  ์„ค์ • (์‹ค์‹œ๊ฐ„ ๋ฐ˜์˜)")]
    [Tooltip("๋…ธ์ด์ฆˆ ์‹œ๊ฐ„ ์ง„ํ–‰ ์†๋„")]
    [Range(0f,     3f)]   public float floatSpeed     = 0.4f;

    [Tooltip("๋…ธ์ด์ฆˆ ๊ณต๊ฐ„ ์ฃผํŒŒ์ˆ˜")]
    [Range(0.01f,  3f)]   public float noiseScale     = 0.6f;

    [Tooltip("๋Œ€๋ฒ”์œ„ ๋ถ€์œ  ํฌ๊ธฐ. 0์ด์–ด๋„ baseJitter๋กœ ํ•ญ์ƒ ๋ฏธ์„ธํ•˜๊ฒŒ ์›€์ง์ž…๋‹ˆ๋‹ค.")]
    [Range(0f,     5f)] public float floatAmplitude = 0.0f;

    [Tooltip("ํ•ญ์ƒ ์ ์šฉ๋˜๋Š” ์ตœ์†Œ ์ง„๋™ ํฌ๊ธฐ")]
    [Range(0.001f, 0.05f)] public float baseJitter    = 0.008f;

    [Header("์ƒ‰์ƒ ์„ค์ • (์‹ค์‹œ๊ฐ„ ๋ฐ˜์˜)")]
    [Range(0f, 1f)] public float hueBase    = 0.55f;
    [Range(0f, 1f)] public float hueRange   = 0.30f;
    [Range(0f, 1f)] public float saturation = 0.75f;
    [Range(0f, 1f)] public float brightness = 0.90f;

    // ============================================================
    // ์ธ์Šคํ„ด์Šค ๋ฐ์ดํ„ฐ
    // ============================================================

    struct InstanceData
    {
        public Vector3 worldBasePos; // ์›”๋“œ ๊ณต๊ฐ„ ๊ธฐ์ค€ ์œ„์น˜
        public float   hue;
        public float   phase;
        public float   jitterAmp;
    }

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

    // ์›”๋“œ ๊ณต๊ฐ„ ์‚ผ๊ฐํ˜• ์บ์‹œ
    Vector3[] _triA;
    Vector3[] _triB;
    Vector3[] _triC;

    // ์ƒ˜ํ”Œ๋ง์šฉ ์›”๋“œ ๋ฐ”์šด๋”ฉ ๋ฐ•์Šค
    Bounds _worldBounds;

    // ============================================================
    // ๊ตฌ์กฐ ํŒŒ๋ผ๋ฏธํ„ฐ ์บ์‹œ
    // ============================================================

    MeshFilter _cachedMeshFilter;
    int        _cachedParticleCount;
    int        _cachedColorSeed;

    // ============================================================
    // Generate
    // ============================================================

    protected override void Generate()
    {
        _instances.Clear();
        _generated = false;

        if (targetMeshFilter == null || targetMeshFilter.sharedMesh == null)
        {
            Debug.LogWarning("[MeshVolumeArtwork] Target Mesh Filter๊ฐ€ ํ• ๋‹น๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.");
            return;
        }

        Mesh      mesh          = targetMeshFilter.sharedMesh;
        Transform meshTransform = targetMeshFilter.transform;

        // ์‚ผ๊ฐํ˜•์„ ์›”๋“œ ๊ณต๊ฐ„์œผ๋กœ ๋ณ€ํ™˜ํ•ด์„œ ์บ์‹ฑํ•ฉ๋‹ˆ๋‹ค.
        // localToWorldMatrix ํ•œ ๋ฒˆ์œผ๋กœ ์Šค์ผ€์ผยทํšŒ์ „ยท์œ„์น˜๊ฐ€ ๋ชจ๋‘ ๋ฐ˜์˜๋ฉ๋‹ˆ๋‹ค.
        BuildWorldTriangleCache(mesh, meshTransform);

        // ์›”๋“œ ๊ณต๊ฐ„ ๋ฐ”์šด๋”ฉ ๋ฐ•์Šค๋ฅผ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค.
        _worldBounds = CalcWorldBounds(mesh.bounds, meshTransform);

        Random.InitState(colorSeed);
        int maxAttempts = particleCount * 30;
        int attempts    = 0;

        while (_instances.Count < particleCount && attempts < maxAttempts)
        {
            attempts++;

            // ์›”๋“œ ๊ณต๊ฐ„์—์„œ ๋žœ๋ค ํ›„๋ณด์ ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
            Vector3 candidate = new Vector3(
                Random.Range(_worldBounds.min.x, _worldBounds.max.x),
                Random.Range(_worldBounds.min.y, _worldBounds.max.y),
                Random.Range(_worldBounds.min.z, _worldBounds.max.z)
            );

            // ์—ฃ์ง€ ํ†ต๊ณผ ์ˆ˜์น˜ ์˜ค๋ฅ˜ ๋ฐฉ์ง€: ํ›„๋ณด์ ์— ๋ฏธ์„ธ ์ง€ํ„ฐ๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.
            // ๋ฐฉํ–ฅ์ด ์•„๋‹ˆ๋ผ ์  ์ž์ฒด๋ฅผ ํ”๋“ค์–ด์•ผ ์—ฃ์ง€ ์ผ€์ด์Šค๋ฅผ ์•ˆ์ „ํ•˜๊ฒŒ ํšŒํ”ผํ•ฉ๋‹ˆ๋‹ค.
            Vector3 jittered = candidate + new Vector3(
                (Random.value - 0.5f) * 0.0001f,
                (Random.value - 0.5f) * 0.0001f,
                (Random.value - 0.5f) * 0.0001f
            );

            if (IsInsideWorld(jittered))
            {
                _instances.Add(new InstanceData
                {
                    worldBasePos = candidate, // ์›๋ž˜ ์ขŒํ‘œ ์ €์žฅ (์ง€ํ„ฐ ์—†๋Š” ๊ฐ’)
                    hue          = (hueBase + Random.value * hueRange) % 1f,
                    phase        = Random.Range(0f, 100f),
                    jitterAmp    = Random.Range(0.7f, 1.3f)
                });
            }
        }

        if (_instances.Count == 0)
        {
            Debug.LogError("[MeshVolumeArtwork] ํŒŒํ‹ฐํด์„ ๋ฐฐ์น˜ํ•˜์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค. " +
                           "๋ฉ”์‹œ๊ฐ€ ๋‹ซํžŒ ํ˜•ํƒœ(Closed Mesh)์ธ์ง€ ํ™•์ธํ•˜์„ธ์š”.");
            return;
        }

        if (_instances.Count < particleCount)
            Debug.LogWarning($"[MeshVolumeArtwork] ๋ชฉํ‘œ {particleCount}๊ฐœ ์ค‘ {_instances.Count}๊ฐœ ๋ฐฐ์น˜. " +
                             $"(์‹œ๋„: {attempts}ํšŒ)");
        else
            Debug.Log($"[MeshVolumeArtwork] {_instances.Count}๊ฐœ ๋ฐฐ์น˜ ์™„๋ฃŒ. (์‹œ๋„: {attempts}ํšŒ)");

        int count = _instances.Count;
        _matrices = new Matrix4x4[count];
        _colors   = new Vector4[count];

        _generated = true;
        Animate(0f);
    }

    // ============================================================
    // ์‚ผ๊ฐํ˜• ์บ์‹œ โ€” ์›”๋“œ ๊ณต๊ฐ„์œผ๋กœ ๋ณ€ํ™˜ํ•ด์„œ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค.
    // FBX์˜ localScale์ด (0.01, 0.01, 0.01)์ด์–ด๋„ ์ •ํ™•ํ•˜๊ฒŒ ๋ฐ˜์˜๋ฉ๋‹ˆ๋‹ค.
    // ============================================================

    void BuildWorldTriangleCache(Mesh mesh, Transform t)
    {
        Vector3[]  verts    = mesh.vertices;
        int[]      tris     = mesh.triangles;
        int        triCount = tris.Length / 3;
        Matrix4x4  l2w      = t.localToWorldMatrix;

        _triA = new Vector3[triCount];
        _triB = new Vector3[triCount];
        _triC = new Vector3[triCount];

        for (int i = 0; i < triCount; i++)
        {
            _triA[i] = l2w.MultiplyPoint3x4(verts[tris[i * 3 + 0]]);
            _triB[i] = l2w.MultiplyPoint3x4(verts[tris[i * 3 + 1]]);
            _triC[i] = l2w.MultiplyPoint3x4(verts[tris[i * 3 + 2]]);
        }
    }

    // ============================================================
    // ๋ฉ”์‹œ ๋‚ด๋ถ€ ํŒ์ • โ€” ์›”๋“œ ๊ณต๊ฐ„ ๋‹จ์ผ ๋ฐฉํ–ฅ ๋ ˆ์ด์บ์ŠคํŠธ
    //
    // +Y ๋ฐฉํ–ฅ์œผ๋กœ ๋ฐœ์‚ฌํ•œ ๊ด‘์„ ์˜ ๊ต์ฐจ ํšŸ์ˆ˜๊ฐ€ ํ™€์ˆ˜์ด๋ฉด ๋‚ด๋ถ€์ž…๋‹ˆ๋‹ค.
    // ์‚ผ๊ฐํ˜•์ด ์›”๋“œ ๊ณต๊ฐ„์œผ๋กœ ๋ณ€ํ™˜๋˜์–ด ์žˆ์œผ๋ฏ€๋กœ ์Šค์ผ€์ผ ๋ถˆ์ผ์น˜ ์˜ค๋ฅ˜๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.
    // ============================================================

    bool IsInsideWorld(Vector3 worldPoint)
    {
        int hits = 0;
        for (int i = 0; i < _triA.Length; i++)
        {
            if (RayIntersectsTriangle(worldPoint, Vector3.up, _triA[i], _triB[i], _triC[i]))
                hits++;
        }
        return hits % 2 == 1;
    }

    // Mรถllerโ€“Trumbore ๊ด‘์„ -์‚ผ๊ฐํ˜• ๊ต์ฐจ ์•Œ๊ณ ๋ฆฌ์ฆ˜
    static bool RayIntersectsTriangle(Vector3 origin, Vector3 direction,
                                      Vector3 a,      Vector3 b,      Vector3 c)
    {
        const float EPSILON = 1e-6f;

        Vector3 edge1 = b - a;
        Vector3 edge2 = c - a;
        Vector3 h     = Vector3.Cross(direction, edge2);
        float   det   = Vector3.Dot(edge1, h);

        if (det > -EPSILON && det < EPSILON) return false;

        float   invDet = 1f / det;
        Vector3 s      = origin - a;
        float   u      = invDet * Vector3.Dot(s, h);

        if (u < 0f || u > 1f) return false;

        Vector3 q = Vector3.Cross(s, edge1);
        float   v = invDet * Vector3.Dot(direction, q);

        if (v < 0f || u + v > 1f) return false;

        float t = invDet * Vector3.Dot(edge2, q);
        return t > EPSILON;
    }

    // ============================================================
    // ์›”๋“œ ๋ฐ”์šด๋”ฉ ๋ฐ•์Šค ๊ณ„์‚ฐ
    // ============================================================

    static Bounds CalcWorldBounds(Bounds localBounds, Transform t)
    {
        Vector3 c = localBounds.center;
        Vector3 e = localBounds.extents;

        Vector3[] corners = new Vector3[8]
        {
            t.TransformPoint(c + new Vector3(-e.x, -e.y, -e.z)),
            t.TransformPoint(c + new Vector3( e.x, -e.y, -e.z)),
            t.TransformPoint(c + new Vector3(-e.x,  e.y, -e.z)),
            t.TransformPoint(c + new Vector3( e.x,  e.y, -e.z)),
            t.TransformPoint(c + new Vector3(-e.x, -e.y,  e.z)),
            t.TransformPoint(c + new Vector3( e.x, -e.y,  e.z)),
            t.TransformPoint(c + new Vector3(-e.x,  e.y,  e.z)),
            t.TransformPoint(c + new Vector3( e.x,  e.y,  e.z)),
        };

        Bounds wb = new Bounds(corners[0], Vector3.zero);
        for (int i = 1; i < 8; i++) wb.Encapsulate(corners[i]);
        return wb;
    }

    // ============================================================
    // Animate โ€” ์ด์ค‘ ๋ ˆ์ด์–ด ๋ถ€์œ  (์›”๋“œ ๊ณต๊ฐ„)
    //
    // Layer 1 (baseJitter): ํ•ญ์ƒ ๋™์ž‘ํ•˜๋Š” ๋ฏธ์„ธ ์ง„๋™
    // Layer 2 (floatAmplitude): ์ถ”๊ฐ€ ๋Œ€๋ฒ”์œ„ ์ด๋™
    // ============================================================

    protected override void Animate(float t)
    {
        if (!_generated) return;

        float animT = t * floatSpeed;

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

            // Layer 1 โ€” ๋ฏธ์„ธ ์ง„๋™ (floatAmplitude = 0์ผ ๋•Œ ์ฃผ ์›€์ง์ž„ ๋‹ด๋‹น)
            float jitterScale = baseJitter * inst.jitterAmp
                              * (floatAmplitude < 0.001f ? 2f : 1f);

            float jx = SampleNoise(inst.worldBasePos.x * 2.3f, inst.worldBasePos.z * 2.1f,
                                   inst.phase,         animT * 1.7f) * jitterScale;
            float jy = SampleNoise(inst.worldBasePos.y * 2.1f, inst.worldBasePos.x * 2.4f,
                                   inst.phase + 31.4f, animT * 1.7f) * jitterScale;
            float jz = SampleNoise(inst.worldBasePos.z * 2.4f, inst.worldBasePos.y * 2.2f,
                                   inst.phase + 72.8f, animT * 1.7f) * jitterScale;

            // Layer 2 โ€” ๋Œ€๋ฒ”์œ„ ๋ถ€์œ 
            float nx = SampleNoise(inst.worldBasePos.x, inst.worldBasePos.z,
                                   inst.phase,         animT) * floatAmplitude;
            float ny = SampleNoise(inst.worldBasePos.y, inst.worldBasePos.x,
                                   inst.phase + 31.4f, animT) * floatAmplitude;
            float nz = SampleNoise(inst.worldBasePos.z, inst.worldBasePos.y,
                                   inst.phase + 72.8f, animT) * floatAmplitude;

            Vector3 animPos = SoftClampWorld(
                inst.worldBasePos + new Vector3(jx + nx, jy + ny, jz + nz),
                inst.worldBasePos
            );

            _matrices[i] = Matrix4x4.TRS(animPos, Quaternion.identity, Vector3.one * particleSize);

            Color col  = Color.HSVToRGB(inst.hue, saturation, brightness);
            _colors[i] = new Vector4(col.r, col.g, col.b, 1f);
        }
    }

    float SampleNoise(float a, float b, float phase, float t)
    {
        return (Mathf.PerlinNoise(a * noiseScale + t + phase, b * noiseScale + phase) - 0.5f) * 2f;
    }

    // ์›”๋“œ ๋ฐ”์šด๋”ฉ ๋ฐ•์Šค๋ฅผ ๋ฒ—์–ด๋‚˜๋ฉด basePos ๋ฐฉํ–ฅ์œผ๋กœ ๋ณต์›ํ•ฉ๋‹ˆ๋‹ค.
    Vector3 SoftClampWorld(Vector3 pos, Vector3 basePos)
    {
        if (_worldBounds.Contains(pos)) return pos;
        return Vector3.Lerp(pos, basePos, 0.7f);
    }

    // ============================================================
    // ๊ตฌ์กฐ ํŒŒ๋ผ๋ฏธํ„ฐ ์บ์‹œ
    // ============================================================

    protected override bool StructureParamsChanged()
    {
        return targetMeshFilter != _cachedMeshFilter
            || particleCount   != _cachedParticleCount
            || colorSeed       != _cachedColorSeed;
    }

    protected override void CacheStructureParams()
    {
        _cachedMeshFilter    = targetMeshFilter;
        _cachedParticleCount = particleCount;
        _cachedColorSeed     = colorSeed;
    }
}

๐Ÿ“’ GPUInstanceRenderer.cs

using UnityEngine;

// ============================================================
// GPU Instancing ๋“œ๋กœ์šฐ ์ฝœ ์ „๋‹ด ํด๋ž˜์Šค
// ============================================================
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 ๋ฐฐ์—ด์„ 1023๊ฐœ ๋‹จ์œ„ ๋ฐฐ์น˜๋กœ ๋‚˜๋ˆ  GPU์— ์ „์†กํ•ฉ๋‹ˆ๋‹ค.
    // count๋Š” ๋ฐฐ์—ด์˜ ์‹ค์ œ ์‚ฌ์šฉ ๊ธธ์ด์ž…๋‹ˆ๋‹ค.
    public void Render(Matrix4x4[] matrices, Vector4[] colors, 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];

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

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

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

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

GenArtBase.cs

using UnityEngine;

#if UNITY_EDITOR
using UnityEditor;
#endif

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

    // ์„œ๋ธŒํด๋ž˜์Šค์—์„œ Animate()๊ฐ€ ์ฑ„์šฐ๊ณ  ๋ Œ๋”๋Ÿฌ๊ฐ€ ์ฝ๋Š” ๋ฐฐ์—ด
    protected Matrix4x4[] _matrices;
    protected Vector4[]   _colors;

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

    GPUInstanceRenderer _renderer;

#if UNITY_EDITOR
    bool _generateQueued;
#endif

    // ============================================================
    // Unity ์ด๋ฒคํŠธ
    // ============================================================

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

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

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

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

        if (!Application.isPlaying)
        {
            Animate(0f);
            _renderer.Render(_matrices, _colors, _matrices.Length);
            return;
        }

        Animate(Time.time);
        _renderer.Render(_matrices, _colors, _matrices.Length);
    }

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

    void EnsureResources()
    {
        if (instanceMesh     == null) instanceMesh     = GenArtResources.CreateDefaultMesh();
        if (instanceMaterial == null) instanceMaterial = GenArtResources.CreateDefaultMaterial();

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

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

#if UNITY_EDITOR
    void QueueGenerate()
    {
        if (_generateQueued) return;
        _generateQueued = true;

        EditorApplication.delayCall += () =>
        {
            _generateQueued = false;
            if (this == null) return;
            Generate();
        };
    }
#endif

    // ============================================================
    // ์„œ๋ธŒํด๋ž˜์Šค ๊ตฌํ˜„ ์š”๊ตฌ
    // ============================================================

    protected abstract void Generate();
    protected abstract void Animate(float t);
    protected abstract bool StructureParamsChanged();
    protected abstract void CacheStructureParams();
}

๐Ÿ“’ GenArtResources.cs

using UnityEngine;
using UnityEngine.Rendering;

// ============================================================
// ๋ฉ”์‹œยท๋จธํ‹ฐ๋ฆฌ์–ผ ์ž๋™ ์ƒ์„ฑ ์œ ํ‹ธ๋ฆฌํ‹ฐ
// ์ธ์ŠคํŽ™ํ„ฐ ์Šฌ๋กฏ์ด ๋น„์–ด์žˆ์„ ๋•Œ GenArtBase.OnEnable์—์„œ ํ˜ธ์ถœ๋ฉ๋‹ˆ๋‹ค.
// ============================================================
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;

        return mat;
    }

    // ============================================================
    // ์…ฐ์ด๋” ํ•ด๊ฒฐ ์ˆœ์„œ
    // 1์ˆœ์œ„: Custom/GPUInstancingLit (UNITY_INSTANCING_BUFFER ๊ธฐ๋ฐ˜)
    // 2์ˆœ์œ„: Shader.Find() ํด๋ฐฑ
    // ============================================================
    static Shader ResolveShader()
    {
        // 1์ˆœ์œ„: ์ปค์Šคํ…€ ์ธ์Šคํ„ด์‹ฑ ์…ฐ์ด๋”
        // 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");
    }
}

๐Ÿ“’ GPUInstancingLit.shader

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

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

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

            HLSLPROGRAM
            #pragma vertex   vert
            #pragma fragment frag
            #pragma multi_compile_instancing

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

            // SRP Batcher๋ฅผ ์šฐํšŒํ•˜๋Š” per-instance ํ”„๋กœํผํ‹ฐ ๋ฒ„ํผ์ž…๋‹ˆ๋‹ค.
            // MaterialPropertyBlock.SetVectorArray("_BaseColor", ...) ๊ฐ’์ด ์—ฌ๊ธฐ๋กœ ๋“ค์–ด์˜ต๋‹ˆ๋‹ค.
            UNITY_INSTANCING_BUFFER_START(Props)
                UNITY_DEFINE_INSTANCED_PROP(float4, _BaseColor)
            UNITY_INSTANCING_BUFFER_END(Props)

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

            struct Varyings
            {
                float4 positionHCS : SV_POSITION;
                float3 normalWS    : TEXCOORD0;
                UNITY_VERTEX_INPUT_INSTANCE_ID
            };

            Varyings vert(Attributes IN)
            {
                Varyings OUT;
                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);

                // Lambert diffuse + ambient
                Light   mainLight = GetMainLight();
                float3  normalWS  = normalize(IN.normalWS);
                float   NdotL     = saturate(dot(normalWS, mainLight.direction));

                float3 diffuse = baseColor.rgb * mainLight.color.rgb * NdotL;
                float3 ambient = baseColor.rgb * 0.25;

                return half4(ambient + diffuse, baseColor.a);
            }
            ENDHLSL
        }

        // ----------------------------------------------------------
        // ShadowCaster Pass
        // ----------------------------------------------------------
        Pass
        {
            Name "ShadowCaster"
            Tags { "LightMode" = "ShadowCaster" }

            ZWrite    On
            ZTest     LEqual
            ColorMask 0
            Cull      Back

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

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

            // ShadowCaster๋Š” ์ƒ‰์ƒ ๋ฐ์ดํ„ฐ๊ฐ€ ํ•„์š” ์—†์ง€๋งŒ,
            // ์ธ์Šคํ„ด์‹ฑ ๋ฒ„ํผ๋ฅผ ์„ ์–ธํ•ด์•ผ UNITY_SETUP_INSTANCE_ID๊ฐ€ ์ •์ƒ ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค.
            UNITY_INSTANCING_BUFFER_START(Props)
                UNITY_DEFINE_INSTANCED_PROP(float4, _BaseColor)
            UNITY_INSTANCING_BUFFER_END(Props)

            struct Attributes
            {
                float4 positionOS : POSITION;
                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);
                OUT.positionHCS = TransformObjectToHClip(IN.positionOS.xyz);
                return OUT;
            }

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

ํ•ต์‹ฌ ๊ธฐ์ˆ : ๋ฉ”์‹œ ๋‚ด๋ถ€ ํŒ์ •

Rejection Sampling

๋ฉ”์‹œ ๋‚ด๋ถ€์— ํŒŒํ‹ฐํด์„ ๊ท ์ผํ•˜๊ฒŒ ์ฑ„์šฐ๋Š” ๋ฐฉ๋ฒ•์ž…๋‹ˆ๋‹ค.

  1. ๋ฉ”์‹œ์˜ ์›”๋“œ ๋ฐ”์šด๋”ฉ ๋ฐ•์Šค ์•ˆ์—์„œ ๋žœ๋ค ํ›„๋ณด์ ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
  2. ํ›„๋ณด์ ์ด ๋ฉ”์‹œ ๋‚ด๋ถ€์ธ์ง€ ํŒ์ •ํ•ฉ๋‹ˆ๋‹ค.
  3. ๋‚ด๋ถ€์ด๋ฉด _instances์— ์ถ”๊ฐ€ํ•˜๊ณ , ์™ธ๋ถ€์ด๋ฉด ๋ฒ„๋ฆฝ๋‹ˆ๋‹ค.
  4. ๋ชฉํ‘œ ํŒŒํ‹ฐํด ์ˆ˜์— ๋„๋‹ฌํ•  ๋•Œ๊นŒ์ง€ ๋ฐ˜๋ณตํ•ฉ๋‹ˆ๋‹ค.

๋ฐ”์šด๋”ฉ ๋ฐ•์Šค ๋Œ€๋น„ ๋ฉ”์‹œ ๋ถ€ํ”ผ๊ฐ€ ์ž‘์„์ˆ˜๋ก ๋ฒ„๋ ค์ง€๋Š” ํ›„๋ณด์ ์ด ๋งŽ์•„์ง‘๋‹ˆ๋‹ค. ๊ตฌ ํ˜•ํƒœ๋Š” ํšจ์œจ์ด ๋†’๊ณ (์•ฝ 52%), ๋‚ฉ์ž‘ํ•˜๊ฑฐ๋‚˜ ๊ตฌ๋ฉ์ด ๋งŽ์€ ํ˜•ํƒœ๋Š” ๋‚ฎ์Šต๋‹ˆ๋‹ค.

Odd-Even Rule (ํ™€์ง ๊ทœ์น™)

ํ›„๋ณด์ ์ด ๋ฉ”์‹œ ๋‚ด๋ถ€์ธ์ง€ ํŒ์ •ํ•˜๋Š” ์•Œ๊ณ ๋ฆฌ์ฆ˜์ž…๋‹ˆ๋‹ค.
์ž„์˜์˜ ์  P์—์„œ ์ž„์˜์˜ ๋ฐฉํ–ฅ์œผ๋กœ ๊ด‘์„ ์„ ๋ฐœ์‚ฌํ•ด ๋ฉ”์‹œ ํ‘œ๋ฉด๊ณผ ๊ต์ฐจํ•˜๋Š” ํšŸ์ˆ˜๋ฅผ ์…‰๋‹ˆ๋‹ค.

๊ต์ฐจ ํšŸ์ˆ˜ ํ™€์ˆ˜ โ†’ ๋‚ด๋ถ€
๊ต์ฐจ ํšŸ์ˆ˜ ์ง์ˆ˜ โ†’ ์™ธ๋ถ€ (๋˜๋Š” ๋ฉ”์‹œ ํ‘œ๋ฉด ์œ„)

์ด ์ฝ”๋“œ์—์„œ๋Š” +Y ๋ฐฉํ–ฅ์œผ๋กœ ๋ฐœ์‚ฌํ•˜๊ณ  ์›”๋“œ ๊ณต๊ฐ„์œผ๋กœ ๋ณ€ํ™˜๋œ ์‚ผ๊ฐํ˜• ๋ฐฐ์—ด ์ „์ฒด๋ฅผ ์ˆœํšŒํ•ฉ๋‹ˆ๋‹ค.

Mรถllerโ€“Trumbore ์•Œ๊ณ ๋ฆฌ์ฆ˜

๊ด‘์„ ๊ณผ ์‚ผ๊ฐํ˜•์˜ ๊ต์ฐจ ์—ฌ๋ถ€๋ฅผ ๊ณ„์‚ฐํ•˜๋Š” ์•Œ๊ณ ๋ฆฌ์ฆ˜์ž…๋‹ˆ๋‹ค. ๊ทธ๋ž˜ํ”ฝ์Šค ๊ต์žฌ์—์„œ ๊ด‘์„ -์‚ผ๊ฐํ˜• ๊ต์ฐจ์˜ ํ‘œ์ค€์œผ๋กœ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.
ํ–‰๋ ฌ ์—ญ๋ณ€ํ™˜ ์—†์ด ๋ฒกํ„ฐ ์™ธ์ ยท๋‚ด์  ์—ฐ์‚ฐ๋งŒ์œผ๋กœ ๊ต์ฐจ์ ์˜ ๋ฌด๊ฒŒ์ค‘์‹ฌ ์ขŒํ‘œ (u, v)์™€ ๊ด‘์„  ๊ฑฐ๋ฆฌ t๋ฅผ ๋™์‹œ์— ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค.

edge1 = B - A
edge2 = C - A
h     = Cross(direction, edge2)
det   = Dot(edge1, h)          โ† 0์— ๊ฐ€๊นŒ์šฐ๋ฉด ๊ด‘์„ ์ด ์‚ผ๊ฐํ˜•๊ณผ ํ‰ํ–‰
u     = Dot(origin-A, h) / det โ† ๋ฌด๊ฒŒ์ค‘์‹ฌ ์ขŒํ‘œ u
v     = Dot(direction, Cross(origin-A, edge1)) / det โ† ๋ฌด๊ฒŒ์ค‘์‹ฌ ์ขŒํ‘œ v
t     = Dot(edge2, Cross(origin-A, edge1)) / det     โ† ๊ต์ฐจ ๊ฑฐ๋ฆฌ

์กฐ๊ฑด: 0 โ‰ค u โ‰ค 1, 0 โ‰ค v, u+v โ‰ค 1, t > 0 โ†’ ๊ต์ฐจ

t > 0 ์กฐ๊ฑด์œผ๋กœ ๊ด‘์„  ๋’ค์ชฝ(๋ฐ˜๋Œ€ ๋ฐฉํ–ฅ)์˜ ๊ต์ฐจ๋ฅผ ์ œ์™ธํ•ฉ๋‹ˆ๋‹ค.

์›”๋“œ ๊ณต๊ฐ„ ๊ธฐ๋ฐ˜ ํŒ์ •

์‚ผ๊ฐํ˜•์„ ๋กœ์ปฌ ๊ณต๊ฐ„์ด ์•„๋‹Œ ์›”๋“œ ๊ณต๊ฐ„์œผ๋กœ ๋ณ€ํ™˜ํ•ด์„œ ์บ์‹ฑํ•ฉ๋‹ˆ๋‹ค.

Matrix4x4 l2w = meshTransform.localToWorldMatrix;
_triA[i] = l2w.MultiplyPoint3x4(verts[tris[i * 3 + 0]]);

FBX Import ์‹œ Unity๊ฐ€ ์ž๋™์œผ๋กœ ์ ์šฉํ•˜๋Š” ์Šค์ผ€์ผ(localScale = (0.01, 0.01, 0.01) ๋“ฑ)์ด ํŒ์ • ๊ณผ์ •์— ๊ทธ๋Œ€๋กœ ๋ฐ˜์˜๋ฉ๋‹ˆ๋‹ค. ๋กœ์ปฌ ๊ณต๊ฐ„์œผ๋กœ ํŒ์ •ํ•˜๋ฉด ์Šค์ผ€์ผ ๋ถˆ์ผ์น˜๋กœ ๋‚ด๋ถ€/์™ธ๋ถ€ ํŒ์ •์ด ์ „๋ฉด ์˜ค๋ฅ˜๊ฐ€ ๋‚ฉ๋‹ˆ๋‹ค.

์ˆ˜์น˜ ์•ˆ์ •์„ฑ โ€” ํ›„๋ณด์  jitter

ํ›„๋ณด์ ์ด ์‚ผ๊ฐํ˜• ์—ฃ์ง€๋ฅผ ์ •ํ™•ํžˆ ํ†ต๊ณผํ•˜๋ฉด ๊ต์ฐจ ํšŸ์ˆ˜๊ฐ€ 0 ๋˜๋Š” 2๋กœ ๊ณ„์‚ฐ๋˜์–ด ์™ธ๋ถ€๋กœ ์˜คํŒ๋ฉ๋‹ˆ๋‹ค. ์ด๋ฅผ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด ํŒ์ • ์ง์ „ ํ›„๋ณด์ ์— 0.0001 ์ˆ˜์ค€์˜ ๋ฏธ์„ธ ๋žœ๋ค ์˜คํ”„์…‹์„ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.

Vector3 jittered = candidate + new Vector3(
    (Random.value - 0.5f) * 0.0001f,
    (Random.value - 0.5f) * 0.0001f,
    (Random.value - 0.5f) * 0.0001f
);

์ €์žฅ๋˜๋Š”worldBasePos๋Š” ์ง€ํ„ฐ๊ฐ€ ์—†๋Š” ์›๋ž˜ ์ขŒํ‘œ๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.


ํ•ต์‹ฌ ๊ธฐ์ˆ : GPU Instancing

Graphics.DrawMeshInstanced

Unity์—์„œ GameObject ์—†์ด ๋ฉ”์‹œ๋ฅผ ๋ Œ๋”๋งํ•˜๋Š” API์ž…๋‹ˆ๋‹ค. ์”ฌ ๊ณ„์ธต(Hierarchy)์— ์˜ค๋ธŒ์ ํŠธ๋ฅผ ๋“ฑ๋กํ•˜์ง€ ์•Š๊ณ  ์œ„์น˜ยท์ƒ‰์ƒ ๋ฐฐ์—ด๋งŒ GPU์— ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค.

Graphics.DrawMeshInstanced(
    mesh,           // ๋ Œ๋”๋งํ•  ๋ฉ”์‹œ
    0,              // ์„œ๋ธŒ๋ฉ”์‹œ ์ธ๋ฑ์Šค
    material,       // ๋จธํ‹ฐ๋ฆฌ์–ผ
    batchMatrices,  // ์ธ์Šคํ„ด์Šค๋ณ„ TRS ํ–‰๋ ฌ ๋ฐฐ์—ด
    batch,          // ์ด๋ฒˆ ๋ฐฐ์น˜์˜ ์ธ์Šคํ„ด์Šค ์ˆ˜
    block           // ์ธ์Šคํ„ด์Šค๋ณ„ ์ƒ‰์ƒ (MaterialPropertyBlock)
);

ํ•œ ๋ฒˆ ํ˜ธ์ถœ์— ์ตœ๋Œ€ 1023๊ฐœ๊นŒ์ง€๋งŒ ์ฒ˜๋ฆฌํ•˜๋ฏ€๋กœ GPUInstanceRenderer๊ฐ€ while ๋ฃจํ”„๋กœ ๋ฐฐ์น˜๋ฅผ ๋ถ„ํ• ํ•ฉ๋‹ˆ๋‹ค.

UNITY_INSTANCING_BUFFER

URP์˜ SRP Batcher๋Š” GPU Instancing๊ณผ ๋™์‹œ์— ๋™์ž‘ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. MaterialPropertyBlock์œผ๋กœ ์ฃผ์ž…ํ•˜๋Š” ์ƒ‰์ƒ ๋ฐ์ดํ„ฐ๋ฅผ SRP Batcher๊ฐ€ ๋ฎ์–ด์“ฐ๋Š” ๋ฌธ์ œ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.

์ปค์Šคํ…€ ์…ฐ์ด๋”์—์„œ UNITY_INSTANCING_BUFFER_START/END ๋ธ”๋ก์œผ๋กœ ์ƒ‰์ƒ ํ”„๋กœํผํ‹ฐ๋ฅผ ์„ ์–ธํ•˜๋ฉด SRP Batcher ๋Œ€์‹  GPU Instancing ์ „์šฉ ๋ฒ„ํผ์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ์ฝ์Šต๋‹ˆ๋‹ค.

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

// ์…ฐ์ด๋” ๋‚ด๋ถ€์—์„œ ์ธ์Šคํ„ด์Šค๋ณ„ ์ƒ‰์ƒ์„ ์ฝ์Šต๋‹ˆ๋‹ค
float4 baseColor = UNITY_ACCESS_INSTANCED_PROP(Props, _BaseColor);

Matrix4x4.TRS

๊ฐ ํŒŒํ‹ฐํด์˜ ์œ„์น˜ยทํšŒ์ „ยทํฌ๊ธฐ๋ฅผ ํ•˜๋‚˜์˜ ํ–‰๋ ฌ๋กœ ํ‘œํ˜„ํ•ฉ๋‹ˆ๋‹ค.

_matrices[i] = Matrix4x4.TRS(
    animPos,              // Translation: ์›”๋“œ ์œ„์น˜
    Quaternion.identity,  // Rotation: ํšŒ์ „ ์—†์Œ
    Vector3.one * particleSize  // Scale: ๊ท ์ผ ํฌ๊ธฐ
);

GPU๋Š” ์ด ํ–‰๋ ฌ ํ•˜๋‚˜๋กœ ํ•ด๋‹น ์ธ์Šคํ„ด์Šค์˜ ๋ชจ๋“  ๊ณต๊ฐ„ ๋ณ€ํ™˜์„ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

ํ•ต์‹ฌ ๊ธฐ์ˆ : Perlin Noise ๋ถ€์œ  ์• ๋‹ˆ๋ฉ”์ด์…˜

์ด์ค‘ ๋ ˆ์ด์–ด ๊ตฌ์กฐ

๋ ˆ์ด์–ดํŒŒ๋ผ๋ฏธํ„ฐ์—ญํ• 
Layer 1baseJitterํ•ญ์ƒ ๋™์ž‘ํ•˜๋Š” ๋ฏธ์„ธ ์ง„๋™. floatAmplitude = 0์ด์–ด๋„ ํŒŒํ‹ฐํด์€ ์‚ด์•„์žˆ๋Š” ๋А๋‚Œ์œผ๋กœ ์›€์ง์ž…๋‹ˆ๋‹ค.
Layer 2floatAmplitude์ถ”๊ฐ€์ ์ธ ๋Œ€๋ฒ”์œ„ ์ด๋™. 0์ด๋ฉด ๋น„ํ™œ์„ฑ.

floatAmplitude = 0์ด๋ฉด Layer 1์˜ ์Šค์ผ€์ผ์„ 2๋ฐฐ๋กœ ํ‚ค์›Œ ์ฃผ ์›€์ง์ž„์„ ๋‹ด๋‹นํ•˜๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค.

3D Perlin Noise ๊ทผ์‚ฌ

Unity์˜ Mathf.PerlinNoise()๋Š” 2D์ž…๋‹ˆ๋‹ค. ์„ธ ์ถ• ๊ฐ๊ฐ ๋…๋ฆฝ์ ์ธ ์›€์ง์ž„์„ ๋งŒ๋“ค๊ธฐ ์œ„ํ•ด ์ถ•๋งˆ๋‹ค ๋‹ค๋ฅธ UV ์กฐํ•ฉ๊ณผ ํฐ ์œ„์ƒ ์˜คํ”„์…‹์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

float nx = SampleNoise(pos.x, pos.z, phase,         t); // X์ถ• ์ด๋™
float ny = SampleNoise(pos.y, pos.x, phase + 31.4f, t); // Y์ถ• ์ด๋™
float nz = SampleNoise(pos.z, pos.y, phase + 72.8f, t); // Z์ถ• ์ด๋™

phase๋Š” ํŒŒํ‹ฐํด๋งˆ๋‹ค ๋‹ค๋ฅธ ๋žœ๋ค ๊ฐ’์ด๋ฏ€๋กœ ๋ชจ๋“  ํŒŒํ‹ฐํด์ด ๋™์ผํ•˜๊ฒŒ ์›€์ง์ด๋Š” ๋™์กฐ ํ˜„์ƒ์ด ์—†์Šต๋‹ˆ๋‹ค.
SampleNoise()๋Š” 0~1 ๋ฒ”์œ„์ธ Perlin ์ถœ๋ ฅ์„ -1~+1๋กœ ์ •๊ทœํ™”ํ•ฉ๋‹ˆ๋‹ค.

float SampleNoise(float a, float b, float phase, float t)
{
    return (Mathf.PerlinNoise(a * noiseScale + t + phase, b * noiseScale + phase) - 0.5f) * 2f;
}

SoftClamp

ํŒŒํ‹ฐํด์ด ๋ถ€์œ  ์ค‘ ๋ฐ”์šด๋”ฉ ๋ฐ•์Šค ๋ฐ–์œผ๋กœ ๋‚˜๊ฐ€๋ฉด basePos ๋ฐฉํ–ฅ์œผ๋กœ 70% ๋ณต์›ํ•ฉ๋‹ˆ๋‹ค. ์™„์ „ํžˆ ํด๋žจํ”„ํ•˜๋ฉด ๊ฒฝ๊ณ„์—์„œ ํŒŒํ‹ฐํด์ด ๋”ฑ๋”ฑํ•˜๊ฒŒ ๋ฉˆ์ถ”๋Š” ๋А๋‚Œ์ด ๋‚˜๋ฏ€๋กœ Lerp๋กœ ๋ถ€๋“œ๋Ÿฝ๊ฒŒ ๋Œ์–ด๋‹น๊น๋‹ˆ๋‹ค.


์ธ์ŠคํŽ™ํ„ฐ ํŒŒ๋ผ๋ฏธํ„ฐ ์š”์•ฝ

ํŒŒ๋ผ๋ฏธํ„ฐ๋ถ„๋ฅ˜์„ค๋ช…
targetMeshFilter๊ตฌ์กฐํŒŒํ‹ฐํด์„ ์ฑ„์šธ FBX ์˜ค๋ธŒ์ ํŠธ์˜ MeshFilter
particleCount๊ตฌ์กฐ๋ฐฐ์น˜ํ•  ํŒŒํ‹ฐํด ์ด ์ˆ˜
colorSeed๊ตฌ์กฐ์ƒ‰์ƒ ๋ฌด์ž‘์œ„ ์‹œ๋“œ
particleSize์‹œ๊ฐํŒŒํ‹ฐํด ํ•˜๋‚˜์˜ ํฌ๊ธฐ
floatSpeed์‹œ๊ฐ๋…ธ์ด์ฆˆ ์‹œ๊ฐ„ ์ง„ํ–‰ ์†๋„
noiseScale์‹œ๊ฐ๋…ธ์ด์ฆˆ ๊ณต๊ฐ„ ์ฃผํŒŒ์ˆ˜ (ํด์ˆ˜๋ก ํŒŒํ‹ฐํด ๊ฐ„ ์›€์ง์ž„์ด ์ œ๊ฐ๊ฐ)
floatAmplitude์‹œ๊ฐ๋Œ€๋ฒ”์œ„ ๋ถ€์œ  ํฌ๊ธฐ. 0์ด์–ด๋„ ๊ธฐ๋ณธ ์ง„๋™์€ ๋™์ž‘ํ•จ
baseJitter์‹œ๊ฐํ•ญ์ƒ ์ ์šฉ๋˜๋Š” ์ตœ์†Œ ์ง„๋™ ํฌ๊ธฐ
hueBase์‹œ๊ฐ์ƒ‰์ƒ Hue ์‹œ์ž‘๊ฐ’
hueRange์‹œ๊ฐํŒŒํ‹ฐํด ๊ฐ„ Hue ๋ถ„ํฌ ๋ฒ”์œ„
saturation์‹œ๊ฐ์ฑ„๋„
brightness์‹œ๊ฐ๋ช…๋„

๊ตฌ์กฐ ํŒŒ๋ผ๋ฏธํ„ฐ๊ฐ€ ๋ณ€๊ฒฝ๋˜๋ฉด Generate()๊ฐ€ ์žฌ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค. ์‹œ๊ฐ ํŒŒ๋ผ๋ฏธํ„ฐ๋Š” Animate(0f)๋งŒ ์žฌ์‹คํ–‰๋˜์–ด ์ฆ‰์‹œ ๋ฐ˜์˜๋ฉ๋‹ˆ๋‹ค.


๋ฉ”์‹œ ์กฐ๊ฑด

  • ๋‹ซํžŒ ํ˜•ํƒœ(Closed Mesh) ์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๊ตฌ๋ฉ์ด ์žˆ์œผ๋ฉด ํ™€์ง ํŒ์ •์ด ์˜ค์ž‘๋™ํ•ฉ๋‹ˆ๋‹ค.
  • ํด๋ฆฌ๊ณค ์ˆ˜๊ฐ€ ๋งŽ์„์ˆ˜๋ก Generate() ์‹œ๊ฐ„์ด ๊ธธ์–ด์ง‘๋‹ˆ๋‹ค. ์‹ค์‹œ๊ฐ„ ๋ Œ๋”๋ง ์†๋„์—๋Š” ์˜ํ–ฅ์ด ์—†์Šต๋‹ˆ๋‹ค.
  • Blender์—์„œ Exportํ•  ๋•Œ Apply Transforms๋ฅผ ์ฒดํฌํ•˜๋ฉด ์Šค์ผ€์ผ ๋ฌธ์ œ๋ฅผ ๋ฐฉ์ง€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
profile
Coding Art with Blender / oF / Processing / p5.js / nannou

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