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

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

Unity GenArt

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

1. ๊ฐœ์š”

๊ฐ ํŒŒํ‹ฐํด์„ ๋…๋ฆฝ์ ์ธ GameObject๋กœ ์ƒ์„ฑํ•˜๊ณ  ์Šคํฌ๋ฆฝํŠธ๋กœ ์œ„์น˜/์ƒ‰์ƒ/์ˆ˜๋ช…์„ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

  • 5๊ฐœ์˜ ํ”„๋ฆฌํŒน ์Šฌ๋กฏ์— ๋ฉ”์‹œ ์ฑ„์šฐ๊ณ , ์ฑ„์›Œ์ง„ ์Šฌ๋กฏ ๊ฐœ์ˆ˜(n)์— ๋”ฐ๋ผ 1/n ๊ท ๋“ฑ ํ™•๋ฅ ๋กœ ์ƒ์„ฑ.
  • ๊ฐ ํŒŒํ‹ฐํด์— ๋…๋ฆฝ์ ์ธ ์ปดํฌ๋„ŒํŠธ๋‚˜ ์ฝœ๋ผ์ด๋” ๋ถ€์ฐฉ ๊ฐ€๋Šฅ.
  • ํŒŒํ‹ฐํด ์ˆ˜๊ฐ€ ๋งŽ์•„์ง€๋ฉด(1000๊ฐœ ์ด์ƒ) ์„ฑ๋Šฅ ์ €ํ•˜ ์‹ฌํ•จ.
  • Draw Call ์ด ๋งŽ์ด ๋ฐœ์ƒํ•˜์—ฌ ๋ Œ๋”๋ง ์˜ค๋ฒ„ํ—ค๋“œ ์ฆ๊ฐ€.
  • ๋ฉ€ํ‹ฐ ํ”„๋ฆฌํŒน ํ’€๋ง ์‹œ์Šคํ…œ์œผ๋กœ ์„ฑ๋Šฅ ์ตœ์ ํ™”.

1. Inspector ์„ค์ • ๋ฐฉ๋ฒ•

  1. ๋นˆ GameObject ์ƒ์„ฑ ํ›„ ์Šคํฌ๋ฆฝํŠธ ์ฒจ๋ถ€
  2. Particle Settings ์„น์…˜์˜ Particle Prefabs ๋ฐฐ์—ด ํฌ๊ธฐ ํ™•์ธ (๊ธฐ๋ณธ 5).
  3. ์›ํ•˜๋Š” ๋ชจ๋ธ๋ง ๋ฉ”์‹œ๊ฐ€ ์ ์šฉ๋œ ํ”„๋ฆฌํŒน์„ ์Šฌ๋กฏ 1๊ฐœ ์ด์ƒ ๋“œ๋ž˜๊ทธ&๋“œ๋กญ์œผ๋กœ ๋ถ€์ฐฉ.
  4. Max Particles ์„ค์ • ์‹œ, ํ• ๋‹น๋œ ํ”„๋ฆฌํŒน ๊ฐœ์ˆ˜(n)๋กœ ๊ท ๋“ฑ ๋ถ„ํ• ๋จ. (์˜ˆ: MaxParticles=100, ํ”„๋ฆฌํŒน 2๊ฐœ -> ๊ฐ 50๊ฐœ ํ’€ ํ• ๋‹น)

2. ํ™•๋ฅ  ๊ณ„์‚ฐ ์›๋ฆฌ (1/n)

GameObject selectedPrefab = validPrefabs[Random.Range(0, validPrefabs.Count)];
  • Random.Range(0, n) ์€ 0 ๋ถ€ํ„ฐ n-1 ๊นŒ์ง€์˜ ์ •์ˆ˜๋ฅผ ๊ท ๋“ฑํ•œ ํ™•๋ฅ ๋กœ ๋ฐ˜ํ™˜.
  • ํ• ๋‹น๋œ ํ”„๋ฆฌํŒน์ด 1๊ฐœ๋ผ๋ฉด 100%, 2๊ฐœ๋ผ๋ฉด ๊ฐ 50%, 5๊ฐœ๋ผ๋ฉด ๊ฐ 20% ์˜ ๋นˆ๋„๋กœ ์„ ํƒ๋จ.
  • ๋นˆ ๋ฐฐ์—ด ์Šฌ๋กฏ(null) ์€ validPrefabs ์ƒ์„ฑ ์‹œ ์ž๋™์œผ๋กœ ์ œ์™ธ๋˜๋ฏ€๋กœ ํ™•๋ฅ  ๊ณ„์‚ฐ์— ์˜ํ–ฅ์„ ์ฃผ์ง€ ์•Š์Œ.

