๐ŸซงArt_013 Particles on Sphere

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

Unity GenArt

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

๐Ÿ“„ParticleOnSphere.cs

// ์ค‘์‹ฌ์—์„œ ์Šคํฐ๋œ ํŒŒํ‹ฐํด์ด ๊ตฌ์ฒด ํ‘œ๋ฉด์œผ๋กœ ๋Œ๋ ค๋ถ™๋Š” ์•„ํŠธ์›Œํฌ.
// [ExecuteAlways] - ์—๋””ํ„ฐ ํ”„๋ฆฌ๋ทฐ ์ง€์›.
// ์š”๊ตฌ์‚ฌํ•ญ: Unity 6.0 + URP, Assets/Shaders/ShaderUnlit.shader

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
#if UNITY_EDITOR
using UnityEditor;
#endif

[ExecuteAlways]
[AddComponentMenu("GenArt/Attractor Artwork")]
public class AttractorArtwork : MonoBehaviour
{
    // === Spawn ===

    [Header("Spawn")]
    [Tooltip("๋™์‹œ์— ์กด์žฌํ•  ์ˆ˜ ์žˆ๋Š” ํŒŒํ‹ฐํด ์ตœ๋Œ€ ์ˆ˜. CPU ์‹œ๋ฎฌ๋ ˆ์ด์…˜์ด๋ฏ€๋กœ 20000 ์ดํ•˜ ๊ถŒ์žฅ.")]
    [Range(100, 20000)] public int maxCount = 5000;

    [Tooltip("์ดˆ๋‹น ํŒŒํ‹ฐํด ์ƒ์„ฑ ์ˆ˜.")]
    [Range(1f, 2000f)] public float spawnRate = 200f;

    // === Sphere Attractor ===

    [Header("Sphere Attractor")]
    [Tooltip("๋ชฉํ‘œ ๊ตฌ์ฒด์˜ ๋ฐ˜์ง€๋ฆ„.")]
    [Range(0.1f, 10f)] public float sphereRadius = 1.97f;

    [Tooltip("ํ‘œ๋ฉด์„ ํ–ฅํ•œ ์ธ๋ ฅ ์†๋„ ์Šค์ผ€์ผ.")]
    [Range(0.1f, 20f)] public float attractionSpeed = 5f;

    [Tooltip("์ธ๋ ฅ ๊ฐ•๋„.")]
    [Range(0f, 5f)] public float attractionForce = 0.78f;

    [Tooltip("ํ‘œ๋ฉด์— ๋‹ฌ๋ผ๋ถ™๊ธฐ ์‹œ์ž‘ํ•˜๋Š” ๊ฑฐ๋ฆฌ.")]
    [Range(0f, 3f)] public float stickDistance = 1.12f;

    [Tooltip("ํ‘œ๋ฉด ๊ณ ์ • ํž˜์˜ ๊ฐ•๋„.")]
    [Range(0f, 5f)] public float stickForce = 1.08f;

    // === Turbulence ===

    [Header("Turbulence")]
    [Tooltip("ํ„ฐ๋ทธ๋Ÿฐ์Šค ์„ธ๊ธฐ.")]
    [Range(0f, 3f)] public float turbIntensity = 0.55f;

    [Tooltip("ํ„ฐ๋ทธ๋Ÿฐ์Šค ๋“œ๋ž˜๊ทธ (์ง€์ˆ˜ ๊ฐ์‡ ).")]
    [Range(0f, 10f)] public float turbDrag = 2f;

    [Tooltip("๋…ธ์ด์ฆˆ ๊ณต๊ฐ„ ์ฃผํŒŒ์ˆ˜. ํด์ˆ˜๋ก ์„ธ๋ฐ€ํ•œ ํŒจํ„ด.")]
    [Range(0.1f, 10f)] public float turbFrequency = 2.11f;

    [Tooltip("๋…ธ์ด์ฆˆ ์‹œ๊ฐ„ ํ๋ฆ„ ์†๋„.")]
    [Range(0f, 2f)] public float turbTimeScale = 0.3f;

    // === Particle Appearance ===

    [Header("Particle Size")]
    [Range(0.001f, 0.1f)] public float minSize = 0.008f;
    [Range(0.001f, 0.15f)] public float maxSize = 0.035f;

    // === Color by Speed ===

    [Header("Color by Speed")]
    [Tooltip("์ €์† ํŒŒํ‹ฐํด ์ƒ‰์ƒ (ํ‘œ๋ฉด์— ๋ถ™์€ ์ƒํƒœ).")]
    public Color slowColor = new Color(0.65f, 0.25f, 1.0f);   // purple

