🫧Art_017 Mesh Particle Swarm

BamgasiJMΒ·2026λ…„ 3μ›” 27일

Unity GenArt

λͺ©λ‘ 보기
28/41
post-thumbnail

1. μ•„νŠΈμ›Œν¬ κ°œμš”

.fbx λͺ¨λΈλ§ νŒŒμΌμ—μ„œ λ©”μ‹œλ₯Ό μΆ”μΆœ
νŒŒν‹°ν΄μ΄ λ©”μ‹œμ˜ ν‘œλ©΄μ— 포인트 ν΄λΌμš°λ“œλ₯Ό μ΄λ£¨λ©΄μ„œ μ±„μ›Œμ§. λ©”μ‹œμ˜ λ²„ν…μŠ€λ₯Ό λ”°λ₯΄μ§€ μ•Šκ³  ν‘œλ©΄μ— μ‚Όκ°ν˜•μ„ κ·Έλ €μ„œ κ·Έ μœ„λ‘œ μ±„μš°λŠ” 둜직.
마우슀의 μ»€μ„œλŠ” 깊이 + λ²”μœ„λ₯Ό μ²΄ν¬ν•˜μ—¬ νŒŒν‹°ν΄μ„ λΆ„μ‚°μ‹œμΌœ μœ„λ‘œ μ˜¬λΌκ°€κ²Œ 함.


2. 적용 방법

2-1. Compute Shader μž‘μ„±

  1. Create > Shader > Compute Shader둜 μƒˆλ‘œμš΄ Shader λ§Œλ“€κ³  이름 λ³€κ²½(MeshParticleSwarm.compute)

  2. 셰이더 슀크립트 μž‘μ„±

  • Unity 6.0+ / SM 4.5
  • νŒŒν‹°ν΄ 물리: 마우슀 κ·Όμ ‘ β†’ ν„°λ·ΈλŸ°μŠ€ λΆ€μœ , λ©€μ–΄μ§€λ©΄ 원점 볡귀

2-2. URP Unlit Shader μž‘μ„±κ³Ό Material μ„€μ •

  1. Asssets / Shader 폴더에 Create > Shader > URP Unlit Shader λ§Œλ“€κΈ° (MeshParticleDraw.shader)

  2. 슀크립트 μž‘μ„±

  • URP / HDRP 곡용 Unlit Additive νŒŒν‹°ν΄ 셰이더
  • SV_VertexID 기반 quad billboarding β€” GeometryShader 없이 λ™μž‘
  • νŒŒν‹°ν΄λ‹Ή 6 verts (2 tri) 둜 DrawProcedural 호좜
  1. Create > Material둜 μƒˆλ‘œμš΄ Material λ§Œλ“€κΈ° - 이름은 μž„μ˜λ‘œ μ§€μ • (예: ParticleSwarmMat)

  2. μƒμ„±ν•œ Material을 선택 β†’ Inspector μ΅œμƒλ‹¨ Shader λ“œλ‘­λ‹€μš΄ 클릭 : Custom > MeshParticleDraw 선택

  1. Inspector에 Color, Particle Size, Brightness ν”„λ‘œνΌν‹° 확인

2-3. GameObject μ„€μ •

  1. GameObject에 MeshParticleSwarm.cs μ½”λ“œλ₯Ό μ»΄ν¬λ„ŒνŠΈλ‘œ λΆ€μ°©
  2. Inspector μ°½μ—μ„œ 빈 ν•„λ“œμ—
  • Source Mesh에 μ›ν•˜λŠ” λ©”μ‹œλ₯Ό ν• λ‹Ή
  • Compute Shader에 MeshParticleSwarm.compute μ½”λ“œλ₯Ό ν• λ‹Ή
  • Particle Material에 ParticleSwarmMat 맀터리얼을 ν• λ‹Ή

