๐ŸซงArt_027 Sphere Flow Particle System [๊ฒŒ์ž„์˜ค๋ธŒ์ ํŠธ]

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

Unity GenArt

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

1. ๊ฐœ์š” ๋ฐ ๋™์ž‘ ์›๋ฆฌ

1. ์›์  ์Šคํฐ ๋ฐ ํ‘œ๋ฉด ์ด๋™ (birthDuration)

  • ํŒŒํ‹ฐํด์€ ์ƒ์„ฑ ์‹œ Vector3.zero(์›์ )์—์„œ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค.
  • age / birthDuration ๋น„์œจ์„ ์‚ฌ์šฉํ•˜์—ฌ 0์ดˆ์—์„œ ์„ค์ •๋œ ์‹œ๊ฐ„๊นŒ์ง€ ์„ ํ˜• ๋ณด๊ฐ„(Lerp)์œผ๋กœ ํ‘œ๋ฉด๊นŒ์ง€ ์ด๋™ํ•ฉ๋‹ˆ๋‹ค.
  • ์ด ๊ธฐ๊ฐ„ ๋™์•ˆ์€ ๋…ธ์ด์ฆˆ ํ๋ฆ„์ด ์ ์šฉ๋˜์ง€ ์•Š์•„, ๋งˆ์น˜ ์ค‘์‹ฌ์—์„œ ๋ถ„์ถœ๋˜์–ด ํ‘œ๋ฉด์œผ๋กœ ์•ˆ์ฐฉํ•˜๋Š” ๋“ฏํ•œ ์—ฐ์ถœ์„ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค.

2. ํ•ด๋ฅ˜์™€ ๊ฐ™์€ ๋…ธ์ด์ฆˆ ํ•„๋“œ ๊ตฌํ˜„

  • Unity์˜ Mathf.PerlinNoise๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ 2D ํ•จ์ˆ˜์ž…๋‹ˆ๋‹ค. ์ด๋ฅผ 3D ๊ตฌ ํ‘œ๋ฉด์—์„œ ํ•ด๋ฅ˜์ฒ˜๋Ÿผ ํ๋ฅด๋„๋ก ๋งŒ๋“ค๊ธฐ ์œ„ํ•ด ์˜์‚ฌ 3D ๋ฒกํ„ฐ์žฅ(Pseudo-3D Vector Field) ๊ธฐ๋ฒ•์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
  • nx, ny, nz๋ฅผ ์„œ๋กœ ๋‹ค๋ฅธ ์ขŒํ‘œ ์กฐํ•ฉ๊ณผ ์‹œ๊ฐ„ ์˜คํ”„์…‹์œผ๋กœ ์ƒ˜ํ”Œ๋งํ•˜์—ฌ, ๊ณต๊ฐ„์ ์œผ๋กœ ์ผ๊ด€๋˜๋ฉด์„œ๋„ ํšŒ์ „ ์„ฑ๋ถ„์„ ๊ฐ€์ง„ ๋ฒกํ„ฐ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
  • Vector3.Dot(noiseVector, normal) * normal ๊ณ„์‚ฐ์„ ํ†ตํ•ด ๋…ธ์ด์ฆˆ ๋ฒกํ„ฐ์—์„œ ๋ฐ˜์ง€๋ฆ„ ๋ฐฉํ–ฅ ์„ฑ๋ถ„(๊ตฌ ๋ฐ–์œผ๋กœ ๋‚˜๊ฐ€๋Š” ํž˜)์„ ์ œ๊ฑฐํ•ฉ๋‹ˆ๋‹ค. ๊ฒฐ๊ณผ์ ์œผ๋กœ ํŒŒํ‹ฐํด์€ ํ•ญ์ƒ ๊ตฌ ํ‘œ๋ฉด์— ์ ‘ํ•˜๋Š” ๋ฐฉํ–ฅ(Tangent)์œผ๋กœ๋งŒ ํž˜์„ ๋ฐ›์•„ ํ‘œ๋ฉด ์œ„๋ฅผ ๋ฏธ๋„๋Ÿฌ์ง€๋“ฏ ํ๋ฅด๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.
  • ๋งˆ์ง€๋ง‰์œผ๋กœ .normalized * sphereRadius๋ฅผ ์ ์šฉํ•˜์—ฌ ์œ„์น˜๊ฐ€ ๊ตฌ ํ‘œ๋ฉด์—์„œ ๋ฒ—์–ด๋‚˜์ง€ ์•Š๋„๋ก ๊ฐ•์ œ ๊ตฌ์†ํ•ฉ๋‹ˆ๋‹ค.

