


Shader.Find("Custom/SpiralVortexLit") : ์กฐ๋ช
์ํฅ ๋ฐ๋ ์
ฐ์ด๋Shader.Find("Custom/SpiralVortexUnlit") : ์กฐ๋ช
์ํฅ ๋ฐ์ง ์๋ ์
ฐ์ด๋GPU Instancing์ผ๋ก ํํฐํด์ ๋ง๋ค ๋ URP/Lit ์
ฐ์ด๋๋ DrawMeshInstanced + MaterialPropertyBlock์ _BaseColor๋ฅผ per-instance๋ก ์ฝ์ง ์์ต๋๋ค. ์ด ์ํ๋ ์ ์ฒด์ ๋์ผํ ์์ ์ ์ฉํฉ๋๋ค.
๊ทธ๋ผ๋ฐ์ด์
์ผ๋ก ๋ฎ์ผ๋ ค๋ฉด UNITY_INSTANCING_BUFFER๋ฅผ ์ง์ ์ ์ธํ ์ปค์คํ
์
ฐ์ด๋๋ฅผ ๋ง๋ญ๋๋ค. csํ์ผ๊ณผ shader ๋ ํ์ผ์ ๊ฐ์ ํด๋์ ๋๊ณ cs์์ ์ปค์คํ
์
ฐ์ด๋๋ฅผ ์ฝ๋๋ก ํฉ๋๋ค.
| ๋ฐฉ์ | per-instance ์์ ๋์ |
|---|---|
| URP/Lit + MaterialPropertyBlock | โ ๋ด๋ถ์ ์ผ๋ก SRP Batcher๊ฐ ๊ฐ์ ํด instanced prop ๋ฌด์ |
| ์ปค์คํ ์ ฐ์ด๋ + UNITY_INSTANCING_BUFFER | โ #pragma multi_compile_instancing์ผ๋ก ๊ฐ ์ธ์คํด์ค๊ฐ ๋ ๋ฆฝ์ ์ผ๋ก ๋ฒํผ ์ ๊ทผ |
์ด๋ ๊ฒ ์ปค์คํ ์ ฐ์ดํฐ๋ฅผ ๋ง๋ค ๋ ๊ธฐ๋ณธ์ ์ผ๋ก Unlit์ ์ ํํ๋ฉด ๋ผ์ดํ ์ฐ์ฐ์ด ์ ํ ์์ด์ ์กฐ๋ช ๊ณผ ์์ ํ ๋ฌด๊ดํ flat color๋ก๋ง ๋ ๋๋ฉ๋๋ค.
| ํญ๋ชฉ | Unlit | Lit |
|---|---|---|
| ์กฐ๋ช ์ํฅ | โ | โ |
| ์์ (Diffuse / Specular) | โ | โ |
| ๊ทธ๋ฆผ์ ์์ | โ | โ (ShadowCaster Pass ํ์) |
| Per-instance Color | โ (์ง์ ๊ตฌํ) | โ (SRP Batcher ๊ฐ์ญ) |
| ํผํฌ๋จผ์ค | ๊ฐ๋ฒผ์ | ์๋์ ์ผ๋ก ๋ฌด๊ฑฐ์ |
์กฐ๋ช ์ ๋ฐ์ผ๋ ค๋ฉด ์ ฐ์ด๋์ ๋ผ์ดํ ์ฐ์ฐ์ ์ถ๊ฐํด์ผ ํฉ๋๋ค. ํต์ฌ์ ๋ ๊ฐ์ง์ ๋๋ค.
GetMainLight()๋ก ๋ฉ์ธ ๋ผ์ดํธ๋ฅผ ๋ฐ์ ๊ณ์ฐusing System.Collections.Generic;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif
[ExecuteAlways]
public class SpiralVortexGPUInstancing : MonoBehaviour
{
enum RenderPipeline { BuiltIn, URP, HDRP }
struct InstanceData
{
public float age;
public float lifetime;
public float baseAngle;
public float radiusOffset;
public float riseSpeed;
public float size;
public float noiseSeed;
}
// =========================================================
[Header("Structure")]
[SerializeField] int particleCount = 2000;
[SerializeField] float spiralRadius = 2.0f;
[SerializeField] float radiusJitter = 0.6f;
[SerializeField] float maxHeight = 8.0f;
[SerializeField] float riseSpeedMin = 1.0f;
[SerializeField] float riseSpeedMax = 2.0f;
[SerializeField] float twistSpeed = 3.0f;
[SerializeField] int randomSeed = 1234;
[Header("Radius Growth")]
[SerializeField, Range(0.1f, 4f),
Tooltip("๋ฐ๊ฒฝ ์ฑ์ฅ ๊ณก์ . 1=์ ํ / >1=์๋์ชฝ์์ ์ข๊ฒ ์ ์ง, ์๋ก ๊ฐ์๋ก ๋ฒ์ด์ง / <1=๋น ๋ฅด๊ฒ ํผ์ณ์ง")]
float radiusGrowthCurve = 1.8f;
[Header("Rise Speed Curve")]
[SerializeField, Range(0.1f, 3f),
Tooltip("์์น ์๋ ๊ณก์ . <1=๋น ๋ฅด๊ฒ ์ถ๋ฐ ์ ์ ๊ฐ์ / 1=์ ํ / >1=์ฒ์ฒํ ์ถ๋ฐ ์ ์ ๊ฐ์")]
float riseCurve = 0.4f;
[Header("Scale")]
[SerializeField] float minSize = 0.05f;
[SerializeField] float maxSize = 0.18f;
[Header("Color")]
[SerializeField, Tooltip("ํํฐํด ์์ฑ ์งํ ์์ (ํ๋จ)")]
Color birthColor = new Color(0.2f, 0.8f, 1.0f, 1.0f);
[SerializeField, Tooltip("ํํฐํด ์๋ฉธ ์ง์ ์์ (์๋จ)")]
Color deathColor = new Color(1.0f, 0.2f, 0.8f, 1.0f);
[SerializeField] float emissionIntensity = 1.5f;
[Header("Noise")]
[SerializeField] float noiseStrength = 0.35f;
[SerializeField] float noiseScale = 0.45f;
[SerializeField] float noiseScrollSpeed = 0.7f;
[SerializeField] float verticalNoiseAmount = 0.25f;
[Header("Rendering")]
[SerializeField] Mesh instanceMesh;
[SerializeField] Material instanceMaterial;
// =========================================================
const int BATCH_SIZE = 1023;
readonly List<InstanceData> _instances = new List<InstanceData>();
Matrix4x4[] _matrices;
Vector4[] _colors;
Vector4[] _emissions;
bool _generated;
float _simTime;
RenderPipeline _pipeline;
// --- Structure cache ---
int _cachedParticleCount;
float _cachedSpiralRadius, _cachedRadiusJitter, _cachedMaxHeight;
float _cachedRiseSpeedMin, _cachedRiseSpeedMax, _cachedTwistSpeed;
float _cachedMinSize, _cachedMaxSize;
int _cachedRandomSeed;
#if UNITY_EDITOR
bool _generateQueued;
#endif
// =========================================================
void OnEnable()
{
_pipeline = DetectPipeline();
EnsureResources();
Generate();
CacheStructureParams();
}
void OnValidate()
{
particleCount = Mathf.Max(1, particleCount);
spiralRadius = Mathf.Max(0f, spiralRadius);
radiusJitter = Mathf.Max(0f, radiusJitter);
maxHeight = Mathf.Max(0.01f, maxHeight);
riseSpeedMin = Mathf.Max(0.01f, riseSpeedMin);
riseSpeedMax = Mathf.Max(riseSpeedMin, riseSpeedMax);
minSize = Mathf.Max(0.001f, minSize);
maxSize = Mathf.Max(minSize, maxSize);
noiseStrength = Mathf.Max(0f, noiseStrength);
noiseScale = Mathf.Max(0.001f, noiseScale);
noiseScrollSpeed = Mathf.Max(0f, noiseScrollSpeed);
verticalNoiseAmount = Mathf.Max(0f, verticalNoiseAmount);
#if UNITY_EDITOR
_pipeline = DetectPipeline();
EnsureResources();
if (StructureParamsChanged())
{
CacheStructureParams();
QueueGenerate();
}
else
{
Animate(Application.isPlaying ? _simTime : 0f);
}
#endif
}
void Update()
{
if (!_generated) return;
if (!Application.isPlaying)
{
Animate(0f);
RenderInstances();
return;
}
float dt = Time.deltaTime;
_simTime += dt;
StepSimulation(dt);
Animate(_simTime);
RenderInstances();
}
// =========================================================
// ๊ธฐ๋ณธ ๋ฉ์๋ฅผ Cube๋ก ์์ฑ. ๋จธํฐ๋ฆฌ์ผ์ ํ์ดํ๋ผ์ธ์ ๋ง์ถฐ ์๋ ์ ํ.
void EnsureResources()
{
if (instanceMesh == null)
{
GameObject temp = GameObject.CreatePrimitive(PrimitiveType.Cube);
instanceMesh = temp.GetComponent<MeshFilter>().sharedMesh;
DestroyImmediate(temp);
}
if (instanceMaterial == null)
{
// ์ปค์คํ
์
ฐ์ด๋ ์ฐ์ , ์์ผ๋ฉด URP Unlit ํด๋ฐฑ
Shader shader =
Shader.Find("Custom/SpiralVortexLit") ??
Shader.Find("Universal Render Pipeline/Unlit") ??
Shader.Find("Standard");
instanceMaterial = new Material(shader);
instanceMaterial.enableInstancing = true;
}
}
// ํํฐํด ๋ฐฐ์ด ์ด๊ธฐํ ๋ฐ ๋๋ค ๋ฐฐ์น
void Generate()
{
_instances.Clear();
_generated = false;
_simTime = 0f;
Random.InitState(randomSeed);
for (int i = 0; i < particleCount; i++)
_instances.Add(CreateParticle(true));
_matrices = new Matrix4x4[_instances.Count];
_colors = new Vector4[_instances.Count];
_emissions = new Vector4[_instances.Count];
_generated = true;
Animate(0f);
}
InstanceData CreateParticle(bool randomizeAge)
{
float riseSpeed = Random.Range(riseSpeedMin, riseSpeedMax);
float lifetime = maxHeight / riseSpeed;
float age = randomizeAge ? Random.Range(0f, lifetime) : 0f;
return new InstanceData
{
age = age,
lifetime = lifetime,
baseAngle = Random.Range(0f, Mathf.PI * 2f),
radiusOffset = Random.Range(-radiusJitter, radiusJitter),
riseSpeed = riseSpeed,
size = Random.Range(minSize, maxSize),
noiseSeed = Random.Range(0f, 1000f)
};
}
void StepSimulation(float dt)
{
for (int i = 0; i < _instances.Count; i++)
{
InstanceData inst = _instances[i];
inst.age += dt;
if (inst.age >= inst.lifetime)
inst = CreateParticle(false);
_instances[i] = inst;
}
}
// =========================================================
// ๋งค ํ๋ ์ ๋ชจ๋ ํํฐํด์ ์์น / ํ์ / ์์ ๊ณ์ฐ
void Animate(float t)
{
if (_matrices == null || _colors == null || _emissions == null) return;
for (int i = 0; i < _instances.Count; i++)
{
InstanceData inst = _instances[i];
float life01 = Mathf.Clamp01(inst.age / inst.lifetime);
// riseCurve < 1 โ ์๋์์ ๋น ๋ฅด๊ฒ ์ถ๋ฐ, ์๋ก ๊ฐ์๋ก ๊ฐ์
float curvedLife = Mathf.Pow(life01, riseCurve);
float yBase = curvedLife * maxHeight;
// --- ๋
ธ์ด์ฆ ---
float noiseTime = t * noiseScrollSpeed;
float n1 = Mathf.PerlinNoise(inst.noiseSeed, yBase * noiseScale + noiseTime);
float n2 = Mathf.PerlinNoise(inst.noiseSeed + 31.7f, yBase * noiseScale + noiseTime);
float sn1 = (n1 - 0.5f) * 2f;
float sn2 = (n2 - 0.5f) * 2f;
// ๋ฐ๊ฒฝ: ํ๋จ์์ 0 โ ์๋จ์์ spiralRadius ๊น์ง ์ฑ์ฅ (radiusGrowthCurve๋ก ๊ณก์ ์ ์ด)
float angle = inst.baseAngle + inst.age * twistSpeed + sn1 * noiseStrength;
float radius = Mathf.Pow(life01, radiusGrowthCurve) * spiralRadius
+ inst.radiusOffset + sn2 * noiseStrength;
radius = Mathf.Max(0.01f, radius);
float y = yBase + sn1 * verticalNoiseAmount * noiseStrength;
Vector3 localPos = new Vector3(Mathf.Cos(angle) * radius, y, Mathf.Sin(angle) * radius);
Vector3 worldPos = transform.TransformPoint(localPos);
Quaternion worldRot = transform.rotation * Quaternion.Euler(0f, -Mathf.Rad2Deg * angle, 0f);
_matrices[i] = Matrix4x4.TRS(worldPos, worldRot, Vector3.one * inst.size);
// ์์์ Y์ถ ๋์ด์ ๋ฐ๋ผ birthColor โ deathColor ๊ทธ๋ผ๋ฐ์ด์
float heightT = Mathf.Clamp01(y / maxHeight);
Color albedo = Color.Lerp(birthColor, deathColor, heightT);
Color emission = albedo * emissionIntensity;
_colors[i] = new Vector4(albedo.r, albedo.g, albedo.b, albedo.a);
_emissions[i] = new Vector4(emission.r, emission.g, emission.b, 1f);
}
}
// =========================================================
// 1023๊ฐ ๋จ์ ๋ฐฐ์น๋ก DrawMeshInstanced ํธ์ถ
void RenderInstances()
{
if (instanceMesh == null || instanceMaterial == null) return;
int total = _instances.Count;
int index = 0;
while (total > 0)
{
int batch = Mathf.Min(BATCH_SIZE, total);
Matrix4x4[] batchMatrices = new Matrix4x4[batch];
Vector4[] batchColors = new Vector4[batch];
System.Array.Copy(_matrices, index, batchMatrices, 0, batch);
System.Array.Copy(_colors, index, batchColors, 0, batch);
MaterialPropertyBlock block = new MaterialPropertyBlock();
block.SetVectorArray("_BaseColor", batchColors);
Graphics.DrawMeshInstanced(
instanceMesh, 0, instanceMaterial,
batchMatrices, batch, block
);
index += batch;
total -= batch;
}
}
// =========================================================
RenderPipeline DetectPipeline()
{
var rp = UnityEngine.Rendering.GraphicsSettings.currentRenderPipeline;
if (rp == null) return RenderPipeline.BuiltIn;
string name = rp.GetType().Name;
if (name.Contains("Universal")) return RenderPipeline.URP;
if (name.Contains("HighDefinition") || name.Contains("HDRP")) return RenderPipeline.HDRP;
return RenderPipeline.BuiltIn;
}
string GetColorPropertyName() => _pipeline switch
{
RenderPipeline.URP => "_BaseColor",
RenderPipeline.HDRP => "_BaseColor",
_ => "_Color"
};
string GetEmissionPropertyName() => _pipeline switch
{
RenderPipeline.HDRP => "_EmissiveColor",
_ => "_EmissionColor"
};
// =========================================================
bool StructureParamsChanged() =>
_cachedParticleCount != particleCount ||
!Mathf.Approximately(_cachedSpiralRadius, spiralRadius) ||
!Mathf.Approximately(_cachedRadiusJitter, radiusJitter) ||
!Mathf.Approximately(_cachedMaxHeight, maxHeight) ||
!Mathf.Approximately(_cachedRiseSpeedMin, riseSpeedMin) ||
!Mathf.Approximately(_cachedRiseSpeedMax, riseSpeedMax) ||
!Mathf.Approximately(_cachedTwistSpeed, twistSpeed) ||
!Mathf.Approximately(_cachedMinSize, minSize) ||
!Mathf.Approximately(_cachedMaxSize, maxSize) ||
_cachedRandomSeed != randomSeed;
void CacheStructureParams()
{
_cachedParticleCount = particleCount;
_cachedSpiralRadius = spiralRadius;
_cachedRadiusJitter = radiusJitter;
_cachedMaxHeight = maxHeight;
_cachedRiseSpeedMin = riseSpeedMin;
_cachedRiseSpeedMax = riseSpeedMax;
_cachedTwistSpeed = twistSpeed;
_cachedMinSize = minSize;
_cachedMaxSize = maxSize;
_cachedRandomSeed = randomSeed;
}
#if UNITY_EDITOR
void QueueGenerate()
{
if (_generateQueued) return;
_generateQueued = true;
EditorApplication.delayCall += () =>
{
_generateQueued = false;
if (this == null) return;
Generate();
};
}
#endif
}
Shader "Custom/SpiralVortexUnlit"
{
Properties
{
[PerRendererData] _BaseColor ("Color", Color) = (1,1,1,1)
}
SubShader
{
Tags
{
"RenderType" = "Opaque"
"Queue" = "Geometry"
"RenderPipeline" = "UniversalPipeline"
}
Cull Back
ZWrite On
Pass
{
Name "UniversalForward"
Tags { "LightMode" = "UniversalForward" }
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_instancing
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
// per-instance color ๋ฒํผ
UNITY_INSTANCING_BUFFER_START(Props)
UNITY_DEFINE_INSTANCED_PROP(float4, _BaseColor)
UNITY_INSTANCING_BUFFER_END(Props)
struct Attributes
{
float4 positionOS : POSITION;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct Varyings
{
float4 positionCS : SV_POSITION;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
Varyings vert(Attributes IN)
{
Varyings OUT;
UNITY_SETUP_INSTANCE_ID(IN);
UNITY_TRANSFER_INSTANCE_ID(IN, OUT);
OUT.positionCS = TransformObjectToHClip(IN.positionOS.xyz);
return OUT;
}
half4 frag(Varyings IN) : SV_Target
{
UNITY_SETUP_INSTANCE_ID(IN);
return half4(UNITY_ACCESS_INSTANCED_PROP(Props, _BaseColor));
}
ENDHLSL
}
}
}
Shader "Custom/SpiralVortexLit"
{
Properties
{
[PerRendererData] _BaseColor ("Color", Color) = (1,1,1,1)
_Smoothness ("Smoothness", Range(0,1)) = 0.3
}
SubShader
{
Tags
{
"RenderType" = "Opaque"
"Queue" = "Geometry"
"RenderPipeline" = "UniversalPipeline"
}
Cull Back
ZWrite On
// --- ๋ฉ์ธ ์กฐ๋ช
+ ์์ ํจ์ค ---
Pass
{
Name "UniversalForward"
Tags { "LightMode" = "UniversalForward" }
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_instancing
#pragma multi_compile _ _MAIN_LIGHT_SHADOWS _MAIN_LIGHT_SHADOWS_CASCADE
#pragma multi_compile _ _SHADOWS_SOFT
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
UNITY_INSTANCING_BUFFER_START(Props)
UNITY_DEFINE_INSTANCED_PROP(float4, _BaseColor)
UNITY_INSTANCING_BUFFER_END(Props)
CBUFFER_START(UnityPerMaterial)
float _Smoothness;
CBUFFER_END
struct Attributes
{
float4 positionOS : POSITION;
float3 normalOS : NORMAL;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct Varyings
{
float4 positionCS : SV_POSITION;
float3 normalWS : TEXCOORD0;
float3 positionWS : TEXCOORD1;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
Varyings vert(Attributes IN)
{
Varyings OUT;
UNITY_SETUP_INSTANCE_ID(IN);
UNITY_TRANSFER_INSTANCE_ID(IN, OUT);
VertexPositionInputs posInputs = GetVertexPositionInputs(IN.positionOS.xyz);
VertexNormalInputs nrmInputs = GetVertexNormalInputs(IN.normalOS);
OUT.positionCS = posInputs.positionCS;
OUT.positionWS = posInputs.positionWS;
OUT.normalWS = nrmInputs.normalWS;
return OUT;
}
half4 frag(Varyings IN) : SV_Target
{
UNITY_SETUP_INSTANCE_ID(IN);
float4 baseColor = UNITY_ACCESS_INSTANCED_PROP(Props, _BaseColor);
// ๋ฉ์ธ ๋ผ์ดํธ
float4 shadowCoord = TransformWorldToShadowCoord(IN.positionWS);
Light mainLight = GetMainLight(shadowCoord);
float3 normalWS = normalize(IN.normalWS);
float NdotL = saturate(dot(normalWS, mainLight.direction));
// Lambert diffuse + ambient
float3 ambient = SampleSH(normalWS) * baseColor.rgb;
float3 diffuse = mainLight.color * mainLight.shadowAttenuation * NdotL * baseColor.rgb;
return half4(ambient + diffuse, baseColor.a);
}
ENDHLSL
}
// --- ๊ทธ๋ฆผ์ ์บ์คํฐ ํจ์ค ---
Pass
{
Name "ShadowCaster"
Tags { "LightMode" = "ShadowCaster" }
ZWrite On
ZTest LEqual
ColorMask 0
HLSLPROGRAM
#pragma vertex vertShadow
#pragma fragment fragShadow
#pragma multi_compile_instancing
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Shadows.hlsl"
struct AttributesShadow
{
float4 positionOS : POSITION;
float3 normalOS : NORMAL;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct VaryingsShadow
{
float4 positionCS : SV_POSITION;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
VaryingsShadow vertShadow(AttributesShadow IN)
{
VaryingsShadow OUT;
UNITY_SETUP_INSTANCE_ID(IN);
UNITY_TRANSFER_INSTANCE_ID(IN, OUT);
float3 posWS = TransformObjectToWorld(IN.positionOS.xyz);
float3 nrmWS = TransformObjectToWorldNormal(IN.normalOS);
float4 posCS = TransformWorldToHClip(ApplyShadowBias(posWS, nrmWS, _LightDirection));
// ๋ค์งํ ๋ฐฉ์ง
#if UNITY_REVERSED_Z
posCS.z = min(posCS.z, posCS.w * UNITY_NEAR_CLIP_VALUE);
#else
posCS.z = max(posCS.z, posCS.w * UNITY_NEAR_CLIP_VALUE);
#endif
OUT.positionCS = posCS;
return OUT;
}
half4 fragShadow(VaryingsShadow IN) : SV_Target { return 0; }
ENDHLSL
}
}
}