2-4. λͺ¨λΈλ§ μ€€λΉ„

  1. λΈ”λ Œλ”μ—μ„œ νšŒμ „ μ„ΈνŒ… ν›„ fbx Export

    λΈ”λ Œλ” κΈ°μ€€μœΌλ‘œ Frontμ—μ„œ λ°”λ‹₯이 보이고, Topμ—μ„œ λ’·λͺ¨μŠ΅μ΄ 보이도둝 νšŒμ „.
    즉 λ’€λŒμ•„μ„œ μ•žμœΌλ‘œ 고꾸라진 λͺ¨μŠ΅μœΌλ‘œ ν•΄ 놓고 fbx둜 내보내야지 μœ λ‹ˆν‹°μ—μ„œ μ •λ©΄μœΌλ‘œ λ³΄μž…λ‹ˆλ‹€. 그리고 Option μ°½μ—μ„œ Transform을 Z Forward, Y Up으둜 ν•©λ‹ˆλ‹€.

  1. μœ λ‹ˆν‹°μ—μ„œ Assets > Modeling 폴더 속에 fbx νŒŒμΌμ„ λ“œλž˜κ·Έ&λ“œλžν•΄μ„œ 뢈러였기.
  2. Scale 수치둜 λͺ¨λΈλ§ 크기 λ³€κ²½, Read/Write에 λ°˜λ“œμ‹œ 체크할 것.


3. μ½”λ“œ

πŸ“’ MeshParticleSwarm.cs

#if UNITY_EDITOR
using UnityEditor;
#endif

using System;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.InputSystem;

[ExecuteAlways]
[RequireComponent(typeof(MeshFilter))]
public sealed class MeshParticleSwarm : MonoBehaviour
{
    [Header("Mesh & Particles")]
    public Mesh sourceMesh;
    public int particleCount = 300_000;

    [Header("Mouse Influence")]
    public float mouseRadius = 2.5f;
    public float liftForce = 6f;

    [Header("Turbulence")]
    public float turbulenceScale = 1.2f;
    public float turbulenceStrength = 3f;

    [Header("Physics")]
    public float gravity = -3f;
    public float returnStrength = 4f;
    [Range(0f, 1f)]
    public float damping = 0.88f;

    [Header("Rendering")]
    public ComputeShader computeShader;
    public Material particleMaterial;
    public float particleSize = 0.018f;
    public Color particleColor = Color.white;
    [Range(0.1f, 5f)]
    public float brightness = 1f;

    struct Particle
    {
        public Vector3 origin;
        public Vector3 position;
        public Vector3 velocity;
        public Vector3 normal;
        public float phase;
        public float influenced;
    }
    const int STRIDE = 14 * sizeof(float);

    ComputeBuffer _particleBuffer;
    MaterialPropertyBlock _mpb;
    int _kernelUpdate;
    Camera _mainCam;
    int _actualCount;
    Bounds _worldBounds;

    static readonly int ID_Particles = Shader.PropertyToID("_Particles");
    static readonly int ID_ParticleSize = Shader.PropertyToID("_ParticleSize");
    static readonly int ID_Color = Shader.PropertyToID("_Color");
    static readonly int ID_Brightness = Shader.PropertyToID("_Brightness");

    // ── 에디터 μ „μš©: νŒŒλΌλ―Έν„° λ³€κ²½ κ°μ§€μš© μΊμ‹œ ─────────────────────────────
#if UNITY_EDITOR
    int   _cachedParticleCount;
    Mesh  _cachedMesh;
#endif

    void OnEnable() => Initialize();
    void OnDisable() => Cleanup();

    void Initialize()
    {
        _mainCam = Camera.main;

        Mesh mesh = sourceMesh != null ? sourceMesh : GetComponent<MeshFilter>()?.sharedMesh;
        if (mesh == null || computeShader == null || particleMaterial == null) return;

        Particle[] particles = BuildParticles(mesh);
        _actualCount = particles.Length;

        _particleBuffer = new ComputeBuffer(_actualCount, STRIDE);
        _particleBuffer.SetData(particles);

        _kernelUpdate = computeShader.FindKernel("CSUpdate");
        computeShader.SetBuffer(_kernelUpdate, "_Particles", _particleBuffer);

        _mpb = new MaterialPropertyBlock();

        Bounds local = mesh.bounds;
        Vector3 center = transform.TransformPoint(local.center);
        Vector3 size = Vector3.Scale(local.size, transform.lossyScale) * 3f;
        _worldBounds = new Bounds(center, size);

#if UNITY_EDITOR
        _cachedParticleCount = particleCount;
        _cachedMesh          = mesh;
#endif

        Debug.Log($"[MeshParticleSwarm] {_actualCount:N0}개 μ΄ˆκΈ°ν™” μ™„λ£Œ (isPlaying={Application.isPlaying})");
    }

