


Assets/
βββ SpiralGalaxy/
βββ SpiralGalaxySystem.cs β C# μ€ν¬λ¦½νΈ
βββ Resources/
β βββ SpiralGalaxyCompute.compute β β
Resources ν΄λ νμ
βββ Shaders/
βββ SpiralGalaxy.shader
μ£Όμ:
SpiralGalaxyCompute.computeλ λ°λμResources/ν΄λ μμ μμ΄μΌ ν©λλ€.
SpiralGalaxy.shaderλ μ΄λ ν΄λμ μμ΄λ 무방ν©λλ€ (Shader.Findλ‘ μ΄λ¦ κ²μ).
Create Empty)SpiralGalaxySystem μ»΄ν¬λνΈ μΆκ°[ExecuteAlways] κ° μ μΈλμ΄ μμ΄ Play μμ΄ Scene/Game λ·°μμ μ¦μ λ λλ§λ©λλ€.OnValidate β Cleanup β Initialize μμλ‘ μ¬μ΄κΈ°νλ©λλ€.| νλΌλ―Έν° | κΈ°λ³Έκ° | μ€λͺ |
|---|---|---|
| Particle Count | 500,000 | νν°ν΄ μ (GPU λ©λͺ¨λ¦¬ μ£Όμ) |
| Galaxy Radius | 40 | μν λ°μ§λ¦ (Unit) |
| Spiral Tightness | 0.35 | λμ κ°κΈ° μ λ |
| Arm Count | 4 | λμ ν μ |
| Arm Width | 0.25 | λμ ν λκ» (0~1) |
| Disk Thickness | 0.08 | μλ° μμ§ λκ» λΉμ¨ |
| Rotation Speed | 4 | νμ μλ (Play λͺ¨λ) |
| Particle Size Min/Max | 0.04 / 0.18 | νν°ν΄ ν¬κΈ° λ²μ |
| Brightness Boost | 1.2 | μ 체 λ°κΈ° λ°°μ¨ |
Solid Color β μμ ν κ²μ (0,0,0,0)Solid Colorμ΄ μ °μ΄λλ Built-in Render Pipeline κΈ°μ€μ λλ€.
URP μ¬μ© μ SpiralGalaxy.shader μ Pass λΈλ‘μ μλμ κ°μ΄ μμ νμΈμ:
Tags { "LightMode" = "UniversalForward" }
κ·Έλ¦¬κ³ UnityCG.cginc β Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl λ‘ κ΅μ²΄ν©λλ€.
| μ¦μ | μμΈ | ν΄κ²° |
|---|---|---|
SpiralGalaxyCompute.compute λ₯Ό μ°Ύμ μ μμ΅λλ€ | Resources ν΄λ λ―Έμ‘΄μ¬ | Resources/ ν΄λ μμ .compute νμΌ λ°°μΉ |
Shader 'SpiralGalaxy/Particle' λ₯Ό μ°Ύμ μ μμ΅λλ€ | Shader μ΄λ¦ λΆμΌμΉ | .shader νμΌ μ²« μ€ Shader "SpiralGalaxy/Particle" νμΈ |
| μλν°μμ μ무κ²λ μ 보μ | Scene view Camera not rendering | Scene λ·° μλ¨ Gizmos νμ±ν, Game λ·° νμΈ |
| νν°ν΄μ΄ ν°μ μ¬κ°νμΌλ‘ νμλ¨ | Shader compile μ€λ₯ | Console μ°½ μ€λ₯ νμΈ, #pragma target 4.5 μ§μ GPU μ¬λΆ νμΈ |
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif
/// ν΄λ ꡬ쑰:
/// Assets/SpiralGalaxy/
/// βββ SpiralGalaxySystem.cs
/// βββ Resources/
/// β βββ SpiralGalaxyCompute.compute
/// βββ Shaders/
/// βββ SpiralGalaxy.shader
[ExecuteAlways]
public class SpiralGalaxySystem : MonoBehaviour
{
[Header("Particle Settings")]
public int particleCount = 500000;
[Header("Galaxy Shape")]
public float galaxyRadius = 40f;
public float spiralTightness = 0.35f;
public int armCount = 4;
public float armWidth = 0.25f;
public float diskThickness = 0.08f;
[Header("Animation")]
public float rotationSpeed = 4f;
[Header("Visuals")]
public float particleSizeMin = 0.04f;
public float particleSizeMax = 0.18f;
public float brightnessBoost = 1.2f;
// ββ λ΄λΆ μν βββββββββββββββββββββββββββββββββββββββββββββββββ
ComputeShader _compute;
Material _material;
Mesh _mesh;
ComputeBuffer _particleBuffer;
int _kernelInit;
int _kernelUpdate;
int _threadGroups;
bool _initialized;
// position(float3) + velocity(float3) + size(float) + color(float4) = 11 floats = 44 bytes
const int PARTICLE_STRIDE = 11 * sizeof(float);
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
void OnEnable() => Initialize();
void OnDisable() => Cleanup();
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
void Initialize()
{
if (_initialized) return;
// ββ 1. ComputeShader (Resources ν΄λ νμ) ββββββββββββββββ
if (_compute == null)
{
_compute = Resources.Load<ComputeShader>("SpiralGalaxyCompute");
if (_compute == null)
{
Debug.LogError("[SpiralGalaxy] Resources/SpiralGalaxyCompute.compute μμ");
return;
}
}
// ββ 2. Shader β Material ββββββββββββββββββββββββββββββββββ
if (_material == null)
{
var shader = Shader.Find("SpiralGalaxy/Particle");
if (shader == null)
{
Debug.LogError("[SpiralGalaxy] Shader 'SpiralGalaxy/Particle' μμ");
return;
}
_material = new Material(shader) { hideFlags = HideFlags.HideAndDontSave };
}
// ββ 3. Quad Mesh μ§μ μμ± (CreatePrimitive μμ) βββββββββ
if (_mesh == null)
_mesh = CreateQuadMesh();
// ββ 4. 컀λ μΈλ±μ€ ββββββββββββββββββββββββββββββββββββββββ
_kernelInit = _compute.FindKernel("Initialize");
_kernelUpdate = _compute.FindKernel("Update");
// ββ 5. ComputeBuffer ββββββββββββββββββββββββββββββββββββββ
_particleBuffer = new ComputeBuffer(particleCount, PARTICLE_STRIDE);
// ββ 6. λ²νΌ λ°μΈλ© ββββββββββββββββββββββββββββββββββββββββ
_compute.SetBuffer(_kernelInit, "particles", _particleBuffer);
_compute.SetBuffer(_kernelUpdate, "particles", _particleBuffer);
_material.SetBuffer("particles", _particleBuffer);
// ββ 7. νλΌλ―Έν° β Initialize 컀λ μ€ν βββββββββββββββββββ
UploadShapeParams();
_threadGroups = Mathf.CeilToInt(particleCount / 256f);
_compute.Dispatch(_kernelInit, _threadGroups, 1, 1);
_initialized = true;
}
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
void Update()
{
if (!_initialized) Initialize();
if (!_initialized) return;
if (Application.isPlaying)
{
_compute.SetFloat("deltaTime", Time.deltaTime);
_compute.SetFloat("time", Time.time);
_compute.SetFloat("rotationSpeed", rotationSpeed);
_compute.Dispatch(_kernelUpdate, _threadGroups, 1, 1);
}
_material.SetFloat("_BrightnessBoost", brightnessBoost);
Render();
}
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
void Render()
{
if (_mesh == null || _material == null || _particleBuffer == null) return;
Graphics.DrawMeshInstancedProcedural(
_mesh,
0,
_material,
new Bounds(Vector3.zero, Vector3.one * galaxyRadius * 3f),
particleCount
);
}
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
void UploadShapeParams()
{
_compute.SetFloat("galaxyRadius", galaxyRadius);
_compute.SetFloat("spiralTightness", spiralTightness);
_compute.SetFloat("armWidth", armWidth);
_compute.SetFloat("diskThickness", diskThickness);
_compute.SetFloat("particleSizeMin", particleSizeMin);
_compute.SetFloat("particleSizeMax", particleSizeMax);
_compute.SetInt("particleCount", particleCount);
_compute.SetInt("armCount", armCount);
}
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
void Cleanup()
{
_particleBuffer?.Release();
_particleBuffer = null;
SafeDestroy(_material); _material = null;
SafeDestroy(_mesh); _mesh = null;
_initialized = false;
}
// OnValidate μμ DestroyImmediate μ§μ νΈμΆ κΈμ§
// β delayCall λ‘ νμ¬ μ½λ°±μ΄ λλ λ€ μ¬μ΄κΈ°ν
void OnValidate()
{
#if UNITY_EDITOR
EditorApplication.delayCall += () =>
{
if (this == null) return; // μ»΄ν¬λνΈκ° μμ λ κ²½μ° λ°©μ΄
Cleanup();
Initialize();
};
#endif
}
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// CreatePrimitive + Destroy λμ Mesh μ§μ μμ±
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
static Mesh CreateQuadMesh()
{
var m = new Mesh
{
name = "GalaxyQuad",
hideFlags = HideFlags.HideAndDontSave
};
m.vertices = new[]
{
new Vector3(-0.5f, -0.5f, 0f),
new Vector3( 0.5f, -0.5f, 0f),
new Vector3( 0.5f, 0.5f, 0f),
new Vector3(-0.5f, 0.5f, 0f),
};
m.uv = new[]
{
new Vector2(0f, 0f),
new Vector2(1f, 0f),
new Vector2(1f, 1f),
new Vector2(0f, 1f),
};
m.triangles = new[] { 0, 2, 1, 0, 3, 2 };
m.RecalculateNormals();
return m;
}
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// μλν°/λ°νμ κ³΅ν΅ μμ μμ
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
static void SafeDestroy(Object obj)
{
if (obj == null) return;
#if UNITY_EDITOR
if (!Application.isPlaying)
DestroyImmedi...
<...etc...>
// SpiralGalaxyCompute.compute
// μμΉ: Assets/SpiralGalaxy/Resources/SpiralGalaxyCompute.compute
//
// Particle stride = 11 floats = 44 bytes
// float3 position (0~11)
// float3 velocity (12~23)
// float size (24~27)
// float4 color (28~43)
#pragma kernel Initialize
#pragma kernel Update
struct Particle
{
float3 position;
float3 velocity;
float size;
float4 color;
};
RWStructuredBuffer<Particle> particles;
// ββ Galaxy shape params ββββββββββββββββββββββββββββββββββββββββββ
float galaxyRadius;
float spiralTightness;
float armWidth;
float diskThickness;
float particleSizeMin;
float particleSizeMax;
int particleCount;
int armCount;
// ββ Animation params βββββββββββββββββββββββββββββββββββββββββββββ
float rotationSpeed;
float deltaTime;
float time;
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// Hash functions (integer-based, no stdlib μμ‘΄ μμ)
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
uint hash1(uint n)
{
n = (n ^ 61u) ^ (n >> 16u);
n *= 9u;
n = n ^ (n >> 4u);
n *= 0x27d4eb2du;
n = n ^ (n >> 15u);
return n;
}
float rand(uint seed)
{
return float(hash1(seed) & 0x00FFFFFFu) / float(0x01000000u);
}
// Box-Muller: κ°μ°μμ λΆν¬ (νκ· 0, νμ€νΈμ°¨ 1)
float2 randGaussian(uint seed)
{
float u1 = max(rand(seed * 1664525u + 1013904223u), 1e-6);
float u2 = rand(seed * 22695477u + 1u);
float mag = sqrt(-2.0 * log(u1));
float phi = 6.28318530718 * u2;
return float2(mag * cos(phi), mag * sin(phi));
}
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// Initialize 컀λ
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
[numthreads(256, 1, 1)]
void Initialize(uint3 id : SV_DispatchThreadID)
{
uint i = id.x;
if ((int)i >= particleCount) return;
// ββ λμ ν μ ν ββββββββββββββββββββββββββββββββββββββββββββββ
int arm = (int)(i % (uint)armCount);
float armAngle = (float)arm / (float)armCount * 6.28318530718;
// ββ λ°μ§λ¦: μ§μ λΆν¬ (μ€μ¬μ μ§μ€, μΈκ³½μΌλ‘ ν¬λ―Έν΄μ§) ββββββββ
float r_raw = rand(hash1(i * 3u + 0u));
// pow(x, 0.6)μΌλ‘ μ€κ° λ°μ§λ¦λ λ°λ κ°ν
float r = pow(r_raw, 0.6) * galaxyRadius;
// ββ λμ κ°λ = λμ ν μμ + λμ κ°κΈ° + ν λ΄ λΆμ° βββββββββ
float spiralAngle = r * spiralTightness * 6.28318530718;
// ν λ΄ κ°λ λΆμ° (κ°μ°μμ)
float2 g = randGaussian(hash1(i * 7u + 1u));
float scatter = g.x * armWidth * (1.0 + r / galaxyRadius); // μΈκ³½μΌμλ‘ νΌμ§
float theta = armAngle + spiralAngle + scatter;
// ββ μμΉ κ³μ° βββββββββββββββββββββββββββββββββββββββββββββββββ
float x = r * cos(theta);
float z = r * sin(theta);
// μμ§ λ°©ν₯: μ€μ¬μ λκ»κ³ μΈκ³½μ μμ μλ°
float yScale = galaxyRadius * diskThickness * (1.0 - r / galaxyRadius * 0.7);
float y = g.y * yScale;
particles[i].position = float3(x, y, z);
particles[i].velocity = float3(0, 0, 0);
// ββ νν°ν΄ ν¬κΈ° βββββββββββββββββββββββββββββββββββββββββββββββ
float sizeFactor = rand(hash1(i * 11u + 3u));
// μ€μ¬λΆμ μ½κ° λ ν° μ
μ λ°°μΉ
float coreBoost = exp(-r / (galaxyRadius * 0.3)) * 0.5;
particles[i].size = lerp(particleSizeMin, particleSizeMax, sizeFactor + coreBoost);
// ββ μμ: μ€μ¬(μ²λ°±) β μ€κ°(λ°λ»ν ν°μ) β μΈκ³½(μ κ°μ/ν¬λ―Έ) ββ
float t = r / galaxyRadius; // 0 = μ€μ¬, 1 = μΈκ³½
// λμ ν μμ μλμ§ νλ³ (ν κ°λμμ 거리)
float onArm = saturate(1.0 - abs(scatter) / (armWidth * 2.0));
float4 coreColor = float4(0.55, 0.80, 1.00, 1.0); // μ°¨κ°μ΄ μ²λ°±
float4 midColor = float4(1.00, 0.95, 0.80, 1.0); // λ°λ»ν ν°μ
float4 outerColor = float4(0.90, 0.45, 0.20, 0.5); // λΆμλΉ, ν¬λ―Έ
float4 col;
if (t < 0.4)
col = lerp(coreColor, midColor, t / 0.4);
else
col = lerp(midColor, outerColor, (t - 0.4) / 0.6);
// λμ ν λ°κΉ₯ μ
μλ λ ν¬λ―Ένκ²
col.a *= lerp(0.3, 1.0, onArm);
// μ€μ¬μΌλ‘ κ°μλ‘ λ°κ²
col.a *= lerp(0.5, 1.0, 1.0 - t * 0.8);
// μΈκ³½ νμ΄λμμ
col.a *= saturate(1.0 - t * t);
particles[i].color = col;
}
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// Update 컀λ (Play λͺ¨λμμλ§ νΈμΆλ¨)
// μ°¨λ± νμ (Differential rotation): λ΄λΆκ° λ λΉ λ₯΄κ² νμ
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
[numthreads(256, 1, 1)]
void Update(uint3 id : SV_DispatchThreadID)
{
uint i = id.x;
if ((int)i >= particleCount) return;
float3 pos = particles[i].position;
// XZ νλ©΄ λ°μ§λ¦
float r = length(float2(pos.x, pos.z));
// μΌνλ¬μ μ°¨λ± νμ : μ€μ¬ κ·Όμ²λ λΉ λ₯΄κ³ , μΈκ³½μ λλ¦Ό
// v β 1 / sqrt(r + epsilon) λ₯Ό κ·Όμ¬
float keplerian = rotationSpeed / sqrt(max(r * 0.4 + 0.5, 0.01));
float angle = keplerian * deltaTime;
float cosA = cos(angle);
float sinA = sin(angle);
float newX = pos.x * cosA - pos.z * sinA;
float newZ = pos.x * sinA + pos.z * cosA;
particles[i].position = float3(newX, pos.y, newZ);
}
// μμΉ: Assets/SpiralGalaxy/Shaders/SpiralGalaxy.shader
Shader "SpiralGalaxy/Particle"
{
Properties
{
_BrightnessBoost ("Brightness Boost", Float) = 1.2
}
SubShader
{
Tags
{
"Queue" = "Transparent"
"RenderType" = "Transparent"
"IgnoreProjector" = "True"
}
Pass
{
Blend One One // Additive: νν°ν΄ λμ = μ±μ΄ ν¨κ³Ό
ZWrite Off
ZTest LEqual
Cull Off
Lighting Off
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma target 4.5
// DrawMeshInstancedProcedural μ νμ
#pragma multi_compile_instancing
#pragma instancing_options procedural:setup
#include "UnityCG.cginc"
// ββ Particle ꡬ쑰체 (C# PARTICLE_STRIDE = 44 bytes μ λμΌ) ββ
struct Particle
{
float3 position; // 12 bytes
float3 velocity; // 12 bytes
float size; // 4 bytes
float4 color; // 16 bytes
};
StructuredBuffer<Particle> particles;
float _BrightnessBoost;
// procedural instancing setup ν¨μ (λΉ ν¨μμ¬λ μ μΈ νμ)
void setup() {}
struct appdata
{
float4 vertex : POSITION;
float2 texcoord : TEXCOORD0;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct v2f
{
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
float4 color : COLOR;
};
v2f vert(appdata v)
{
UNITY_SETUP_INSTANCE_ID(v);
// unity_InstanceID: DrawMeshInstancedProcedural μμ
// SV_InstanceID κ° μλμΌλ‘ μ΄ λ³μμ μ μ₯λ¨
Particle p = particles[unity_InstanceID];
// ββ λΉλ³΄λ: View Matrix μ κ° ν = μΉ΄λ©λΌ λ‘컬 μΆ(μλ κΈ°μ€)
//
// UNITY_MATRIX_V (View Matrix) ν ꡬ쑰:
// [0].xyz = μΉ΄λ©λΌ Right (μλ 곡κ°)
// [1].xyz = μΉ΄λ©λΌ Up (μλ 곡κ°)
// [2].xyz = μΉ΄λ©λΌ Back (μλ 곡κ°, -Forward)
//
// μ£Όμ: UNITY_MATRIX_V[row][col] μμ
// UNITY_MATRIX_V[0].xyz κ° μ¬λ°λ₯Έ ν μ κ·Όλ²
// UNITY_MATRIX_V[0][0], [1][0], [2][0] μ μ΄ 0μ κ° μ±λΆμΌλ‘
// Right/Up/Back μ΄ μλλΌ μλͺ»λ 벑ν°κ° λ¨ β μ΄μ λ²μ μ λ²κ·Έ
float3 camRight = UNITY_MATRIX_V[0].xyz;
float3 camUp = UNITY_MATRIX_V[1].xyz;
// Quad UV λ 0~1 λ²μ, μ€μ¬ κΈ°μ€ -0.5~0.5 λ‘ μ€νμ
λ³ν
float2 offset = v.texcoord.xy - 0.5;
float3 worldPos = p.position
+ camRight * (offset.x * p.size)
+ camUp * (offset.y * p.size);
v2f o;
o.pos = UnityWorldToClipPos(worldPos);
o.uv = v.texcoord.xy;
o.color = p.color * _BrightnessBoost;
return o;
}
float4 frag(v2f i) : SV_Target
{
// Gaussian falloff: μ€μ¬μ΄ λ°κ³ κ°μ₯μ리λ λΆλλ½κ² 0μΌλ‘
float2 uv = i.uv * 2.0 - 1.0; // -1 ~ 1
float dist2 = dot(uv, uv); // |uv|^2
float alpha = exp(-dist2 * 3.5);
// κ±°μ ν¬λͺ
ν νν°ν΄ μ‘°κΈ° νκΈ° (fillrate μ μ½)
clip(alpha * i.color.a - 0.002);
float4 col = i.color;
col.rgb *= alpha;
col.a = alpha * i.color.a;
return col;
}
ENDCG
}
}
FallBack Off
}