3. 1/n ๊ท ๋“ฑ ํ™•๋ฅ  ๋ฐ ํ’€๋ง

  • validPrefabs ๋ฐฐ์—ด์˜ ๊ธธ์ด n์— ๋Œ€ํ•ด Random.Range(0, n)์„ ํ˜ธ์ถœํ•˜์—ฌ ๊ฐ ํ”„๋ฆฌํŒน์ด ์ •ํ™•ํžˆ 1/n์˜ ํ™•๋ฅ ๋กœ ์„ ํƒ๋ฉ๋‹ˆ๋‹ค.
  • ์„ ํƒ๋œ ํ”„๋ฆฌํŒน์— ํ• ๋‹น๋œ ์ „์šฉ ํ’€์—์„œ๋งŒ ๋น„ํ™œ์„ฑํ™” ๊ฐ์ฒด๋ฅผ ์žฌ์‚ฌ์šฉํ•˜๋ฏ€๋กœ, ํ”„๋ฆฌํŒน ํƒ€์ž…๋ณ„ ๋ฉ”๋ชจ๋ฆฌ ์ง€์—ญ์„ฑ์ด ์œ ์ง€๋˜๊ณ  ๊ฐ€๋น„์ง€ ์ปฌ๋ ‰์…˜ ๋ถ€ํ•˜๊ฐ€ ์ตœ์†Œํ™”๋ฉ๋‹ˆ๋‹ค.

4. Inspector ๊ถŒ์žฅ ์„ค์ •๊ฐ’

ํ•ญ๋ชฉ์„ค๋ช…์ถ”์ฒœ๊ฐ’
Max Particles๋™์‹œ ํ™œ์„ฑ ํŒŒํ‹ฐํด ์ˆ˜ (์„ฑ๋Šฅ์— ์˜ํ–ฅ)200 ~ 600
Sphere Radius๊ตฌ์˜ ํฌ๊ธฐ5.0
Noise Frequencyํ๋ฅด๋Š” ๋ฌผ๊ฒฐ์˜ ๋ฐ€๋„0.4 ~ 0.8
Noise Strengthํ๋ฆ„์˜ ์ง„ํญ (ํ‘œ๋ฉด์„ ๋ฒ—์–ด๋‚˜์ง€ ์•Š๊ฒŒ ์ •๊ทœํ™”๋จ)1.5 ~ 3.0
Flow Speedํ•ด๋ฅ˜์˜ ์ด๋™ ์†๋„1.0 ~ 3.0
Birth Duration์ค‘์‹ฌ์—์„œ ํ‘œ๋ฉด๊นŒ์ง€ ๋„๋‹ฌ ์‹œ๊ฐ„0.8 ~ 1.5