    void Cleanup()
    {
        _particleBuffer?.Release();
        _particleBuffer = null;
    }

    void Update()
    {
        if (_particleBuffer == null) return;

#if UNITY_EDITOR
        // ── 에디터 μ „μš©: νŒŒλΌλ―Έν„°κ°€ λ°”λ€Œλ©΄ μž¬μ΄ˆκΈ°ν™” ─────────────────────
        Mesh currentMesh = sourceMesh != null ? sourceMesh : GetComponent<MeshFilter>()?.sharedMesh;
        if (currentMesh != _cachedMesh || particleCount != _cachedParticleCount)
        {
            Cleanup();
            Initialize();
            return;
        }

        // ── 에디터 λͺ¨λ“œ: 정적 μŠ€λƒ…μƒ·λ§Œ λ Œλ” (물리 μ—°μ‚° μ—†μŒ) ─────────────
        if (!Application.isPlaying)
        {
            DrawParticles();
            return;
        }
#endif

        // ── Play λͺ¨λ“œ: 정상 물리 μ—…λ°μ΄νŠΈ ────────────────────────────────
        Vector2 screenPos = Mouse.current != null
            ? Mouse.current.position.ReadValue()
            : Vector2.zero;
        Ray ray = _mainCam != null ? _mainCam.ScreenPointToRay(screenPos) : new Ray();
        var plane = new Plane(-(_mainCam != null ? _mainCam.transform.forward : Vector3.forward),
                                  transform.position);
        Vector3 mouseWorld = plane.Raycast(ray, out float dist)
            ? ray.GetPoint(dist) : Vector3.zero;

        computeShader.SetVector("_MouseWorld", mouseWorld);
        computeShader.SetFloat("_MouseRadius", mouseRadius);
        computeShader.SetFloat("_LiftForce", liftForce);
        computeShader.SetFloat("_TurbulenceScale", turbulenceScale);
        computeShader.SetFloat("_TurbulenceStrength", turbulenceStrength);
        computeShader.SetFloat("_Gravity", gravity);
        computeShader.SetFloat("_ReturnStrength", returnStrength);
        computeShader.SetFloat("_Damping", damping);
        computeShader.SetFloat("_DeltaTime", Time.deltaTime);
        computeShader.SetFloat("_Time", Time.time);

        computeShader.Dispatch(_kernelUpdate, Mathf.CeilToInt(_actualCount / 64f), 1, 1);

        DrawParticles();
    }

    // ── 곡톡 λ“œλ‘œμš° 호좜 ──────────────────────────────────────────────────
    void DrawParticles()
    {
        if (_particleBuffer == null || particleMaterial == null) return;

        _mpb.SetBuffer(ID_Particles, _particleBuffer);
        _mpb.SetFloat(ID_ParticleSize, particleSize);
        _mpb.SetColor(ID_Color, particleColor);
        _mpb.SetFloat(ID_Brightness, brightness);

        Graphics.DrawProcedural(
            particleMaterial,
            _worldBounds,
            MeshTopology.Triangles,
            _actualCount * 6,
            1,
            null,
            _mpb,
            ShadowCastingMode.Off,
            false,
            gameObject.layer);
    }

    void OnDestroy() => Cleanup();

