


// GenerativePaperSwarm.cs
using UnityEngine;
using UnityEngine.InputSystem;
public class GenerativePaperSwarm : MonoBehaviour
{
[Header("Particle Settings")]
public int particleCount = 500;
public float spawnRadius = 5f;
[Header("Movement")]
public float moveSpeed = 3f;
public float swarmStrength = 2f;
public float noiseSpeed = 1.0f;
[Header("Steering")]
public float steeringSpeed = 3f; // ๋ฎ์์๋ก ๋ฐฉํฅ์ ํ ๋๋ฆฌ๊ณ ์ ์ฐ (๊ถ์ฅ: 2~4)
[Header("Orbit")]
public float orbitRadius = 3f;
public float orbitSpeed = 1.5f;
[Range(0f, 1f)]
public float orbitBlend = 0.85f;
[Header("Wave (Tail)")]
public float waveAmplitude = 0.4f;
public float waveFreqA = 1.1f;
public float waveFreqB = 2.7f;
public float waveFreqC = 5.3f;
public float waveSpeedMult = 1.0f;
[Header("Target")]
public Vector3 targetPosition;
[Header("Rendering")]
public Mesh particleMesh;
public Material material;
ComputeBuffer positionBuffer;
ComputeBuffer velocityBuffer;
ComputeBuffer argsBuffer;
Vector3[] positions;
Vector3[] velocities;
float[] phases;
Material runtimeMaterial;
Camera cam;
void OnEnable() => Init();
void Init()
{
if (material == null || particleMesh == null)
{
Debug.LogError("[PaperSwarm] Material ๋๋ Mesh๊ฐ Inspector์ ํ ๋น๋์ง ์์์ต๋๋ค.");
return;
}
cam = Camera.main;
runtimeMaterial = new Material(material);
positions = new Vector3[particleCount];
velocities = new Vector3[particleCount];
phases = new float[particleCount];
for (int i = 0; i < particleCount; i++)
{
positions[i] = transform.position + Random.insideUnitSphere * spawnRadius;
velocities[i] = Random.insideUnitSphere;
phases[i] = Random.Range(0f, Mathf.PI * 2f);
}
positionBuffer = new ComputeBuffer(particleCount, 12);
velocityBuffer = new ComputeBuffer(particleCount, 12);
positionBuffer.SetData(positions);
velocityBuffer.SetData(velocities);
uint[] args = new uint[5]
{
particleMesh.GetIndexCount(0),
(uint)particleCount,
0, 0, 0
};
argsBuffer = new ComputeBuffer(
1, args.Length * sizeof(uint), ComputeBufferType.IndirectArguments);
argsBuffer.SetData(args);
runtimeMaterial.SetBuffer("_Positions", positionBuffer);
runtimeMaterial.SetBuffer("_Velocities", velocityBuffer);
}
void Update()
{
if (runtimeMaterial == null || positionBuffer == null) return;
HandleInput();
UpdateParticles();
runtimeMaterial.SetBuffer("_Positions", positionBuffer);
runtimeMaterial.SetBuffer("_Velocities", velocityBuffer);
Graphics.DrawMeshInstancedIndirect(
particleMesh,
0,
runtimeMaterial,
new Bounds(transform.position, Vector3.one * 100f),
argsBuffer
);
}
void HandleInput()
{
if (Mouse.current == null) return;
if (!Mouse.current.leftButton.wasPressedThisFrame) return;
Ray ray = cam.ScreenPointToRay(Mouse.current.position.ReadValue());
Plane plane = new Plane(Vector3.up, Vector3.zero);
if (plane.Raycast(ray, out float enter))
targetPosition = ray.GetPoint(enter);
}
void UpdateParticles()
{
float t = Time.time;
// ํ๋ ์๋ ์ดํธ ๋
๋ฆฝ์ ์ง์ ๊ฐ์ ๊ณ์ (๋งค ํ๋ ์ 1ํ ๊ณ์ฐ)
float smooth = 1f - Mathf.Exp(-steeringSpeed * Time.deltaTime);
for (int i = 0; i < particleCount; i++)
{
Vector3 toTarget = targetPosition - positions[i];
float dist = toTarget.magnitude;
// โโ 1. ์ ๊ทผ ์๋ (๊ฑฐ๋ฆฌ ๊ธฐ๋ฐ ํด๋จํ โ ๊ทผ๊ฑฐ๋ฆฌ ๊ธ๊ฐ์ ๋ฐฉ์ง) โโโโโโโโโโ
float approachSpeed = Mathf.Min(moveSpeed, dist * 0.8f);
Vector3 approachVel = toTarget.normalized * approachSpeed;
// โโ 2. ๊ถค๋ ์ด๋ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
float orbitT = Mathf.Clamp01(1f - dist / orbitRadius);
orbitT = Mathf.SmoothStep(0f, 1f, orbitT) * orbitBlend;
Vector3 toNorm = toTarget.normalized;
Vector3 tangent = Vector3.Cross(Vector3.up, toNorm).normalized * orbitSpeed;
float radialCorr = (dist - orbitRadius) * 0.8f;
Vector3 orbitVel = tangent + toNorm * radialCorr;
Vector3 desired = Vector3.Lerp(approachVel, orbitVel, orbitT);
// โโ 3. ์ง๋จ ์ค์ ๋
ธ์ด์ฆ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Vector3 swarm = new Vector3(
Mathf.PerlinNoise(i * 0.1f, t * noiseSpeed),
Mathf.PerlinNoise(t * noiseSpeed, i * 0.1f),
Mathf.PerlinNoise(i * 0.05f + 100f, t * noiseSpeed)
) - Vector3.one * 0.5f;
swarm *= swarmStrength;
// โโ 4. ์๋ ์
๋ฐ์ดํธ (ํ๋ ์๋ ์ดํธ ๋
๋ฆฝ Lerp) โโโโโโโโโโโโโโโโโโโ
velocities[i] = Vector3.Lerp(velocities[i], desired + swarm, smooth);
// โโ 5. ๊ผฌ๋ฆฌ ํ๋ค๋ฆผ ๋
ธ์ด์ฆ (velocity ์ค์ผ ๋ฐฉ์ง ์ํด ์์น์ ์ง์ ๊ฐ์ฐ) โ
float p = phases[i];
float spd = velocities[i].magnitude;
float amp = waveAmplitude * Mathf.Clamp(spd / Mathf.Max(moveSpeed, 0.01f), 0.2f, 1.5f);
Vector3 wave = new Vector3(
Mathf.Sin(t * waveFreqA * waveSpeedMult + p) * amp
+ Mathf.Sin(t * waveFreqB * waveSpeedMult + p * 1.3f) * amp * 0.5f
+ Mathf.Sin(t * waveFreqC * waveSpeedMult + p * 2.1f) * amp * 0.25f,
Mathf.Cos(t * waveFreqA * waveSpeedMult + p * 0.7f) * amp * 0.4f,
Mathf.Sin(t * waveFreqB * waveSpeedMult + p * 1.7f) * amp * 0.6f
+ Mathf.Cos(t * waveFreqC * waveSpeedMult + p * 0.9f) * amp * 0.3f
);
positions[i] += (velocities[i] + wave) * Time.deltaTime;
}
positionBuffer.SetData(positions);
velocityBuffer.SetData(velocities);
}
void OnDisable()
{
positionBuffer?.Release();
velocityBuffer?.Release();
argsBuffer?.Release();
if (runtimeMaterial != null)
Destroy(runtimeMaterial);
}
}
Material ๋ง๋ค๊ณ Custom ์ฌ๋กฏ์์ ์ฐพ์ ์ฐ๊ฒฐ
// PaperSwarmLit.shader
Shader "Custom/PaperSwarmLit"
{
Properties
{
_ColorA ("Color A", Color) = (1,0,0,1)
_ColorB ("Color B", Color) = (0,0,1,1)
_BendStrength ("Bend Strength", Range(0,1)) = 0.15
_WaveSpeed ("Wave Speed", Float) = 2.0
_WaveFrequency ("Wave Frequency", Float) = 8.0
_Smoothness ("Smoothness", Range(0,1)) = 0.3
}
SubShader
{
Tags
{
"RenderType" = "Opaque"
"RenderPipeline" = "UniversalPipeline"
}
Pass
{
Name "ForwardLit"
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_instancing
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
StructuredBuffer<float3> _Positions;
StructuredBuffer<float3> _Velocities;
float4 _ColorA;
float4 _ColorB;
float _BendStrength;
float _WaveSpeed;
float _WaveFrequency;
float _Smoothness;
struct appdata
{
float3 vertex : POSITION;
float2 uv : TEXCOORD0;
uint id : SV_InstanceID;
};
struct v2f
{
float4 positionHCS : SV_POSITION;
float2 uv : TEXCOORD0;
float3 normalWS : TEXCOORD1;
};
v2f vert(appdata v)
{
v2f o;
float3 pos = _Positions[v.id];
float3 vel = _Velocities[v.id];
// โโ 1. ์๋ ๋ฐฉํฅ์ผ๋ก ๋ฉ์ ํ์ ์ ๋ ฌ โโโโโโโโโโโโโโโโโโโโโโโโโโ
float3 forward = normalize(vel + float3(0.0001, 0, 0));
float3 up = float3(0, 1, 0);
float3 right = normalize(cross(up, forward));
up = cross(forward, right);
float3 local = v.vertex;
float3 rotated = local.x * right
+ local.y * up
+ local.z * forward;
// โโ 2. ๋ ์ด์ด๋ wave (๊ผฌ๋ฆฌ ํ๋ญ์) โโโโโโโโโโโโโโโโโโโโโโโโโโ
float time = _Time.y * _WaveSpeed;
float phase = v.id * 0.37; // ํํฐํด๋ณ ์์ (์์ โ ๊ฒน์นจ ๋ฐฉ์ง)
float wave =
sin(time * 1.0 + v.uv.y * _WaveFrequency + phase) * _BendStrength
+ sin(time * 2.3 + v.uv.y * _WaveFrequency * 0.5 + phase * 1.6) * _BendStrength * 0.4
+ sin(time * 5.1 + v.uv.y * _WaveFrequency * 0.2 + phase * 3.1) * _BendStrength * 0.15;
// uv.y๊ฐ ํด์๋ก(๊ผฌ๋ฆฌ ๋) ์งํญ ๊ฐํ
wave *= (0.3 + v.uv.y * 0.7);
rotated.x += wave;
rotated.z += cos(time * 1.7 + v.uv.y * 5.0 + phase) * _BendStrength * 0.5
+ cos(time * 3.9 + v.uv.y * 2.0 + phase * 2.0) * _BendStrength * 0.2;
// โโ 3. ์๋ โ ํด๋ฆฝ ๋ณํ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
float3 worldPos = rotated + pos;
o.positionHCS = TransformWorldToHClip(worldPos);
o.normalWS = up;
o.uv = v.uv;
return o;
}
half4 frag(v2f i) : SV_Target
{
// UV ๊ทธ๋ผ๋์ธํธ ์ปฌ๋ฌ
float t = saturate(i.uv.y);
float3 col = lerp(_ColorA.rgb, _ColorB.rgb, t);
// URP ๋ฉ์ธ ๋ผ์ดํธ ๋ํจ์ฆ
Light mainLight = GetMainLight();
float NdotL = saturate(dot(normalize(i.normalWS), mainLight.direction));
float lighting = 0.4 + NdotL * 0.6;
col *= lighting * mainLight.color;
return float4(col, 1.0);
}
ENDHLSL
}
}
}