4. ์ ์šฉ ์‹œ ์ฐธ๊ณ  ์‚ฌํ•ญ

  1. ํ”„๋ฆฌํŒน ์ค‘์‹ฌ์ถ• ํ™•์ธ: ๋ชจ๋ธ๋ง ๋ฉ”์‹œ์˜ ๋กœ์ปฌ ์ค‘์‹ฌ(pivot)์ด ๊ธฐํ•˜ํ•™์  ์ค‘์‹ฌ์— ์œ„์น˜ํ•ด์•ผ ํ‘œ๋ฉด ๊ตฌ์† ์‹œ ๊ธฐ์šธ์–ด์ง ์—†์ด ๋งค๋„๋Ÿฝ๊ฒŒ ์›€์ง์ž…๋‹ˆ๋‹ค. ์ค‘์‹ฌ์ด ์–ด๊ธ‹๋‚˜๋ฉด ํ”„๋ฆฌํŒน ๋‚ด๋ถ€์— ๋นˆ GameObject๋ฅผ ์ค‘์‹ฌ์œผ๋กœ ์žก๊ณ  ๋ฉ”์‹œ๋ฅผ ํ•˜์œ„๋กœ ๋ฐฐ์น˜ํ•˜์„ธ์š”.
  2. ์„ฑ๋Šฅ ๊ด€๋ฆฌ: maxParticles๋Š” ์‹ค์ œ ํ™”๋ฉด์— ํ•„์š”๋กœ ํ•˜๋Š” ์ตœ๋Œ€ ๊ฐœ์ˆ˜๋กœ ์„ค์ •ํ•˜์„ธ์š”. ํ’€์ด ๊ฐ€๋“ ์ฐจ๋ฉด ์ƒˆ ํŒŒํ‹ฐํด ์ƒ์„ฑ์ด ์Šคํ‚ต๋˜๋ฏ€๋กœ, ํ”„๋ ˆ์ž„ ๋“œ๋กญ ์—†์ด ์•ˆ์ •์ ์œผ๋กœ ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค.
  3. ๋…ธ์ด์ฆˆ ํŒจํ„ด ๋‹ค์–‘ํ™”: noiseFrequency๋ฅผ ๋‚ฎ์ถ”๋ฉด ๋„“์€ ํ•ด๋ฅ˜์ฒ˜๋Ÿผ ๋А๋ฆฌ๊ณ  ๊ฑฐ๋Œ€ํ•œ ํ๋ฆ„์ด, ๋†’์œผ๋ฉด ์ž‘์€ ์†Œ์šฉ๋Œ์ด๊ฐ€ ๋งŽ์ด ์ƒ๊ธฐ๋Š” ํšจ๊ณผ๋ฅผ ์–ป์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์‹คํ–‰ ์ค‘ Inspector ์—์„œ ์‹ค์‹œ๊ฐ„์œผ๋กœ ์กฐ์ •ํ•˜๋ฉฐ ์›ํ•˜๋Š” ํ๋ฆ„์„ ์ฐพ์œผ์„ธ์š”.

2. ์Šคํฌ๋ฆฝํŠธ

using UnityEngine;
using System.Collections.Generic;

public class SphereFlowParticleSystem : MonoBehaviour
{
    // =========================================================
    // [Inspector ๋…ธ์ถœ ๋ณ€์ˆ˜: ํ”„๋ฆฌํŒน ๋ฐ ํ’€ ์„ค์ •]
    // =========================================================
    [Header("Prefab Settings")]
    [Tooltip("์ตœ๋Œ€ 5๊ฐœ์˜ ํ”„๋ฆฌํŒน์„ ํ• ๋‹นํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ตœ์†Œ 1๊ฐœ๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.")]
    public GameObject[] particlePrefabs = new GameObject[5];
    public int maxParticles = 300;        // ์‹œ์Šคํ…œ ์ „์ฒด์—์„œ ๊ด€๋ฆฌํ•  ํŒŒํ‹ฐํด ์ด ๊ฐœ์ˆ˜
    public float lifetime = 8f;           // ํŒŒํ‹ฐํด ์ˆ˜๋ช… (์ดˆ)
    public float birthDuration = 1.2f;    // ์›์ ์—์„œ ๊ตฌ ํ‘œ๋ฉด๊นŒ์ง€ ๋„๋‹ฌํ•˜๋Š” ๋ฐ ๊ฑธ๋ฆฌ๋Š” ์‹œ๊ฐ„