    Particle[] BuildParticles(Mesh mesh)
    {
        Vector3[] verts = mesh.vertices;
        int[] tris = mesh.triangles;
        Vector3[] normals = mesh.normals;
        bool hasNorm = normals != null && normals.Length == verts.Length;

        int triCount = tris.Length / 3;
        float[] cdf = new float[triCount];
        float total = 0f;

        for (int i = 0; i < triCount; i++)
        {
            int i0 = tris[i * 3], i1 = tris[i * 3 + 1], i2 = tris[i * 3 + 2];
            Vector3 a = transform.TransformPoint(verts[i0]);
            Vector3 b = transform.TransformPoint(verts[i1]);
            Vector3 c = transform.TransformPoint(verts[i2]);
            total += Vector3.Cross(b - a, c - a).magnitude * 0.5f;
            cdf[i] = total;
        }
        for (int i = 0; i < triCount; i++) cdf[i] /= total;

        int actual = Mathf.Min(particleCount, triCount * 100);
        var list = new Particle[actual];
        var rng = new System.Random(1337);

        for (int p = 0; p < actual; p++)
        {
            float r = (float)rng.NextDouble();
            int ti = Array.BinarySearch(cdf, r);
            if (ti < 0) ti = ~ti;
            ti = Mathf.Clamp(ti, 0, triCount - 1);

            int i0 = tris[ti * 3], i1 = tris[ti * 3 + 1], i2 = tris[ti * 3 + 2];
            float u = (float)rng.NextDouble();
            float v = (float)rng.NextDouble();
            if (u + v > 1f) { u = 1f - u; v = 1f - v; }
            float w = 1f - u - v;

            Vector3 pos = transform.TransformPoint(
                verts[i0] * w + verts[i1] * u + verts[i2] * v);
            Vector3 nor = hasNorm
                ? transform.TransformDirection(
                    normals[i0] * w + normals[i1] * u + normals[i2] * v).normalized
                : Vector3.up;

            list[p] = new Particle
            {
                origin = pos,
                position = pos,
                velocity = Vector3.zero,
                normal = nor,
                phase = (float)rng.NextDouble() * 97.3f,
                influenced = 0f
            };
        }
        return list;
    }
}

πŸ“’ MeshParticleSwarm.compute

// MeshParticleSwarm.compute
// Unity 6.0+ / SM 4.5
// νŒŒν‹°ν΄ 물리: 마우슀 κ·Όμ ‘ β†’ ν„°λ·ΈλŸ°μŠ€ λΆ€μœ , λ©€μ–΄μ§€λ©΄ 원점 볡귀
#pragma kernel CSUpdate

// ── 데이터 ꡬ쑰 (C# Particle ꡬ쑰체와 byte μ™„μ „ 일치) ────────────────────────
struct Particle
{
    float3 origin;
    float3 position;
    float3 velocity;
    float3 normal;
    float  phase;
    float  influenced;
};

RWStructuredBuffer<Particle> _Particles;

// ── μœ λ‹ˆνΌ ────────────────────────────────────────────────────────────────────
float3 _MouseWorld;
float  _MouseRadius;
float  _LiftForce;
float  _TurbulenceScale;
float  _TurbulenceStrength;
float  _Gravity;
float  _ReturnStrength;
float  _Damping;
float  _DeltaTime;
float  _Time;

// ══════════════════════════════════════════════════════════════════════════════
// Noise ν•¨μˆ˜
// ══════════════════════════════════════════════════════════════════════════════

// 3D ν•΄μ‹œ (Shadertoy ν‘œμ€€)
float3 hash33(float3 p3)
{
    p3 = frac(p3 * float3(0.1031, 0.1030, 0.0973));
    p3 += dot(p3, p3.yxz + 33.33);
    return frac((p3.xxy + p3.yxx) * p3.zyx);
}

// 3D Gradient Noise
float gradNoise(float3 p)
{
    float3 i = floor(p);
    float3 f = frac(p);
    float3 u = f * f * (3.0 - 2.0 * f);    // smoothstep

    #define GRAD(OFFSET) dot(hash33(i + OFFSET) * 2.0 - 1.0, f - OFFSET)
    float n000 = GRAD(float3(0,0,0));
    float n100 = GRAD(float3(1,0,0));
    float n010 = GRAD(float3(0,1,0));
    float n110 = GRAD(float3(1,1,0));
    float n001 = GRAD(float3(0,0,1));
    float n101 = GRAD(float3(1,0,1));
    float n011 = GRAD(float3(0,1,1));
    float n111 = GRAD(float3(1,1,1));
    #undef GRAD

    return lerp(
        lerp(lerp(n000, n100, u.x), lerp(n010, n110, u.x), u.y),
        lerp(lerp(n001, n101, u.x), lerp(n011, n111, u.x), u.y),
        u.z);
}