    [Tooltip("๊ณ ์† ํŒŒํ‹ฐํด ์ƒ‰์ƒ (๋น ๋ฅด๊ฒŒ ์ด๋™ ์ค‘).")]
    public Color fastColor = new Color(0.15f, 1.0f, 0.85f);  // cyan

    [Tooltip("fastColor์— ๋Œ€์‘ํ•˜๋Š” ์†๋„ ์ƒํ•œ. ์ด ์†๋„ ์ด์ƒ์€ ๋ชจ๋‘ fastColor.")]
    [Range(0.05f, 10f)] public float speedRangeMax = 1.5f;

    // === ๋‚ด๋ถ€ ๊ตฌ์กฐ์ฒด ===

    private struct Particle
    {
        public Vector3 pos;
        public Vector3 vel;
        public float size;
        public float speedMag; // ์ด์ „ ํ”„๋ ˆ์ž„ ์†๋„ ํฌ๊ธฐ โ†’ ์ƒ‰์ƒ ๊ณ„์‚ฐ์šฉ
    }

    // === ๋Ÿฐํƒ€์ž„ ์ƒํƒœ ===

    private readonly List<Particle> _p = new();
    private float _spawnTimer;
    private float _time;

    private Mesh _mesh;
    private Material _mat;
    private MaterialPropertyBlock _mpb;

    private const int BATCH = 1023;
    private readonly Matrix4x4[] _mBuf = new Matrix4x4[BATCH];
    private readonly Vector4[] _cBuf = new Vector4[BATCH];

    // === Unity ๋ผ์ดํ”„์‚ฌ์ดํด ===

    void OnEnable()
    {
        _p.Clear();
        _spawnTimer = 0f;
        _time = 0f;
        InitResources();

        if (!Application.isPlaying)
            BuildEditorPreview();

#if UNITY_EDITOR
        SceneView.RepaintAll();
#endif
    }

    void OnDisable()
    {
        CoreUtils.Destroy(_mat);
        _mat = null;
    }

    void Update()
    {
        if (_mat == null || _mesh == null) return;

        if (Application.isPlaying)
        {
            float dt = Time.deltaTime;
            _time += dt;
            Spawn(dt);
            Simulate(dt);
        }

        Render();

#if UNITY_EDITOR
        if (!Application.isPlaying)
            SceneView.RepaintAll();
#endif
    }

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

    void InitResources()
    {
        if (_mesh == null)
            _mesh = Resources.GetBuiltinResource<Mesh>("Sphere.fbx");

        if (_mat == null)
        {
            var sh = Shader.Find("Custom/ShaderUnlit");
            if (sh == null)
            {
                Debug.LogError("[AttractorArtwork] ์…ฐ์ด๋” 'Custom/ShaderUnlit'๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. " +
                               "Assets/Shaders/SwirlUnlit.shader ๋ฅผ ํ”„๋กœ์ ํŠธ์— ์ถ”๊ฐ€ํ•˜์„ธ์š”.");
                return;
            }
            _mat = new Material(sh) { enableInstancing = true };
        }

        _mpb ??= new MaterialPropertyBlock();
    }

    // === ์—๋””ํ„ฐ ํ”„๋ฆฌ๋ทฐ ===

    // ์ •์ง€ ์ƒํƒœ์—์„œ ๊ตฌ์ฒด ํ‘œ๋ฉด ๋ถ„ํฌ ์Šค๋ƒ…์ƒท
    void BuildEditorPreview()
    {
        _p.Clear();
        int count = Mathf.Min(maxCount, 3000);
        for (int i = 0; i < count; i++)
        {
            _p.Add(new Particle
            {
                pos = Random.onUnitSphere * sphereRadius,
                vel = Vector3.zero,
                size = Random.Range(minSize, maxSize),
                speedMag = 0f
            });
        }
    }

    // === ์Šคํฐ ===