    // =========================================================
    // [Inspector ๋…ธ์ถœ ๋ณ€์ˆ˜: ๊ตฌ ํ‘œ๋ฉด ๋ฐ ๋…ธ์ด์ฆˆ ํ๋ฆ„ ์„ค์ •]
    // =========================================================
    [Header("Sphere & Flow Settings")]
    public float sphereRadius = 5f;       // ํŒŒํ‹ฐํด์ด ์›€์ง์ผ ๊ตฌ์˜ ๋ฐ˜์ง€๋ฆ„
    public float noiseFrequency = 0.6f;   // ๋…ธ์ด์ฆˆ ํ•„๋“œ์˜ ์ฃผํŒŒ์ˆ˜ (๊ฐ’์ด ๋†’์„์ˆ˜๋ก ํ๋ฅด๋Š” ํŒจํ„ด์ด ์ž‘๊ณ  ๋น ๋ฆ„)
    public float noiseStrength = 2.0f;    // ๋…ธ์ด์ฆˆ์— ์˜ํ•œ ์ด๋™๋ ฅ (ํ๋ฆ„์˜ ์„ธ๊ธฐ)
    public float flowSpeed = 1.5f;        // ํ•ด๋ฅ˜ ํ๋ฆ„์˜ ๊ธฐ๋ณธ ์†๋„
    public float flowTimeScale = 0.8f;    // ๋…ธ์ด์ฆˆ ํ•„๋“œ์˜ ์‹œ๊ฐ„ ์ง„ํ–‰ ์†๋„ (ํด์ˆ˜๋ก ํŒจํ„ด ๋ณ€ํ™”๊ฐ€ ๋น ๋ฆ„)

    // =========================================================
    // [Inspector ๋…ธ์ถœ ๋ณ€์ˆ˜: ํšŒ์ „ ์„ค์ •]
    // =========================================================
    [Header("Rotation Settings")]
    public bool enableRandomStartRotation = true;  // ์ƒ์„ฑ ์‹œ ๋žœ๋ค ์ดˆ๊ธฐ ํšŒ์ „ ์ ์šฉ
    public bool enableContinuousRotation = true;   // ์ง€์†์  ์ž์ „ ์ ์šฉ
    public Vector3 minAngularVelocity = new Vector3(-20f, -20f, -20f); // ์ตœ์†Œ ๊ฐ์†๋„
    public Vector3 maxAngularVelocity = new Vector3(20f, 20f, 20f);    // ์ตœ๋Œ€ ๊ฐ์†๋„

    // =========================================================
    // [Private ๋ฉค๋ฒ„ ๋ณ€์ˆ˜]
    // =========================================================
    private List<GameObject> validPrefabs = new List<GameObject>();
    private Dictionary<GameObject, List<ParticleData>> prefabPools = new Dictionary<GameObject, List<ParticleData>>();
    private float spawnTimer = 0f;
    
    // ํŒŒํ‹ฐํด ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ์ฒด
    private struct ParticleData
    {
        public GameObject gameObject;         // ํŒŒํ‹ฐํด ์˜ค๋ธŒ์ ํŠธ
        public GameObject assignedPrefab;     // ํ• ๋‹น๋œ ํ”„๋ฆฌํŒน ํƒ€์ž…
        public Vector3 initialDirection;      // ์›์ ์—์„œ ํ‘œ๋ฉด์œผ๋กœ ํ–ฅํ•˜๋Š” ์ดˆ๊ธฐ ๋ฐฉํ–ฅ
        public Vector3 angularVelocity;       // ์ž์ „ ๊ฐ์†๋„
        public float remainingLife;           // ๋‚จ์€ ์ˆ˜๋ช…
        public float age;                     // ์ƒ์„ฑ ํ›„ ๊ฒฝ๊ณผ ์‹œ๊ฐ„
        public bool isActive;                 // ํ™œ์„ฑํ™” ์ƒํƒœ
    }