3. ํ’€๋ง ์‹œ์Šคํ…œ (Pooling System) ๋™์ž‘ ๋ฐฉ์‹

  • ์„ฑ๋Šฅ ์ตœ์ ํ™”๋ฅผ ์œ„ํ•ด ํ”„๋ฆฌํŒน ํƒ€์ž…๋ณ„ ๋…๋ฆฝ ํ’€์„ ์‚ฌ์šฉ.
  • SpawnParticle() ํ˜ธ์ถœ ์‹œ ์„ ํƒ๋œ ํ”„๋ฆฌํŒน์— ํ•ด๋‹นํ•˜๋Š” ํ’€์—์„œ๋งŒ ๋น„ํ™œ์„ฑํ™” ๊ฐ์ฒด๋ฅผ ์žฌ์‚ฌ์šฉ.
  • ๋ชจ๋“  ํ’€์ด ํ™œ์„ฑ ์ƒํƒœ์ผ ๊ฒฝ์šฐ ๊ฒฝ๊ณ  ๋ฉ”์‹œ์ง€์™€ ํ•จ๊ป˜ ์ƒˆ ์ธ์Šคํ„ด์Šค๋ฅผ ์ƒ์„ฑํ•˜๋ฏ€๋กœ, Max Particles ๊ฐ’์„ ์‹ค์ œ ํ•„์š” ๊ฐœ์ˆ˜๋ณด๋‹ค ์—ฌ์œ  ์žˆ๊ฒŒ ์„ค์ •ํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅ.

4. Note

  • ํ”„๋ฆฌํŒน์— Mesh Renderer ๋˜๋Š” Skinned Mesh Renderer ๊ฐ€ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ํ• ๋‹น๋˜์–ด ์žˆ๋Š”์ง€ ํ™•์ธ.
  • ํšŒ์ „ ๊ธฐ๋Šฅ (enableContinuousRotation) ์ด ํ™œ์„ฑํ™”๋œ ๊ฒฝ์šฐ, ํ”„๋ฆฌํŒน์˜ ๋กœ์ปฌ ์ถ•์ด ๋น„๋Œ€์นญ์ด๋ฉด ํšŒ์ „ ๋ฐฉํ–ฅ์ด ์ œ๊ฐ๊ฐ์œผ๋กœ ๋ณด์—ฌ ๋” ์ž์—ฐ์Šค๋Ÿฌ์šด ํšจ๊ณผ๊ฐ€ ๋‚˜์˜ด.
  • ๋Ÿฐํƒ€์ž„ ์ค‘ Inspector ์—์„œ ํ”„๋ฆฌํŒน ํ• ๋‹น/ํ•ด์ œ๋ฅผ ๋ฐ˜๋ณตํ•  ๊ฒฝ์šฐ, Start() ์—์„œ๋งŒ ํ’€์ด ์ดˆ๊ธฐํ™”๋˜๋ฏ€๋กœ Scene ์„ ์žฌ์‹คํ–‰ํ•˜๊ฑฐ๋‚˜ InitializePools() ๋ฅผ ์ง์ ‘ ํ˜ธ์ถœํ•ด์•ผ ๋ณ€๊ฒฝ์‚ฌํ•ญ์ด ๋ฐ˜์˜๋จ.

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

using UnityEngine;
using System.Collections.Generic;
using System.Linq;

public class MeshParticleSystem : MonoBehaviour
{
    // =========================================================
    // [Inspector ๋…ธ์ถœ ๋ณ€์ˆ˜: ํŒŒํ‹ฐํด ๊ธฐ๋ณธ ์„ค์ •]
    // =========================================================
    [Header("Particle Settings")]
    [Tooltip("์ตœ๋Œ€ 5๊ฐœ์˜ ํ”„๋ฆฌํŒน์„ ํ• ๋‹นํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ตœ์†Œ 1๊ฐœ๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.")]
    public GameObject[] particlePrefabs = new GameObject[5]; // 5๊ฐœ ์Šฌ๋กฏ ๋ฐฐ์—ด
    public int maxParticles = 100;        // ์ „์ฒด ์‹œ์Šคํ…œ์—์„œ ๊ด€๋ฆฌํ•  ํŒŒํ‹ฐํด ์ด ๊ฐœ์ˆ˜
    public float spawnRate = 10f;         // ์ดˆ๋‹น ์ƒ์„ฑ ๊ฐœ์ˆ˜
    public Vector3 spawnAreaSize = new Vector3(5, 5, 5); // ์ƒ์„ฑ ์˜์—ญ ํฌ๊ธฐ
    