    // ์›”๋“œ ์›์ (transform.position ๊ธฐ์ค€) ์ค‘์‹ฌ์—์„œ ์ƒ์„ฑ
    void Spawn(float dt)
    {
        if (_p.Count >= maxCount) return;

        _spawnTimer += dt;
        float interval = 1f / Mathf.Max(spawnRate, 0.001f);

        while (_spawnTimer >= interval && _p.Count < maxCount)
        {
            _spawnTimer -= interval;

            // ์™„์ „ํ•œ ์›์ ์ด๋ฉด ๋ฐฉํ–ฅ ๊ณ„์‚ฐ ๋ถˆ๊ฐ€ โ†’ ์•„์ฃผ ์ž‘์€ ๋žœ๋ค ์˜คํ”„์…‹
            Vector3 startPos = Random.insideUnitSphere * 0.05f;
            if (startPos.sqrMagnitude < 1e-6f)
                startPos = new Vector3(0.001f, 0f, 0f);

            _p.Add(new Particle
            {
                pos = transform.position + startPos,
                vel = Random.onUnitSphere * 0.15f,
                size = Random.Range(minSize, maxSize),
                speedMag = 0f
            });
        }

        // ํ”„๋ ˆ์ž„ ๋“œ๋กญ์œผ๋กœ ํƒ€์ด๋จธ ํญ๋ฐœ ๋ฐฉ์ง€
        if (_spawnTimer > interval * 5f)
            _spawnTimer = 0f;
    }

    // === ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ===

    void Simulate(float dt)
    {
        Vector3 center = transform.position;
        float expDrag = Mathf.Exp(-turbDrag * dt);

        for (int i = 0; i < _p.Count; i++)
        {
            var p = _p[i];

            Vector3 fromCenter = p.pos - center;
            float distFromC = fromCenter.magnitude;
            Vector3 radialDir = distFromC > 0.001f ? fromCenter / distFromC : Vector3.up;

            // ๊ตฌ์ฒด ํ‘œ๋ฉด ๋ชฉํ‘œ ์ง€์ 
            Vector3 surfacePoint = center + radialDir * sphereRadius;
            Vector3 toSurface = surfacePoint - p.pos;
            float distToSurf = toSurface.magnitude;

            // 1. ์ธ๋ ฅ (Attractor): ํ‘œ๋ฉด ๋ฐฉํ–ฅ์œผ๋กœ ์ง€์† ํž˜
            p.vel += toSurface.normalized * (attractionForce * attractionSpeed * dt);

            // 2. ๊ณ ์ฐฉ๋ ฅ (Stick): ํ‘œ๋ฉด ๊ทผ์ฒ˜์—์„œ ์„ ํ˜• ์ฆ๊ฐ€ํ•˜๋Š” ์ˆ˜๋ ด ํž˜
            if (distToSurf < stickDistance)
            {
                float t = 1f - (distToSurf / stickDistance);  // 0(๊ฒฝ๊ณ„) ~ 1(ํ‘œ๋ฉด)
                p.vel += toSurface * (stickForce * t * dt);
            }

            // 3. ํ„ฐ๋ทธ๋Ÿฐ์Šค: ์ปฌ ๋…ธ์ด์ฆˆ(curl noise) ๊ทผ์‚ฌ โ†’ ํ‘œ๋ฉด์— ํ๋ฆ„ ์ƒ์„ฑ
            p.vel += CurlNoise(p.pos, _time) * dt;

            // 4. ๋“œ๋ž˜๊ทธ (์ง€์ˆ˜ ๊ฐ์‡ )
            p.vel *= expDrag;

            // 5. ์œ„์น˜ ์ ๋ถ„
            p.pos += p.vel * dt;

            // 6. ์†๋„ ํฌ๊ธฐ ์บ์‹œ (์ƒ‰์ƒ์šฉ)
            p.speedMag = p.vel.magnitude;

            _p[i] = p;
        }
    }

    // === ์ปฌ ๋…ธ์ด์ฆˆ (Curl Noise) ===

    // Perlin ๋…ธ์ด์ฆˆ 6ํšŒ ์ƒ˜ํ”Œ๋กœ ๋ฐœ์‚ฐ ์—†๋Š” 3D ๋ฒกํ„ฐ์žฅ ๊ทผ์‚ฌ.
    // ๊ตฌ์ฒด ํ‘œ๋ฉด์—์„œ ํŒŒํ‹ฐํด์ด ํฉ์–ด์ง€์ง€ ์•Š๊ณ  ํ‘œ๋ฉด์„ ๋”ฐ๋ผ ํ๋ฅด๋Š” ํšจ๊ณผ.
    Vector3 CurlNoise(Vector3 pos, float t)
    {
        float f = turbFrequency;
        float tm = t * turbTimeScale;

        // ๊ฐ ์ถ•์— ๋Œ€ํ•ด ํŽธ๋ฏธ๋ถ„ ๊ทผ์‚ฌ: โˆ‚Fz/โˆ‚y - โˆ‚Fy/โˆ‚z ๋“ฑ
        float nx = N(pos.y * f, pos.z * f + tm) - N(pos.z * f, pos.y * f + tm);
        float ny = N(pos.z * f, pos.x * f + tm) - N(pos.x * f, pos.z * f + tm);
        float nz = N(pos.x * f, pos.y * f + tm) - N(pos.y * f, pos.x * f + tm);

        return new Vector3(nx, ny, nz) * turbIntensity;
    }