// fBm ν„°λ·ΈλŸ°μŠ€ (2μ˜₯νƒ€λΈŒ)
float3 turbulence(float3 p, float t)
{
    // μ‹œκ°„μ— 따라 천천히 ν˜λŸ¬κ°€λŠ” 3채널 λ…Έμ΄μ¦ˆ
    float3 base = p * _TurbulenceScale + float3(t * 0.25, t * 0.14, t * 0.19);

    float3 n;
    n.x = gradNoise(base)                  + 0.5 * gradNoise(base * 2.1 + 5.2);
    n.y = gradNoise(base + float3(4.1,0,0)) + 0.5 * gradNoise(base * 2.1 + 1.3);
    n.z = gradNoise(base + float3(0,8.3,0)) + 0.5 * gradNoise(base * 2.1 + 9.7);

    return n * (_TurbulenceStrength / 1.5);   // μ •κ·œν™” 보정
}

// ══════════════════════════════════════════════════════════════════════════════
// 메인 컀널
// ══════════════════════════════════════════════════════════════════════════════
[numthreads(64, 1, 1)]
void CSUpdate(uint3 id : SV_DispatchThreadID)
{
    Particle p = _Particles[id.x];

    // ── 마우슀 영ν–₯도 (이차 감쇠) ────────────────────────────────────────
    float  distMouse  = distance(p.position, _MouseWorld);
    float  t          = saturate(1.0 - distMouse / max(_MouseRadius, 0.001));
    float  influence  = t * t;                  // 0(λ©€) ~ 1(κ·Όμ ‘)
    p.influenced      = influence;

    // ── 가속도 λˆ„μ  ───────────────────────────────────────────────────────
    float3 accel = (float3)0;

    // 1) 쀑λ ₯
    accel.y += _Gravity;

    // 2) 원점 볡귀 μŠ€ν”„λ§ (영ν–₯도에 따라 약해짐)
    float3 toOrigin      = p.origin - p.position;
    float  springStrength = _ReturnStrength * (1.0 - influence * 0.7);
    accel += toOrigin * springStrength;

    // 3) 마우슀 κ·Όμ ‘ μ‹œ: λΆ€λ ₯ + ν„°λ·ΈλŸ°μŠ€
    if (influence > 0.001)
    {
        // λΆ€λ ₯: λ…Έλ©€ λ°©ν–₯ 30% + μ›”λ“œ Up 70%
        float3 liftDir = normalize(lerp(float3(0,1,0), p.normal, 0.3));
        accel += liftDir * _LiftForce * influence;

        // ν„°λ·ΈλŸ°μŠ€ λ…Έμ΄μ¦ˆ (νŒŒν‹°ν΄ 고유 μœ„μƒμœΌλ‘œ λΆ„μ‚°)
        float3 noisePos = p.position + float3(p.phase, p.phase * 0.61, p.phase * 1.37);
        accel += turbulence(noisePos, _Time) * influence;

        // 마우슀λ₯Ό μ€‘μ‹¬μœΌλ‘œ μ•½ν•œ μ†Œμš©λŒμ΄ (XZ 평면)
        float3 toMouse  = _MouseWorld - p.position;
        float3 swirl    = cross(toMouse, float3(0,1,0));
        accel += normalize(swirl + 0.001) * influence * 1.5;
    }

    // ── 적뢄 ─────────────────────────────────────────────────────────────
    p.velocity  += accel * _DeltaTime;
    p.velocity  *= _Damping;
    p.position  += p.velocity * _DeltaTime;

    _Particles[id.x] = p;
}

πŸ“’ MeshParticleDraw.shader

// MeshParticleDraw.shader
// URP / HDRP 곡용 Unlit Additive νŒŒν‹°ν΄ 셰이더
// SV_VertexID 기반 quad billboarding β€” GeometryShader 없이 λ™μž‘
// νŒŒν‹°ν΄λ‹Ή 6 verts (2 tri) 둜 DrawProcedural 호좜