    // =========================================================
    // [Inspector ๋…ธ์ถœ ๋ณ€์ˆ˜: ์ด๋™ ๋ฐ ํšŒ์ „ ์„ค์ •]
    // =========================================================
    [Header("Motion Settings")]
    public float minSpeed = 1f;           // ์ตœ์†Œ ์ด๋™ ์†๋„
    public float maxSpeed = 5f;           // ์ตœ๋Œ€ ์ด๋™ ์†๋„
    public float lifetime = 3f;           // ํŒŒํ‹ฐํด ์ˆ˜๋ช…
    
    [Header("Rotation Settings")]
    public bool enableRandomStartRotation = true;  // ์‹œ์ž‘ ์‹œ ๋žœ๋ค ํšŒ์ „ ์ ์šฉ ์—ฌ๋ถ€
    public bool enableContinuousRotation = true;   // ์ง€์†์  ํšŒ์ „ ์ ์šฉ ์—ฌ๋ถ€
    public Vector3 minAngularVelocity = new Vector3(-30f, -30f, -30f); // ์ตœ์†Œ ๊ฐ์†๋„ (๋„/์ดˆ)
    public Vector3 maxAngularVelocity = new Vector3(30f, 30f, 30f);    // ์ตœ๋Œ€ ๊ฐ์†๋„ (๋„/์ดˆ)
    
    // =========================================================
    // [Private ๋ฉค๋ฒ„ ๋ณ€์ˆ˜: ํ’€๋ง ๋ฐ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ]
    // =========================================================
    private List<GameObject> validPrefabs = new List<GameObject>();                // null์ด ์•„๋‹Œ ํ”„๋ฆฌํŒน๋งŒ ํ•„ํ„ฐ๋งํ•œ ๋ฆฌ์ŠคํŠธ
    private Dictionary<GameObject, List<ParticleData>> prefabPools = new Dictionary<GameObject, List<ParticleData>>(); // ํ”„๋ฆฌํŒน๋ณ„ ํŒŒํ‹ฐํด ํ’€
    private float spawnTimer = 0f;
    private int totalAllocatedParticles = 0; // ์‹ค์ œ๋กœ ํ• ๋‹น๋œ ํŒŒํ‹ฐํด ์ดํ•ฉ
    
    // =========================================================
    // [ํŒŒํ‹ฐํด ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ์ฒด]
    // =========================================================
    private struct ParticleData
    {
        public GameObject gameObject;         // ํŒŒํ‹ฐํด GameObject
        public GameObject assignedPrefab;     // ์ด ํŒŒํ‹ฐํด์ด ์†ํ•œ ํ”„๋ฆฌํŒน ํƒ€์ž…
        public Vector3 velocity;              // ์ด๋™ ์†๋„ ๋ฒกํ„ฐ
        public Vector3 angularVelocity;       // ๊ฐ ์ถ•๋ณ„ ํšŒ์ „ ์†๋„ (๋„/์ดˆ)
        public float remainingLife;           // ๋‚จ์€ ์ˆ˜๋ช…
        public bool isActive;                 // ํ™œ์„ฑํ™” ์ƒํƒœ
    }
    
    // =========================================================
    // [Unity LifeCycle: OnValidate (Inspector ๋ณ€๊ฒฝ ์‹œ ์ž๋™ ํ˜ธ์ถœ)]
    // =========================================================
    void OnValidate()
    {
        // Inspector ์—์„œ ๋ฐฐ์—ด ๋ณ€๊ฒฝ ์‹œ ์œ ํšจ ํ”„๋ฆฌํŒน ๋ชฉ๋ก ๊ฐฑ์‹ 
        validPrefabs.Clear();
        foreach (var p in particlePrefabs)
        {
            if (p != null && !validPrefabs.Contains(p))
                validPrefabs.Add(p);
        }
        
        // ์ตœ์†Œ 1๊ฐœ ํ• ๋‹น ๊ฒฝ๊ณ 
        if (validPrefabs.Count == 0 && Application.isPlaying)
            Debug.LogWarning("[ManualParticleSystem] ์ตœ์†Œ 1๊ฐœ์˜ ํ”„๋ฆฌํŒน์„ ํ• ๋‹นํ•ด์•ผ ์‹œ์Šคํ…œ์ด ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค.");
        
        // ์†๋„/ํšŒ์ „ ๋ฒ”์œ„ ๋ณด์ •
        if (minSpeed > maxSpeed) maxSpeed = minSpeed;
        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;
    }
    
