

// PaperFlowField.cs
using UnityEngine;
public class PaperFlowField : MonoBehaviour
{
[Header("Particles")]
public int particleCount = 2000;
public float spawnRadius = 10f;
[Header("Flow Field")]
public float flowScale = 0.3f;
public float flowSpeed = 0.5f;
public float flowStrength = 3.0f;
[Header("Swirl")]
public Vector3 center = Vector3.zero;
public float swirlForce = 1.5f; // ์ํ ์์ฉ๋์ด ๊ฐ๋
public float centerForce = 0.4f; // ์ค์ฌ ๋ณต๊ท ๊ฐ๋ (๋๋ฌด ํฌ๋ฉด ๋ญ์นจ)
[Header("Velocity")]
public float maxSpeed = 4f;
public float lerpSpeed = 3f; // ๋ฎ์์๋ก ๋ถ๋๋ฌ์ด ๋ฐฉํฅ์ ํ
[Header("Plane Constraint")]
public float yDamping = 4f; // ์์ง ํ๋ค๋ฆผ ์ต์ ๊ฐ๋
public float yNoiseScale = 0.05f; // ๋ฏธ์ธ Y ๋ถ์ ๊ฐ (0์ด๋ฉด ์์ ํ๋ฉด)
[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 OnDisable() => ReleaseBuffers();
void Init()
{
// [FIX #1] null ์ฒดํฌ
if (material == null || particleMesh == null)
{
Debug.LogError("[PaperFlowField] Material ๋๋ Mesh๊ฐ Inspector์ ํ ๋น๋์ง ์์์ต๋๋ค.");
return;
}
ReleaseBuffers();
cam = Camera.main;
// [FIX #2] ์๋ณธ material ๋ณดํธ: runtimeMaterial ๋ถ๋ฆฌ
runtimeMaterial = new Material(material);
positions = new Vector3[particleCount];
velocities = new Vector3[particleCount];
phases = new float[particleCount];
for (int i = 0; i < particleCount; i++)
{
// [FIX #3] XZ ํ๋ฉด์ ๋ถ์ฐ (insideUnitSphere โ insideUnitCircle)
Vector2 rnd = Random.insideUnitCircle * spawnRadius;
positions[i] = new Vector3(rnd.x, 0f, rnd.y) + transform.position;
velocities[i] = new Vector3(Random.Range(-1f, 1f), 0f, Random.Range(-1f, 1f));
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()
{
// [FIX #4] Edit ๋ชจ๋ ๋งค ํ๋ ์ Init() ์ ๊ฑฐ โ ๋ฌดํ ๋ฉ๋ชจ๋ฆฌ ๋์ ๋ฐฉ์ง
if (runtimeMaterial == null || positionBuffer == null) return;
UpdateParticles();
runtimeMaterial.SetBuffer("_Positions", positionBuffer);
runtimeMaterial.SetBuffer("_Velocities", velocityBuffer);
Graphics.DrawMeshInstancedIndirect(
particleMesh,
0,
runtimeMaterial,
new Bounds(center, Vector3.one * (spawnRadius * 3f)),
argsBuffer
);
}
void UpdateParticles()
{
float t = Time.time;
float smooth = 1f - Mathf.Exp(-lerpSpeed * Time.deltaTime);
for (int i = 0; i < particleCount; i++)
{
Vector3 pos = positions[i];
// โโ 1. Perlin ๋
ธ์ด์ฆ ํ๋ก์ฐ ํ๋ (XZ ํ๋ฉด) โโโโโโโโโโโโโโโโโโ
float nx = pos.x * flowScale;
float nz = pos.z * flowScale;
float nt = t * flowSpeed;
Vector3 flow = new Vector3(
Mathf.PerlinNoise(nx, nt) - 0.5f,
0f,
Mathf.PerlinNoise(nx + 31.7f, nt + 17.3f) - 0.5f
) * flowStrength;
// โโ 2. ์ํ ์์ฉ๋์ด (๊ณก์ ๊ถค์ ) โโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Vector3 toCenter = pos - center;
toCenter.y = 0f;
Vector3 tangent = new Vector3(-toCenter.z, 0f, toCenter.x).normalized;
Vector3 swirl = tangent * swirlForce;
// โโ 3. ์ค์ฌ ๋ณต๊ท (์์ญ ์ดํ ๋ฐฉ์ง) โโโโโโโโโโโโโโโโโโโโโโโโโโโ
float dist = toCenter.magnitude;
float pullT = Mathf.Clamp01((dist - spawnRadius * 0.7f) / (spawnRadius * 0.3f));
Vector3 attract = -toCenter.normalized * centerForce * pullT * dist;
// โโ 4. ์ต์ข
๋ชฉํ ์๋ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Vector3 desired = flow + swirl + attract;
desired.y = 0f;
// ์ต๋ ์๋ ํด๋จํ
if (desired.magnitude > maxSpeed)
desired = desired.normalized * maxSpeed;
// ๋ถ๋๋ฌ์ด ๋ฐฉํฅ์ ํ (ํ๋ ์๋ ์ดํธ ๋
๋ฆฝ)
velocities[i] = Vector3.Lerp(velocities[i], desired, smooth);
// โโ 5. Y ํ๋ฉด ์ ์ง + ๋ฏธ์ธ ๋ถ์ ๊ฐ โโโโโโโโโโโโโโโโโโโโโโโโโโโโ
float yNoise = (Mathf.PerlinNoise(pos.x * 0.15f + phases[i], t * 0.4f) - 0.5f)
* yNoiseScale;
velocities[i].y -= pos.y * yDamping * Time.deltaTime; // Y=0 ๋ณต๊ท๋ ฅ
positions[i] += velocities[i] * Time.deltaTime;
positions[i].y += yNoise;
}
positionBuffer.SetData(positions);
velocityBuffer.SetData(velocities);
}
void ReleaseBuffers()
{
positionBuffer?.Release(); positionBuffer = null;
velocityBuffer?.Release(); velocityBuffer = null;
argsBuffer?.Release(); argsBuffer = null;
if (runtimeMaterial != null)
{
if (Application.isPlaying) Destroy(runtimeMaterial);
else DestroyImmediate(runtimeMaterial);
runtimeMaterial = null;
}
}
}
Material ๋ง๋ค๊ณ Custom ์ฌ๋กฏ์์ ์ฐพ์ ์ฐ๊ฒฐ
// PaperFlowField.shader
Shader "Custom/PaperFlowField"
{
Properties
{
_ColorA ("Color A (base)", Color) = (0.95, 0.93, 0.88, 1)
_ColorB ("Color B (accent)",Color) = (1.0, 0.45, 0.15, 1)
_ColorC ("Color C (dark)", Color) = (0.2, 0.18, 0.15, 1)
_AccentRatio ("Accent Ratio", Range(0,1)) = 0.15
_DarkRatio ("Dark Ratio", Range(0,1)) = 0.1
_BendStrength ("Bend Strength", Range(0,1)) = 0.12
_WaveSpeed ("Wave Speed", Float) = 1.8
_WaveFrequency ("Wave Frequency", Float) = 9.0
}
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;
float4 _ColorC;
float _AccentRatio;
float _DarkRatio;
float _BendStrength;
float _WaveSpeed;
float _WaveFrequency;
struct appdata
{
float3 vertex : POSITION;
float2 uv : TEXCOORD0;
uint id : SV_InstanceID;
};
struct v2f
{
float4 positionHCS : SV_POSITION;
float2 uv : TEXCOORD0;
float3 normalWS : TEXCOORD1;
float colorSeed : TEXCOORD2;
};
// ํํฐํด ID ๊ธฐ๋ฐ ๊ฒฐ์ ๋ก ์ ๋๋ค (uint โ float ๋ช
์ ์บ์คํ
)
float PaperHash(uint n)
{
return frac(sin((float)n * 127.1 + 311.7) * 43758.5453);
}
v2f vert(appdata v)
{
v2f o;
float3 pos = _Positions[v.id];
float3 vel = _Velocities[v.id];
// [FIX #5] velocity ์ ๊ทํ: ์ค์นผ๋ผ ๋ง์
์ ๊ฑฐ โ ์์ ํ normalize
float velLen = length(vel);
float3 forward = velLen > 0.0001
? vel / velLen
: float3(1, 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;
// โโ ๋ ์ด์ด๋ wave (๊ผฌ๋ฆฌ ๋์ผ๋ก ๊ฐ์๋ก ๊ฐํด์ง) โโโโโโโโโโโโโโ
float time = _Time.y * _WaveSpeed;
float phase = (float)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.55 + phase * 1.6) * _BendStrength * 0.45
+ sin(time * 5.1 + v.uv.y * _WaveFrequency * 0.2 + phase * 3.1) * _BendStrength * 0.15;
wave *= (0.2 + v.uv.y * 0.8); // ๊ผฌ๋ฆฌ ๋ ๊ฐ์ค์น
rotated.x += wave;
rotated.z += cos(time * 1.7 + v.uv.y * 4.0 + phase) * _BendStrength * 0.4
+ cos(time * 3.9 + v.uv.y * 1.5 + phase * 2.0) * _BendStrength * 0.15;
// [FIX #6] TransformObjectToHClip โ TransformWorldToHClip
float3 worldPos = rotated + pos;
o.positionHCS = TransformWorldToHClip(worldPos);
o.uv = v.uv;
o.normalWS = up;
o.colorSeed = PaperHash(v.id);
return o;
}
half4 frag(v2f i) : SV_Target
{
// ์ปฌ๋ฌ ๋ถ๋ฅ: ๋๋ถ๋ถ base, ์ผ๋ถ accent, ์ผ๋ถ dark
float3 col;
if (i.colorSeed < _DarkRatio)
col = _ColorC.rgb;
else if (i.colorSeed < _DarkRatio + _AccentRatio)
col = _ColorB.rgb;
else
col = _ColorA.rgb;
// UV.y ๊ธฐ๋ฐ ๋๋ถ๋ถ ์ฝํ ํ์ด๋ (์ข
์ด ๋ ๋๋)
float fade = 1.0 - pow(abs(i.uv.y - 0.5) * 2.0, 3.0) * 0.3;
// URP ๋ํจ์ฆ ๋ผ์ดํ
Light mainLight = GetMainLight();
float NdotL = saturate(dot(normalize(i.normalWS), mainLight.direction));
float lighting = 0.5 + NdotL * 0.5;
col *= lighting * mainLight.color * fade;
return float4(col, 1.0);
}
ENDHLSL
}
}
}