Shader "Custom/MeshParticleDraw"
{
    Properties
    {
        _Color        ("Color",         Color)  = (1, 1, 1, 1)
        _ParticleSize ("Particle Size", Float)  = 0.018
        _Brightness   ("Brightness",    Float)  = 1.0
    }

    SubShader
    {
        Tags
        {
            "RenderType"      = "Transparent"
            "Queue"           = "Transparent"
            "RenderPipeline"  = "UniversalPipeline"
            "IgnoreProjector" = "True"
        }

        Blend   One One     // Additive β€” νŒŒν‹°ν΄ κ²ΉμΉ¨ μ‹œ μžμ—°μŠ€λŸ½κ²Œ 빛남
        ZWrite  Off
        ZTest   LEqual
        Cull    Off

        Pass
        {
            Name "MeshParticle_Forward"

            HLSLPROGRAM
            #pragma vertex   vert
            #pragma fragment frag
            #pragma target   4.5
            // OpenGL ES 2/3 미지원 (StructuredBuffer ν•„μš”)
            #pragma exclude_renderers gles gles3 glcore

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

            // ── GPU νŒŒν‹°ν΄ ꡬ쑰체 (C# / Compute 와 byte 동일) ────────────
            struct Particle
            {
                float3 origin;
                float3 position;
                float3 velocity;
                float3 normal;
                float  phase;
                float  influenced;
            };

            StructuredBuffer<Particle> _Particles;

            CBUFFER_START(UnityPerMaterial)
                float4 _Color;
                float  _ParticleSize;
                float  _Brightness;
            CBUFFER_END

            // ── 6-vertex quad μ½”λ„ˆ (CCW) ──────────────────────────────────
            static const float2 quadUV[6] =
            {
                float2(-1, -1), float2( 1, -1), float2( 1,  1),
                float2(-1, -1), float2( 1,  1), float2(-1,  1)
            };

            // ── Varyings ──────────────────────────────────────────────────
            struct Varyings
            {
                float4 posCS : SV_POSITION;
                float2 uv    : TEXCOORD0;   // -1..1 (μ†Œν”„νŠΈ μ„œν΄μš©)
                float4 col   : COLOR;
            };

            // ── Vertex ────────────────────────────────────────────────────
            Varyings vert(uint vertID : SV_VertexID)
            {
                uint    pid    = vertID / 6u;
                uint    corner = vertID % 6u;
                Particle p     = _Particles[pid];

                // μ›”λ“œ β†’ 클립
                float4 clipPos = TransformWorldToHClip(p.position);

                // ν™”λ©΄ 곡간 λΉŒλ³΄λ“œ (clip.w κ³± β†’ NDC κ³ μ • 크기)
                float2 offset  = quadUV[corner] * _ParticleSize;
                clipPos.xy    += offset * clipPos.w;

                // 색상: κΈ°λ³Έ + 영ν–₯도에 λΉ„λ‘€ν•œ 블루-ν™”μ΄νŠΈ κΈ€λ‘œμš°
                float3 baseCol  = _Color.rgb * _Brightness;
                float3 glowCol  = float3(0.4, 0.65, 1.0) * p.influenced * 3.0;
                // 속도 크기둜 μŠ€νŠΈλ ˆμ΄ν‚Ή 힌트
                float  speed    = length(p.velocity);
                float3 speedCol = float3(1.0, 0.8, 0.4) * saturate(speed * 0.15);

                Varyings o;
                o.posCS = clipPos;
                o.uv    = quadUV[corner];
                o.col   = float4(baseCol + glowCol + speedCol, _Color.a);
                return o;
            }

            // ── Fragment ──────────────────────────────────────────────────
            float4 frag(Varyings i) : SV_Target
            {
                // μ†Œν”„νŠΈ μ›ν˜• μ•ŒνŒŒ (ν•˜λ“œ μ—£μ§€ μ—†μŒ)
                float d     = length(i.uv);
                float alpha = 1.0 - smoothstep(0.4, 1.0, d);
                clip(alpha - 0.01);             // μ™„μ „ 투λͺ… ν”½μ…€ 제거

                // 쀑심 μ½”μ–΄ κ°•μ‘°
                float core  = 1.0 - smoothstep(0.0, 0.35, d);
                float4 col  = i.col;
                col.rgb    += col.rgb * core * 1.5;

                return col * alpha;
            }

            ENDHLSL
        }
    }

    // Built-in RP 폴백 μ—†μŒ (URP/HDRP μ „μš©)
    FallBack Off
}

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

0개의 λŒ“κΈ€