    // =========================================================
    // [Unity LifeCycle: OnValidate]
    // =========================================================
    void OnValidate()
    {
        validPrefabs.Clear();
        foreach (var p in particlePrefabs)
        {
            if (p != null && !validPrefabs.Contains(p))
                validPrefabs.Add(p);
        }

        // ๋ฒ”์œ„ ๋ณด์ •
        if (minAngularVelocity.x > maxAngularVelocity.x) maxAngularVelocity.x = minAngularVelocity.x;
        if (minAngularVelocity.y > maxAngularVelocity.y) maxAngularVelocity.y = minAngularVelocity.y;
        if (minAngularVelocity.z > maxAngularVelocity.z) maxAngularVelocity.z = minAngularVelocity.z;
        
        if (birthDuration <= 0f) birthDuration = 0.1f;
        if (sphereRadius <= 0.1f) sphereRadius = 0.1f;
    }

    // =========================================================
    // [Unity LifeCycle: Start]
    // =========================================================
    void Start()
    {
        validPrefabs.Clear();
        foreach (var p in particlePrefabs)
        {
            if (p != null && !validPrefabs.Contains(p))
                validPrefabs.Add(p);
        }

        if (validPrefabs.Count == 0)
        {
            Debug.LogError("[SphereFlowParticleSystem] ๋™์ž‘ํ•˜๋ ค๋ฉด ์ตœ์†Œ 1๊ฐœ์˜ ํ”„๋ฆฌํŒน์ด ํ• ๋‹น๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.");
            enabled = false;
            return;
        }

        InitializePools();
    }

    // =========================================================
    // [Private Method: ํ’€ ์ดˆ๊ธฐํ™” ๋ฐ ๊ท ๋“ฑ ๋ถ„๋ฐฐ]
    // =========================================================
    void InitializePools()
    {
        int validCount = validPrefabs.Count;
        int baseCount = maxParticles / validCount;
        int remainder = maxParticles % validCount;

        foreach (var prefab in validPrefabs)
        {
            List<ParticleData> pool = new List<ParticleData>();
            int countForPrefab = baseCount + (remainder > 0 ? 1 : 0);
            remainder--;

            for (int i = 0; i < countForPrefab; i++)
            {
                GameObject go = Instantiate(prefab, transform);
                go.SetActive(false);
                pool.Add(new ParticleData
                {
                    gameObject = go,
                    assignedPrefab = prefab,
                    initialDirection = Vector3.zero,
                    angularVelocity = Vector3.zero,
                    remainingLife = 0f,
                    age = 0f,
                    isActive = false
                });
            }
            prefabPools[prefab] = pool;
        }
    }

    // =========================================================
    // [Unity LifeCycle: Update]
    // =========================================================
    void Update()
    {
        spawnTimer += Time.deltaTime;
        if (spawnTimer >= 1f / Mathf.Max(0.1f, maxParticles / lifetime))
        {
            spawnTimer = 0f;
            SpawnParticle();
        }

        UpdateParticles();
    }

