




.fbx λͺ¨λΈλ§ νμΌμμ λ©μλ₯Ό μΆμΆ
νν°ν΄μ΄ λ©μμ νλ©΄μ ν¬μΈνΈ ν΄λΌμ°λλ₯Ό μ΄λ£¨λ©΄μ μ±μμ§. λ©μμ λ²ν
μ€λ₯Ό λ°λ₯΄μ§ μκ³ νλ©΄μ μΌκ°νμ κ·Έλ €μ κ·Έ μλ‘ μ±μ°λ λ‘μ§.
λ§μ°μ€μ 컀μλ κΉμ΄ + λ²μλ₯Ό 체ν¬νμ¬ νν°ν΄μ λΆμ°μμΌ μλ‘ μ¬λΌκ°κ² ν¨.
Create > Shader > Compute Shaderλ‘ μλ‘μ΄ Shader λ§λ€κ³ μ΄λ¦ λ³κ²½(MeshParticleSwarm.compute)

μ °μ΄λ μ€ν¬λ¦½νΈ μμ±
Asssets / Shader ν΄λμ Create > Shader > URP Unlit Shader λ§λ€κΈ° (MeshParticleDraw.shader)

μ€ν¬λ¦½νΈ μμ±
Create > Materialλ‘ μλ‘μ΄ Material λ§λ€κΈ° - μ΄λ¦μ μμλ‘ μ§μ (μ: ParticleSwarmMat)

μμ±ν Materialμ μ ν β Inspector μ΅μλ¨ Shader λλ‘λ€μ΄ ν΄λ¦ : Custom > MeshParticleDraw μ ν


Source Meshμ μνλ λ©μλ₯Ό ν λΉCompute Shaderμ MeshParticleSwarm.compute μ½λλ₯Ό ν λΉParticle Materialμ ParticleSwarmMat λ§€ν°λ¦¬μΌμ ν λΉ


Read/Writeμ λ°λμ 체ν¬ν κ².
#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
// 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
// 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
}