    // Perlin ๋…ธ์ด์ฆˆ๋ฅผ -0.5~0.5 ๋ฒ”์œ„๋กœ ์ •๊ทœํ™”
    float N(float x, float y) => Mathf.PerlinNoise(x + 100f, y + 100f) - 0.5f;

    // === ๋ Œ๋”๋ง ===

    // 1023 ๋ฐฐ์น˜ ๋‹จ์œ„ DrawMeshInstanced
    void Render()
    {
        if (_mat == null || _mesh == null || _p.Count == 0) return;

        int total = _p.Count;
        int offset = 0;

        while (offset < total)
        {
            int n = Mathf.Min(BATCH, total - offset);

            for (int i = 0; i < n; i++)
            {
                var p = _p[offset + i];

                _mBuf[i] = Matrix4x4.TRS(p.pos, Quaternion.identity, Vector3.one * p.size);

                // ์†๋„ ๊ธฐ๋ฐ˜ ์ƒ‰์ƒ lerp
                float tc = Mathf.Clamp01(p.speedMag / Mathf.Max(speedRangeMax, 0.001f));
                Color c = Color.Lerp(slowColor, fastColor, tc);
                _cBuf[i] = new Vector4(c.r, c.g, c.b, 1f);
            }

            _mpb.Clear();
            _mpb.SetVectorArray("_BaseColor", _cBuf);
            Graphics.DrawMeshInstanced(_mesh, 0, _mat, _mBuf, n, _mpb);

            offset += n;
        }
    }

    // === ์—๋””ํ„ฐ ํ›… ===

#if UNITY_EDITOR
    void OnValidate()
    {
        if (Application.isPlaying) return;
        if (_mesh == null || _mat == null) InitResources();
        if (_mesh != null && _mat != null)
        {
            BuildEditorPreview();
            SceneView.RepaintAll();
        }
    }

    // Scene ๋ทฐ์—์„œ ์–ดํŠธ๋ž™ํ„ฐ ๊ตฌ์ฒด Gizmo ํ‘œ์‹œ
    void OnDrawGizmosSelected()
    {
        Gizmos.color = new Color(0.2f, 1f, 0.5f, 0.25f);
        Gizmos.DrawWireSphere(transform.position, sphereRadius);
        Gizmos.color = new Color(0.2f, 1f, 0.5f, 0.08f);
        Gizmos.DrawSphere(transform.position, sphereRadius);
    }
#endif
}

๐Ÿ“„ShaderUnlit.shader

// SwirlUnlit.shader
// URP ์–ธ๋ฆฟ ์…ฐ์ด๋”. UNITY_INSTANCING_BUFFER๋กœ ์ธ์Šคํ„ด์Šค๋ณ„ _BaseColor ์ง€์›.
// Assets/Shaders/ ํด๋”์— ๋ฐฐ์น˜.

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

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

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

            HLSLPROGRAM
            #pragma vertex   Vert
            #pragma fragment Frag
            #pragma multi_compile_instancing
            #pragma instancing_options assumeuniformscaling

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

            // === Per-Instance Property Buffer ===
            UNITY_INSTANCING_BUFFER_START(UnityPerMaterial)
                UNITY_DEFINE_INSTANCED_PROP(float4, _BaseColor)
            UNITY_INSTANCING_BUFFER_END(UnityPerMaterial)

            struct Attributes
            {
                float4 posOS : POSITION;
                UNITY_VERTEX_INPUT_INSTANCE_ID
            };

            struct Varyings
            {
                float4 posHCS : SV_POSITION;
                float4 color  : COLOR0;
                UNITY_VERTEX_INPUT_INSTANCE_ID
            };

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

                OUT.posHCS = TransformObjectToHClip(IN.posOS.xyz);
                OUT.color  = UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial, _BaseColor);
                return OUT;
            }

            half4 Frag(Varyings IN) : SV_Target
            {
                UNITY_SETUP_INSTANCE_ID(IN);
                return half4(IN.color);
            }

            ENDHLSL
        }
    }
}

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

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