    // =========================================================
    // [Private Method: ํŒŒํ‹ฐํด ์ƒ์„ฑ (1/n ํ™•๋ฅ )]
    // =========================================================
    void SpawnParticle()
    {
        if (validPrefabs.Count == 0) return;

        // 1/n ๊ท ๋“ฑ ํ™•๋ฅ ๋กœ ํ”„๋ฆฌํŒน ์„ ํƒ
        GameObject selectedPrefab = validPrefabs[Random.Range(0, validPrefabs.Count)];
        List<ParticleData> targetPool = prefabPools[selectedPrefab];
        ParticleData? target = null;

        // ํ•ด๋‹น ํ’€์—์„œ ๋น„ํ™œ์„ฑํ™”๋œ ํŒŒํ‹ฐํด ํƒ์ƒ‰
        for (int i = 0; i < targetPool.Count; i++)
        {
            if (!targetPool[i].isActive)
            {
                target = targetPool[i];
                break;
            }
        }

        // ํ’€์ด ๊ฐ€๋“ ์ฐฌ ๊ฒฝ์šฐ ๊ฒฝ๊ณ  ํ›„ ์Šคํ‚ต (์„ฑ๋Šฅ ์•ˆ์ •ํ™”๋ฅผ ์œ„ํ•ด ๋Ÿฐํƒ€์ž„ ์ƒ์„ฑ ๋ฐฉ์ง€)
        if (target == null) return;

        ParticleData p = target.Value;
        Transform t = p.gameObject.transform;

        // ์›์  ์Šคํฐ ๋ฐ ์ดˆ๊ธฐ ๋ฐฉํ–ฅ ์„ค์ •
        t.position = Vector3.zero;
        p.initialDirection = Random.onUnitSphere; // ํ‘œ๋ฉด์œผ๋กœ ํ–ฅํ•  ๋ฐฉํ–ฅ ์ €์žฅ
        
        // ๋žœ๋ค ๊ฐ์†๋„ ์„ค์ •
        p.angularVelocity = new Vector3(
            Random.Range(minAngularVelocity.x, maxAngularVelocity.x),
            Random.Range(minAngularVelocity.y, maxAngularVelocity.y),
            Random.Range(minAngularVelocity.z, maxAngularVelocity.z)
        );

        // ์ดˆ๊ธฐ ํšŒ์ „ ์ ์šฉ
        t.rotation = enableRandomStartRotation ? Random.rotationUniform : Quaternion.identity;

        // ์ƒํƒœ ์ดˆ๊ธฐํ™”
        p.remainingLife = lifetime;
        p.age = 0f;
        p.isActive = true;
        p.gameObject.SetActive(true);

        // ํ’€ ๋ฐ์ดํ„ฐ ๊ฐฑ์‹ 
        int idx = targetPool.IndexOf(target.Value);
        targetPool[idx] = p;
    }