    // =========================================================
    // [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("[ManualParticleSystem] ๋™์ž‘ํ•˜๋ ค๋ฉด ์ตœ์†Œ 1๊ฐœ์˜ ํ”„๋ฆฌํŒน์ด ํ• ๋‹น๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์Šคํฌ๋ฆฝํŠธ๋ฅผ ๋น„ํ™œ์„ฑํ™”ํ•ฉ๋‹ˆ๋‹ค.");
            enabled = false;
            return;
        }
        
        // ํ”„๋ฆฌํŒน๋ณ„ ํŒŒํ‹ฐํด ํ’€ ์ดˆ๊ธฐํ™” ๋ฐ ๊ฐœ์ˆ˜ ๋ถ„๋ฐฐ
        InitializePools();
    }
    
    // =========================================================
    // [Private Method: ํ”„๋ฆฌํŒน๋ณ„ ํŒŒํ‹ฐํด ํ’€ ์ƒ์„ฑ ๋ฐ ๊ท ๋“ฑ ๋ถ„๋ฐฐ]
    // =========================================================
    void InitializePools()
    {
        int validCount = validPrefabs.Count;
        int baseCount = maxParticles / validCount;
        int remainder = maxParticles % validCount;
        totalAllocatedParticles = 0;
        
        foreach (var prefab in validPrefabs)
        {
            List<ParticleData> pool = new List<ParticleData>();
            // ๋‚˜๋จธ์ง€ ํŒŒํ‹ฐํด์€ ์ˆœ์„œ๋Œ€๋กœ ์ถ”๊ฐ€ํ•˜์—ฌ ์ดํ•ฉ์ด maxParticles ๊ฐ€ ๋˜๋„๋ก ๊ท ๋“ฑ ๋ถ„๋ฐฐ
            int countForThisPrefab = baseCount + (remainder > 0 ? 1 : 0);
            remainder--;
            
            for (int i = 0; i < countForThisPrefab; i++)
            {
                GameObject go = Instantiate(prefab, transform);
                go.SetActive(false);
                
                pool.Add(new ParticleData
                {
                    gameObject = go,
                    assignedPrefab = prefab,
                    velocity = Vector3.zero,
                    angularVelocity = Vector3.zero,
                    remainingLife = 0f,
                    isActive = false
                });
                totalAllocatedParticles++;
            }
            prefabPools[prefab] = pool;
        }
        
        Debug.Log($"[ManualParticleSystem] {validCount}๊ฐœ ํ”„๋ฆฌํŒน์— ์ด {totalAllocatedParticles}๊ฐœ ํŒŒํ‹ฐํด ํ’€ ํ• ๋‹น ์™„๋ฃŒ.");
    }
    
    // =========================================================
    // [Unity LifeCycle: Update]
    // =========================================================
    void Update()
    {
        // ํŒŒํ‹ฐํด ์ƒ์„ฑ ํƒ€์ด๋จธ ์ฒ˜๋ฆฌ
        spawnTimer += Time.deltaTime;
        if (spawnTimer >= 1f / spawnRate)
        {
            spawnTimer = 0f;
            SpawnParticle();
        }
        
        // ๋ชจ๋“  ํŒŒํ‹ฐํด ์—…๋ฐ์ดํŠธ (์ด๋™ + ํšŒ์ „ + ์ˆ˜๋ช…)
        UpdateParticles();
    }
    
    // =========================================================
    // [Private Method: ํŒŒํ‹ฐํด ์ƒ์„ฑ (1/n ํ™•๋ฅ  ๊ท ๋“ฑ ๋ถ„๋ฐฐ)]
    // =========================================================
    void SpawnParticle()
    {
        // 1/n ๊ท ๋“ฑ ํ™•๋ฅ ๋กœ ์œ ํšจ ํ”„๋ฆฌํŒน ์ค‘ ํ•˜๋‚˜ ์„ ํƒ
        GameObject selectedPrefab = validPrefabs[Random.Range(0, validPrefabs.Count)];
        List<ParticleData> targetPool = prefabPools[selectedPrefab];
        
        // ํ•ด๋‹น ํ”„๋ฆฌํŒน ํ’€์—์„œ ๋น„ํ™œ์„ฑํ™”๋œ ํŒŒํ‹ฐํด ํƒ์ƒ‰
        ParticleData? foundParticle = null;
        for (int i = 0; i < targetPool.Count; i++)
        {
            if (!targetPool[i].isActive)
            {
                foundParticle = targetPool[i];
                break;
            }
        }
        
        // ํ’€์ด ๊ฐ€๋“ ์ฐฌ ๊ฒฝ์šฐ ์ƒˆ ์ธ์Šคํ„ด์Šค ์ƒ์„ฑ (์„ฑ๋Šฅ ๊ฒฝ๊ณ  ๋ฐœ์ƒ)
        if (foundParticle == null)
        {
            Debug.LogWarning($"[ManualParticleSystem] {selectedPrefab.name} ํ’€์ด ๊ฐ€๋“ ์ฐผ์Šต๋‹ˆ๋‹ค. ์ƒˆ ์ธ์Šคํ„ด์Šค๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. (maxParticles ์ฆ๊ฐ€ ๊ถŒ์žฅ)");
            GameObject go = Instantiate(selectedPrefab, transform);
            go.SetActive(false);
            ParticleData newP = new ParticleData
            {
                gameObject = go,
                assignedPrefab = selectedPrefab,
                velocity = Vector3.zero,
                angularVelocity = Vector3.zero,
                remainingLife = 0f,
                isActive = false
            };
            targetPool.Add(newP);
            foundParticle = newP;
            totalAllocatedParticles++;
        }
        
        // -------------------------------------------------
        // [ํŒŒํ‹ฐํด ํ™œ์„ฑํ™” ๋ฐ ๋ฐ์ดํ„ฐ ์„ค์ •]
        // -------------------------------------------------
        ParticleData p = foundParticle.Value;
        
        // ๋žœ๋ค ์œ„์น˜ ์ƒ์„ฑ
        Vector3 spawnPos = transform.position + 
            new Vector3(
                Random.Range(-spawnAreaSize.x * 0.5f, spawnAreaSize.x * 0.5f),
                Random.Range(-spawnAreaSize.y * 0.5f, spawnAreaSize.y * 0.5f),
                Random.Range(-spawnAreaSize.z * 0.5f, spawnAreaSize.z * 0.5f)
            );
        
        // ๋žœ๋ค ์ด๋™ ์†๋„
        Vector3 randomVelocity = Random.onUnitSphere * Random.Range(minSpeed, maxSpeed);
        
        // ๋žœ๋ค ๊ฐ์†๋„
        Vector3 randomAngularVelocity = new Vector3(
            Random.Range(minAngularVelocity.x, maxAngularVelocity.x),
            Random.Range(minAngularVelocity.y, maxAngularVelocity.y),
            Random.Range(minAngularVelocity.z, maxAngularVelocity.z)
        );
        
        // ์œ„์น˜/ํšŒ์ „ ์ ์šฉ
        p.gameObject.transform.position = spawnPos;
        if (enableRandomStartRotation)
            p.gameObject.transform.rotation = Random.rotationUniform;
        else
            p.gameObject.transform.rotation = Quaternion.identity;
        
        // ๋ฐ์ดํ„ฐ ์—…๋ฐ์ดํŠธ
        p.velocity = randomVelocity;
        p.angularVelocity = randomAngularVelocity;
        p.remainingLife = lifetime;
        p.isActive = true;
        p.gameObject.SetActive(true);
        
        // ๋ณ€๊ฒฝ ์‚ฌํ•ญ ํ’€์— ๋ฐ˜์˜ (๊ฐ’ ํƒ€์ž… struct ์ด๋ฏ€๋กœ ๋ช…์‹œ์  ์žฌํ• ๋‹น ํ•„์š”)
        int index = targetPool.IndexOf(foundParticle.Value);
        targetPool[index] = p;
    }
    
    // =========================================================
    // [Private Method: ํŒŒํ‹ฐํด ์—…๋ฐ์ดํŠธ]
    // =========================================================
    void UpdateParticles()
    {
        // ๋ชจ๋“  ํ’€์„ ์ˆœํšŒํ•˜๋ฉฐ ์—…๋ฐ์ดํŠธ
        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;
                
                // ์œ„์น˜ ์—…๋ฐ์ดํŠธ
                t.position += p.velocity * Time.deltaTime;
                
                // ํšŒ์ „ ์—…๋ฐ์ดํŠธ
                if (enableContinuousRotation)
                {
                    t.Rotate(
                        p.angularVelocity.x * Time.deltaTime,
                        p.angularVelocity.y * Time.deltaTime,
                        p.angularVelocity.z * Time.deltaTime,
                        Space.Self
                    );
                }
                
                // ์ˆ˜๋ช… ๊ฐ์†Œ
                p.remainingLife -= Time.deltaTime;
                
                // ์ˆ˜๋ช… ์ข…๋ฃŒ ์‹œ ๋น„ํ™œ์„ฑํ™”
                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๊ฐœ์˜ ๋Œ“๊ธ€