    // =========================================================
    // [Private Method: ํŒŒํ‹ฐํด ์—…๋ฐ์ดํŠธ (๋…ธ์ด์ฆˆ ํ•„๋“œ + ํ‘œ๋ฉด ๊ตฌ์†)]
    // =========================================================
    void UpdateParticles()
    {
        float dt = Time.deltaTime;
        float currentTime = Time.time * flowTimeScale;

        foreach (var pool in prefabPools.Values)
        {
            for (int i = 0; i < pool.Count; i++)
            {
                if (!pool[i].isActive) continue;

                ParticleData p = pool[i];
                Transform t = p.gameObject.transform;

                // ์ˆ˜๋ช… ๋ฐ ๊ฒฝ๊ณผ ์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ
                p.age += dt;
                p.remainingLife -= dt;

                // -------------------------------------------------
                // [1. ์ถœ์ƒ ์• ๋‹ˆ๋ฉ”์ด์…˜: ์›์  -> ๊ตฌ ํ‘œ๋ฉด ์ด๋™]
                // -------------------------------------------------
                float birthProgress = Mathf.Clamp01(p.age / birthDuration);
                Vector3 targetSurfacePos = p.initialDirection * sphereRadius;
                
                // ์ถœ์ƒ ๊ตฌ๊ฐ„์—์„œ๋Š” ํ‘œ๋ฉด์œผ๋กœ ๋ถ€๋“œ๋Ÿฝ๊ฒŒ ๋ณด๊ฐ„
                if (birthProgress < 1f)
                {
                    t.position = Vector3.Lerp(Vector3.zero, targetSurfacePos, birthProgress);
                }
                else
                {
                    // -------------------------------------------------
                    // [2. ๋…ธ์ด์ฆˆ ํ•„๋“œ ๊ธฐ๋ฐ˜ ํ•ด๋ฅ˜ ํ๋ฆ„ (ํ‘œ๋ฉด ๊ตฌ์†)]
                    // -------------------------------------------------
                    Vector3 pos = t.position;
                    Vector3 normal = pos.normalized; // ํ˜„์žฌ ์œ„์น˜์˜ ๊ตฌ ํ‘œ๋ฉด ๋ฒ•์„ 

                    // 3D ๊ณต๊ฐ„์—์„œ์˜ ์ผ๊ด€๋œ ๋…ธ์ด์ฆˆ ๋ฒกํ„ฐ ์ƒ์„ฑ (์˜์‚ฌ 3D Perlin)
                    // ๊ฐ ์ถ•์„ ์ˆœํ™˜ ์ฐธ์กฐํ•˜์—ฌ ์†Œ์šฉ๋Œ์ด(curl) ์„ฑ์งˆ์˜ ํ๋ฆ„ ์ƒ์„ฑ
                    float nx = Mathf.PerlinNoise(pos.y * noiseFrequency + currentTime, pos.z * noiseFrequency + currentTime * 0.3f) * 2f - 1f;
                    float ny = Mathf.PerlinNoise(pos.z * noiseFrequency + currentTime * 0.2f, pos.x * noiseFrequency + currentTime) * 2f - 1f;
                    float nz = Mathf.PerlinNoise(pos.x * noiseFrequency + currentTime * 0.4f, pos.y * noiseFrequency + currentTime * 0.1f) * 2f - 1f;

                    Vector3 noiseVector = new Vector3(nx, ny, nz) * noiseStrength;

                    // ๋…ธ์ด์ฆˆ ๋ฒกํ„ฐ์˜ ๋ฐฉ์‚ฌํ˜• ์„ฑ๋ถ„์„ ์ œ๊ฑฐํ•˜์—ฌ ๊ตฌ ํ‘œ๋ฉด ์ ‘์„  ๋ฒกํ„ฐ๋งŒ ์ถ”์ถœ
                    // tangent = noiseVector - (noiseVector dot normal) * normal
                    Vector3 tangent = noiseVector - Vector3.Dot(noiseVector, normal) * normal;

                    // ์ ‘์„  ๋ฐฉํ–ฅ์œผ๋กœ ์ด๋™ ์ ์šฉ
                    Vector3 nextPos = pos + tangent * flowSpeed * dt;

                    // ์ด๋™ ํ›„ ๋‹ค์‹œ ๊ตฌ ํ‘œ๋ฉด์œผ๋กœ ์ •ํ•ฉ (๋ฐ˜์ง€๋ฆ„ ๊ณ ์ •)
                    nextPos = nextPos.normalized * sphereRadius;
                    t.position = nextPos;
                }

                // -------------------------------------------------
                // [3. ์ง€์†์  ํšŒ์ „ ์ ์šฉ]
                // -------------------------------------------------
                if (enableContinuousRotation)
                {
                    t.Rotate(
                        p.angularVelocity.x * dt,
                        p.angularVelocity.y * dt,
                        p.angularVelocity.z * dt,
                        Space.Self
                    );
                }

                // -------------------------------------------------
                // [4. ์ˆ˜๋ช… ์ข…๋ฃŒ ์ฒ˜๋ฆฌ]
                // -------------------------------------------------
                if (p.remainingLife <= 0f)
                {
                    p.gameObject.SetActive(false);
                    p.isActive = false;
                }

                pool[i] = p;
            }
        }
    }

    // =========================================================
    // [Unity LifeCycle: OnDestroy]
    // =========================================================
    void OnDestroy()
    {
        foreach (var pool in prefabPools.Values)
        {
            foreach (var p in pool)
            {
                if (p.gameObject != null)
                    Destroy(p.gameObject);
            }
            pool.Clear();
        }
        prefabPools.Clear();
        validPrefabs.Clear();
    }
}

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

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