



FBX๋ก ์ ์ํ 3D ๋ชจ๋ธ๋ง์ ๋ด๋ถ ๋ถํผ๋ฅผ ์์ฒ ๊ฐ์ ์์ ํํฐํด๋ก ์ฑ์ฐ๊ณ , ๊ฐ ํํฐํด์ด Perlin Noise๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ๋ฉ์ ๋ด๋ถ์์ ์ฒ์ฒํ ๋ถ์ ํ๋ ์์ฑ ์ํธ์ํฌ์
๋๋ค.
ํํฐํด์ ๋ชจ๋ธ๋ง์ ๊ฒ ํ๋ฉด์ด ์๋ ๋ซํ ๋ฉ์์ ๋ด๋ถ ๊ณต๊ฐ์ ๋ฌด์์๋ก ๋ฐฐ์น๋ฉ๋๋ค. ํํฐํด ์์ฒด๋ Unity์ Particle System์ ์ฌ์ฉํ์ง ์์ผ๋ฉฐ, GPU Instancing์ผ๋ก ์์ฒ ๊ฐ๋ฅผ ๋จ์ผ ๋๋ก์ฐ ์ฝ์ ๊ฐ๊น์ด ๋น์ฉ์ผ๋ก ๋ ๋๋งํฉ๋๋ค.
์ด ์ํธ์ํฌ๋ 4๊ฐ์ ํ์ผ๋ก ๊ตฌ์ฑ๋ ๋ชจ๋ ์์คํ ์์์ ๋์ํฉ๋๋ค.
GenArtResources โ ๋ฉ์ยท๋จธํฐ๋ฆฌ์ผ ์๋ ์์ฑ (static ์ ํธ)
GPUInstanceRenderer โ DrawMeshInstanced 1023 ๋ฐฐ์น ๋ถํ (์ผ๋ฐ ํด๋์ค)
GenArtBase โ Unity ๋ผ์ดํ์ฌ์ดํด ์ ๋ด (abstract MonoBehaviour)
โโโ MeshVolumeArtwork โ ์ํธ์ํฌ ๋ก์ง (concrete ์๋ธํด๋์ค)
MeshVolumeArtwork๋ GenArtBase๋ฅผ ์์ํ๊ณ 4๊ฐ์ abstract ๋ฉ์๋๋ง ๊ตฌํํฉ๋๋ค. Unity ์ด๋ฒคํธ(OnEnable, OnValidate, Update), ์๋ํฐ ์์ ํจํด(QueueGenerate), GPU ๋๋ก์ฐ ํธ์ถ์ ๋ชจ๋ ์์ ํด๋์ค๊ฐ ์ฒ๋ฆฌํฉ๋๋ค.
OnEnable()
โโโ Generate()
โโโ BuildWorldTriangleCache() ์ผ๊ฐํ ๋ฐ์ดํฐ๋ฅผ ์๋ ๊ณต๊ฐ์ผ๋ก ๋ณํยท์บ์ฑ
โโโ CalcWorldBounds() ์๋ ๋ฐ์ด๋ฉ ๋ฐ์ค ๊ณ์ฐ
โโโ Rejection Sampling ๋ฃจํ ๋ด๋ถ ํ์ ํต๊ณผํ ์ ๋ง _instances์ ์ถ๊ฐ
โ โโโ IsInsideWorld()
โ โโโ RayIntersectsTriangle() (์ผ๊ฐํ ์๋งํผ ๋ฐ๋ณต)
โโโ Animate(0f) ์ด๊ธฐ ์ ์ง ์ํ ๊ณ์ฐ
Update() โ ๋งค ํ๋ ์
โโโ Animate(Time.time) _matrices, _colors ๊ฐฑ์
โโโ GPUInstanceRenderer.Render() DrawMeshInstanced ํธ์ถ
Assets/
โโโ GPUInstancingLit.shader โ ์ ฐ์ด๋
โโโ GenArtResources.cs โ ์ ํธ
โโโ GenArtBase.cs โ ๋ฒ ์ด์ค ํด๋์ค
โโโ GPUInstanceRenderer.cs โ ๋๋ก์ฐ ํด๋์ค
โโโ MeshVolumeArtwork.cs โ ๋ฉ์ ๋ด๋ถ ํํฐํด ์ํธ์ํฌ
using UnityEngine;
using System.Collections.Generic;
// ============================================================
// MeshVolumeArtwork โ GenArtBase ์๋ธํด๋์ค
//
// FBX ๋ฉ์์ ๋ด๋ถ ๋ถํผ๋ฅผ ํํฐํด๋ก ์ฑ์ฐ๊ณ Perlin Noise๋ก ๋ถ์ ์ํต๋๋ค.
//
// [๋ด๋ถ ํ์ ์ ๋ต]
// ์ผ๊ฐํ์ ์๋ ๊ณต๊ฐ์ผ๋ก ๋ณํํด์ ์บ์ฑํฉ๋๋ค.
// FBX Import ์ Unity๊ฐ ์ ์ฉํ๋ ์ค์ผ์ผ/ํ์ ์ด ์๋์ผ๋ก ๋ฐ์๋ฉ๋๋ค.
// ์ํ ํ๋ณด ์ ์ ๋ฏธ์ธ ์งํฐ๋ฅผ ์ถ๊ฐํด ์ฃ์ง ํต๊ณผ ์์น ์ค๋ฅ๋ฅผ ํํผํฉ๋๋ค.
// ============================================================
public class MeshVolumeArtwork : GenArtBase
{
// ============================================================
// ์ธ์คํํฐ ํ๋ผ๋ฏธํฐ
// ============================================================
[Header("๊ตฌ์กฐ ์ค์ (๋ณ๊ฒฝ ์ ์ฌ์์ฑ)")]
public MeshFilter targetMeshFilter;
public int particleCount = 3000;
public int colorSeed = 0;
[Header("ํํฐํด ์ค์ (์ค์๊ฐ ๋ฐ์)")]
[Range(0.005f, 0.3f)] public float particleSize = 0.04f;
[Header("๋ถ์ ์ค์ (์ค์๊ฐ ๋ฐ์)")]
[Tooltip("๋
ธ์ด์ฆ ์๊ฐ ์งํ ์๋")]
[Range(0f, 3f)] public float floatSpeed = 0.4f;
[Tooltip("๋
ธ์ด์ฆ ๊ณต๊ฐ ์ฃผํ์")]
[Range(0.01f, 3f)] public float noiseScale = 0.6f;
[Tooltip("๋๋ฒ์ ๋ถ์ ํฌ๊ธฐ. 0์ด์ด๋ baseJitter๋ก ํญ์ ๋ฏธ์ธํ๊ฒ ์์ง์
๋๋ค.")]
[Range(0f, 5f)] public float floatAmplitude = 0.0f;
[Tooltip("ํญ์ ์ ์ฉ๋๋ ์ต์ ์ง๋ ํฌ๊ธฐ")]
[Range(0.001f, 0.05f)] public float baseJitter = 0.008f;
[Header("์์ ์ค์ (์ค์๊ฐ ๋ฐ์)")]
[Range(0f, 1f)] public float hueBase = 0.55f;
[Range(0f, 1f)] public float hueRange = 0.30f;
[Range(0f, 1f)] public float saturation = 0.75f;
[Range(0f, 1f)] public float brightness = 0.90f;
// ============================================================
// ์ธ์คํด์ค ๋ฐ์ดํฐ
// ============================================================
struct InstanceData
{
public Vector3 worldBasePos; // ์๋ ๊ณต๊ฐ ๊ธฐ์ค ์์น
public float hue;
public float phase;
public float jitterAmp;
}
readonly List<InstanceData> _instances = new List<InstanceData>();
// ์๋ ๊ณต๊ฐ ์ผ๊ฐํ ์บ์
Vector3[] _triA;
Vector3[] _triB;
Vector3[] _triC;
// ์ํ๋ง์ฉ ์๋ ๋ฐ์ด๋ฉ ๋ฐ์ค
Bounds _worldBounds;
// ============================================================
// ๊ตฌ์กฐ ํ๋ผ๋ฏธํฐ ์บ์
// ============================================================
MeshFilter _cachedMeshFilter;
int _cachedParticleCount;
int _cachedColorSeed;
// ============================================================
// Generate
// ============================================================
protected override void Generate()
{
_instances.Clear();
_generated = false;
if (targetMeshFilter == null || targetMeshFilter.sharedMesh == null)
{
Debug.LogWarning("[MeshVolumeArtwork] Target Mesh Filter๊ฐ ํ ๋น๋์ง ์์์ต๋๋ค.");
return;
}
Mesh mesh = targetMeshFilter.sharedMesh;
Transform meshTransform = targetMeshFilter.transform;
// ์ผ๊ฐํ์ ์๋ ๊ณต๊ฐ์ผ๋ก ๋ณํํด์ ์บ์ฑํฉ๋๋ค.
// localToWorldMatrix ํ ๋ฒ์ผ๋ก ์ค์ผ์ผยทํ์ ยท์์น๊ฐ ๋ชจ๋ ๋ฐ์๋ฉ๋๋ค.
BuildWorldTriangleCache(mesh, meshTransform);
// ์๋ ๊ณต๊ฐ ๋ฐ์ด๋ฉ ๋ฐ์ค๋ฅผ ๊ณ์ฐํฉ๋๋ค.
_worldBounds = CalcWorldBounds(mesh.bounds, meshTransform);
Random.InitState(colorSeed);
int maxAttempts = particleCount * 30;
int attempts = 0;
while (_instances.Count < particleCount && attempts < maxAttempts)
{
attempts++;
// ์๋ ๊ณต๊ฐ์์ ๋๋ค ํ๋ณด์ ์ ์์ฑํฉ๋๋ค.
Vector3 candidate = new Vector3(
Random.Range(_worldBounds.min.x, _worldBounds.max.x),
Random.Range(_worldBounds.min.y, _worldBounds.max.y),
Random.Range(_worldBounds.min.z, _worldBounds.max.z)
);
// ์ฃ์ง ํต๊ณผ ์์น ์ค๋ฅ ๋ฐฉ์ง: ํ๋ณด์ ์ ๋ฏธ์ธ ์งํฐ๋ฅผ ์ถ๊ฐํฉ๋๋ค.
// ๋ฐฉํฅ์ด ์๋๋ผ ์ ์์ฒด๋ฅผ ํ๋ค์ด์ผ ์ฃ์ง ์ผ์ด์ค๋ฅผ ์์ ํ๊ฒ ํํผํฉ๋๋ค.
Vector3 jittered = candidate + new Vector3(
(Random.value - 0.5f) * 0.0001f,
(Random.value - 0.5f) * 0.0001f,
(Random.value - 0.5f) * 0.0001f
);
if (IsInsideWorld(jittered))
{
_instances.Add(new InstanceData
{
worldBasePos = candidate, // ์๋ ์ขํ ์ ์ฅ (์งํฐ ์๋ ๊ฐ)
hue = (hueBase + Random.value * hueRange) % 1f,
phase = Random.Range(0f, 100f),
jitterAmp = Random.Range(0.7f, 1.3f)
});
}
}
if (_instances.Count == 0)
{
Debug.LogError("[MeshVolumeArtwork] ํํฐํด์ ๋ฐฐ์นํ์ง ๋ชปํ์ต๋๋ค. " +
"๋ฉ์๊ฐ ๋ซํ ํํ(Closed Mesh)์ธ์ง ํ์ธํ์ธ์.");
return;
}
if (_instances.Count < particleCount)
Debug.LogWarning($"[MeshVolumeArtwork] ๋ชฉํ {particleCount}๊ฐ ์ค {_instances.Count}๊ฐ ๋ฐฐ์น. " +
$"(์๋: {attempts}ํ)");
else
Debug.Log($"[MeshVolumeArtwork] {_instances.Count}๊ฐ ๋ฐฐ์น ์๋ฃ. (์๋: {attempts}ํ)");
int count = _instances.Count;
_matrices = new Matrix4x4[count];
_colors = new Vector4[count];
_generated = true;
Animate(0f);
}
// ============================================================
// ์ผ๊ฐํ ์บ์ โ ์๋ ๊ณต๊ฐ์ผ๋ก ๋ณํํด์ ์ ์ฅํฉ๋๋ค.
// FBX์ localScale์ด (0.01, 0.01, 0.01)์ด์ด๋ ์ ํํ๊ฒ ๋ฐ์๋ฉ๋๋ค.
// ============================================================
void BuildWorldTriangleCache(Mesh mesh, Transform t)
{
Vector3[] verts = mesh.vertices;
int[] tris = mesh.triangles;
int triCount = tris.Length / 3;
Matrix4x4 l2w = t.localToWorldMatrix;
_triA = new Vector3[triCount];
_triB = new Vector3[triCount];
_triC = new Vector3[triCount];
for (int i = 0; i < triCount; i++)
{
_triA[i] = l2w.MultiplyPoint3x4(verts[tris[i * 3 + 0]]);
_triB[i] = l2w.MultiplyPoint3x4(verts[tris[i * 3 + 1]]);
_triC[i] = l2w.MultiplyPoint3x4(verts[tris[i * 3 + 2]]);
}
}
// ============================================================
// ๋ฉ์ ๋ด๋ถ ํ์ โ ์๋ ๊ณต๊ฐ ๋จ์ผ ๋ฐฉํฅ ๋ ์ด์บ์คํธ
//
// +Y ๋ฐฉํฅ์ผ๋ก ๋ฐ์ฌํ ๊ด์ ์ ๊ต์ฐจ ํ์๊ฐ ํ์์ด๋ฉด ๋ด๋ถ์
๋๋ค.
// ์ผ๊ฐํ์ด ์๋ ๊ณต๊ฐ์ผ๋ก ๋ณํ๋์ด ์์ผ๋ฏ๋ก ์ค์ผ์ผ ๋ถ์ผ์น ์ค๋ฅ๊ฐ ์์ต๋๋ค.
// ============================================================
bool IsInsideWorld(Vector3 worldPoint)
{
int hits = 0;
for (int i = 0; i < _triA.Length; i++)
{
if (RayIntersectsTriangle(worldPoint, Vector3.up, _triA[i], _triB[i], _triC[i]))
hits++;
}
return hits % 2 == 1;
}
// MรถllerโTrumbore ๊ด์ -์ผ๊ฐํ ๊ต์ฐจ ์๊ณ ๋ฆฌ์ฆ
static bool RayIntersectsTriangle(Vector3 origin, Vector3 direction,
Vector3 a, Vector3 b, Vector3 c)
{
const float EPSILON = 1e-6f;
Vector3 edge1 = b - a;
Vector3 edge2 = c - a;
Vector3 h = Vector3.Cross(direction, edge2);
float det = Vector3.Dot(edge1, h);
if (det > -EPSILON && det < EPSILON) return false;
float invDet = 1f / det;
Vector3 s = origin - a;
float u = invDet * Vector3.Dot(s, h);
if (u < 0f || u > 1f) return false;
Vector3 q = Vector3.Cross(s, edge1);
float v = invDet * Vector3.Dot(direction, q);
if (v < 0f || u + v > 1f) return false;
float t = invDet * Vector3.Dot(edge2, q);
return t > EPSILON;
}
// ============================================================
// ์๋ ๋ฐ์ด๋ฉ ๋ฐ์ค ๊ณ์ฐ
// ============================================================
static Bounds CalcWorldBounds(Bounds localBounds, Transform t)
{
Vector3 c = localBounds.center;
Vector3 e = localBounds.extents;
Vector3[] corners = new Vector3[8]
{
t.TransformPoint(c + new Vector3(-e.x, -e.y, -e.z)),
t.TransformPoint(c + new Vector3( e.x, -e.y, -e.z)),
t.TransformPoint(c + new Vector3(-e.x, e.y, -e.z)),
t.TransformPoint(c + new Vector3( e.x, e.y, -e.z)),
t.TransformPoint(c + new Vector3(-e.x, -e.y, e.z)),
t.TransformPoint(c + new Vector3( e.x, -e.y, e.z)),
t.TransformPoint(c + new Vector3(-e.x, e.y, e.z)),
t.TransformPoint(c + new Vector3( e.x, e.y, e.z)),
};
Bounds wb = new Bounds(corners[0], Vector3.zero);
for (int i = 1; i < 8; i++) wb.Encapsulate(corners[i]);
return wb;
}
// ============================================================
// Animate โ ์ด์ค ๋ ์ด์ด ๋ถ์ (์๋ ๊ณต๊ฐ)
//
// Layer 1 (baseJitter): ํญ์ ๋์ํ๋ ๋ฏธ์ธ ์ง๋
// Layer 2 (floatAmplitude): ์ถ๊ฐ ๋๋ฒ์ ์ด๋
// ============================================================
protected override void Animate(float t)
{
if (!_generated) return;
float animT = t * floatSpeed;
for (int i = 0; i < _instances.Count; i++)
{
InstanceData inst = _instances[i];
// Layer 1 โ ๋ฏธ์ธ ์ง๋ (floatAmplitude = 0์ผ ๋ ์ฃผ ์์ง์ ๋ด๋น)
float jitterScale = baseJitter * inst.jitterAmp
* (floatAmplitude < 0.001f ? 2f : 1f);
float jx = SampleNoise(inst.worldBasePos.x * 2.3f, inst.worldBasePos.z * 2.1f,
inst.phase, animT * 1.7f) * jitterScale;
float jy = SampleNoise(inst.worldBasePos.y * 2.1f, inst.worldBasePos.x * 2.4f,
inst.phase + 31.4f, animT * 1.7f) * jitterScale;
float jz = SampleNoise(inst.worldBasePos.z * 2.4f, inst.worldBasePos.y * 2.2f,
inst.phase + 72.8f, animT * 1.7f) * jitterScale;
// Layer 2 โ ๋๋ฒ์ ๋ถ์
float nx = SampleNoise(inst.worldBasePos.x, inst.worldBasePos.z,
inst.phase, animT) * floatAmplitude;
float ny = SampleNoise(inst.worldBasePos.y, inst.worldBasePos.x,
inst.phase + 31.4f, animT) * floatAmplitude;
float nz = SampleNoise(inst.worldBasePos.z, inst.worldBasePos.y,
inst.phase + 72.8f, animT) * floatAmplitude;
Vector3 animPos = SoftClampWorld(
inst.worldBasePos + new Vector3(jx + nx, jy + ny, jz + nz),
inst.worldBasePos
);
_matrices[i] = Matrix4x4.TRS(animPos, Quaternion.identity, Vector3.one * particleSize);
Color col = Color.HSVToRGB(inst.hue, saturation, brightness);
_colors[i] = new Vector4(col.r, col.g, col.b, 1f);
}
}
float SampleNoise(float a, float b, float phase, float t)
{
return (Mathf.PerlinNoise(a * noiseScale + t + phase, b * noiseScale + phase) - 0.5f) * 2f;
}
// ์๋ ๋ฐ์ด๋ฉ ๋ฐ์ค๋ฅผ ๋ฒ์ด๋๋ฉด basePos ๋ฐฉํฅ์ผ๋ก ๋ณต์ํฉ๋๋ค.
Vector3 SoftClampWorld(Vector3 pos, Vector3 basePos)
{
if (_worldBounds.Contains(pos)) return pos;
return Vector3.Lerp(pos, basePos, 0.7f);
}
// ============================================================
// ๊ตฌ์กฐ ํ๋ผ๋ฏธํฐ ์บ์
// ============================================================
protected override bool StructureParamsChanged()
{
return targetMeshFilter != _cachedMeshFilter
|| particleCount != _cachedParticleCount
|| colorSeed != _cachedColorSeed;
}
protected override void CacheStructureParams()
{
_cachedMeshFilter = targetMeshFilter;
_cachedParticleCount = particleCount;
_cachedColorSeed = colorSeed;
}
}
using UnityEngine;
// ============================================================
// GPU Instancing ๋๋ก์ฐ ์ฝ ์ ๋ด ํด๋์ค
// ============================================================
public class GPUInstanceRenderer
{
const int BATCH_SIZE = 1023;
readonly Mesh _mesh;
readonly Material _material;
public GPUInstanceRenderer(Mesh mesh, Material material)
{
_mesh = mesh;
_material = material;
}
// matrices / colors ๋ฐฐ์ด์ 1023๊ฐ ๋จ์ ๋ฐฐ์น๋ก ๋๋ GPU์ ์ ์กํฉ๋๋ค.
// count๋ ๋ฐฐ์ด์ ์ค์ ์ฌ์ฉ ๊ธธ์ด์
๋๋ค.
public void Render(Matrix4x4[] matrices, Vector4[] colors, int count)
{
if (_mesh == null || _material == null || count == 0) return;
int index = 0;
int remaining = count;
while (remaining > 0)
{
int batch = Mathf.Min(BATCH_SIZE, remaining);
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);
// ๋ฐฐ์น๋ง๋ค new๋ก ์์ฑํด์ผ ์ด์ ๋ฐฐ์น ๋ฐ์ดํฐ์ ๊ฐ์ญ์ด ์์ต๋๋ค.
MaterialPropertyBlock block = new MaterialPropertyBlock();
block.SetVectorArray("_BaseColor", batchColors);
Graphics.DrawMeshInstanced(
_mesh, 0, _material,
batchMatrices, batch, block
);
index += batch;
remaining -= batch;
}
}
}
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif
// ============================================================
// ์์ฑ ์ํธ ๋ฒ ์ด์ค ํด๋์ค
// Unity ๋ผ์ดํ์ฌ์ดํด๊ณผ ์๋ํฐ ์์ ํจํด์ ์ ๋ดํฉ๋๋ค.
//
// ์๋ธํด๋์ค ๊ตฌํ ์๊ตฌ (abstract):
// Generate() โ ๋ฐฐ์น ๊ณต์
// Animate(float t) โ ์ ๋๋ฉ์ด์
๊ณต์
// StructureParamsChanged() โ ๊ตฌ์กฐ ํ๋ผ๋ฏธํฐ ๋ณ๊ฒฝ ๊ฐ์ง
// CacheStructureParams() โ ๊ตฌ์กฐ ํ๋ผ๋ฏธํฐ ์บ์ ์ ์ฅ
// ============================================================
[ExecuteAlways]
public abstract class GenArtBase : MonoBehaviour
{
[Header("๋ ๋ ๋ฆฌ์์ค (๋น์๋๋ฉด ์๋ ์์ฑ)")]
public Mesh instanceMesh;
public Material instanceMaterial;
// ์๋ธํด๋์ค์์ Animate()๊ฐ ์ฑ์ฐ๊ณ ๋ ๋๋ฌ๊ฐ ์ฝ๋ ๋ฐฐ์ด
protected Matrix4x4[] _matrices;
protected Vector4[] _colors;
// false์ด๋ฉด Update์์ ๋ ๋๋ง์ ๊ฑด๋๋๋๋ค.
protected bool _generated;
GPUInstanceRenderer _renderer;
#if UNITY_EDITOR
bool _generateQueued;
#endif
// ============================================================
// Unity ์ด๋ฒคํธ
// ============================================================
void OnEnable()
{
EnsureResources();
Generate();
CacheStructureParams();
}
void OnValidate()
{
#if UNITY_EDITOR
EnsureResources();
if (StructureParamsChanged())
{
CacheStructureParams();
QueueGenerate();
}
else
{
// ์๊ฐ ํ๋ผ๋ฏธํฐ๋ง ๋ณ๊ฒฝ โ ์ฌ์์ฑ ์์ด ์ฆ์ ๊ฐฑ์
if (_generated) Animate(0f);
}
#endif
}
void Update()
{
if (!_generated || _renderer == null) return;
if (!Application.isPlaying)
{
Animate(0f);
_renderer.Render(_matrices, _colors, _matrices.Length);
return;
}
Animate(Time.time);
_renderer.Render(_matrices, _colors, _matrices.Length);
}
// ============================================================
// ๋ฆฌ์์ค ์ด๊ธฐํ
// ============================================================
void EnsureResources()
{
if (instanceMesh == null) instanceMesh = GenArtResources.CreateDefaultMesh();
if (instanceMaterial == null) instanceMaterial = GenArtResources.CreateDefaultMaterial();
// ์ธ์คํํฐ์์ mesh/material์ด ๊ต์ฒด๋ ๊ฒฝ์ฐ๋ฅผ ์ํด ํญ์ ์ฌ๊ตฌ์ฑ
_renderer = new GPUInstanceRenderer(instanceMesh, instanceMaterial);
}
// ============================================================
// ์๋ํฐ ์์ Generate ์์ฝ
// ============================================================
#if UNITY_EDITOR
void QueueGenerate()
{
if (_generateQueued) return;
_generateQueued = true;
EditorApplication.delayCall += () =>
{
_generateQueued = false;
if (this == null) return;
Generate();
};
}
#endif
// ============================================================
// ์๋ธํด๋์ค ๊ตฌํ ์๊ตฌ
// ============================================================
protected abstract void Generate();
protected abstract void Animate(float t);
protected abstract bool StructureParamsChanged();
protected abstract void CacheStructureParams();
}
using UnityEngine;
using UnityEngine.Rendering;
// ============================================================
// ๋ฉ์ยท๋จธํฐ๋ฆฌ์ผ ์๋ ์์ฑ ์ ํธ๋ฆฌํฐ
// ์ธ์คํํฐ ์ฌ๋กฏ์ด ๋น์ด์์ ๋ GenArtBase.OnEnable์์ ํธ์ถ๋ฉ๋๋ค.
// ============================================================
public static class GenArtResources
{
// ์์ Primitive์์ ํ๋ธ ๋ฉ์๋ฅผ ์ถ์ถ ํ ์ฆ์ ์ญ์ ํฉ๋๋ค.
// ์ฌ ๊ณ์ธต(Hierarchy)์ ์๋ฅ ์ค๋ธ์ ํธ๋ฅผ ๋จ๊ธฐ์ง ์์ต๋๋ค.
public static Mesh CreateDefaultMesh()
{
GameObject temp = GameObject.CreatePrimitive(PrimitiveType.Cube);
Mesh mesh = temp.GetComponent<MeshFilter>().sharedMesh;
Object.DestroyImmediate(temp);
return mesh;
}
public static Material CreateDefaultMaterial()
{
Shader shader = ResolveShader();
if (shader == null)
{
Debug.LogError("[GenArtResources] ์ ํจํ ์
ฐ์ด๋๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค. " +
"instanceMaterial์ ์ธ์คํํฐ์์ ์ง์ ํ ๋นํ์ธ์.");
return null;
}
Debug.Log($"[GenArtResources] ์ฌ์ฉ ์
ฐ์ด๋: {shader.name}");
Material mat = new Material(shader);
mat.enableInstancing = true;
return mat;
}
// ============================================================
// ์
ฐ์ด๋ ํด๊ฒฐ ์์
// 1์์: Custom/GPUInstancingLit (UNITY_INSTANCING_BUFFER ๊ธฐ๋ฐ)
// 2์์: Shader.Find() ํด๋ฐฑ
// ============================================================
static Shader ResolveShader()
{
// 1์์: ์ปค์คํ
์ธ์คํด์ฑ ์
ฐ์ด๋
// SRP Batcher๊ฐ ํ์ฑํ๋ URP์์ per-instance ์์์ ์ ํํ ์ ๋ฌํฉ๋๋ค.
Shader custom = Shader.Find("Custom/GPUInstancingLit");
if (custom != null) return custom;
// 2์์: ํด๋ฐฑ (์ปค์คํ
์
ฐ์ด๋ ํ์ผ์ด ์๋ ๊ฒฝ์ฐ)
Debug.LogWarning("[GenArtResources] Custom/GPUInstancingLit ์
ฐ์ด๋๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค. " +
"GPUInstancingLit.shader๋ฅผ Assets ํด๋์ ์ถ๊ฐํ์ธ์.");
return Shader.Find("Universal Render Pipeline/Lit")
?? Shader.Find("Standard");
}
}
Shader "Custom/GPUInstancingLit"
{
Properties
{
_BaseColor ("Base Color", Color) = (1,1,1,1)
}
SubShader
{
Tags
{
"RenderType" = "Opaque"
"RenderPipeline" = "UniversalPipeline"
"Queue" = "Geometry"
}
// ----------------------------------------------------------
// ForwardLit Pass
// ----------------------------------------------------------
Pass
{
Name "ForwardLit"
Tags { "LightMode" = "UniversalForward" }
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"
// SRP Batcher๋ฅผ ์ฐํํ๋ per-instance ํ๋กํผํฐ ๋ฒํผ์
๋๋ค.
// MaterialPropertyBlock.SetVectorArray("_BaseColor", ...) ๊ฐ์ด ์ฌ๊ธฐ๋ก ๋ค์ด์ต๋๋ค.
UNITY_INSTANCING_BUFFER_START(Props)
UNITY_DEFINE_INSTANCED_PROP(float4, _BaseColor)
UNITY_INSTANCING_BUFFER_END(Props)
struct Attributes
{
float4 positionOS : POSITION;
float3 normalOS : NORMAL;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct Varyings
{
float4 positionHCS : SV_POSITION;
float3 normalWS : TEXCOORD0;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
Varyings vert(Attributes IN)
{
Varyings OUT;
UNITY_SETUP_INSTANCE_ID(IN);
UNITY_TRANSFER_INSTANCE_ID(IN, OUT);
OUT.positionHCS = TransformObjectToHClip(IN.positionOS.xyz);
OUT.normalWS = TransformObjectToWorldNormal(IN.normalOS);
return OUT;
}
half4 frag(Varyings IN) : SV_Target
{
UNITY_SETUP_INSTANCE_ID(IN);
float4 baseColor = UNITY_ACCESS_INSTANCED_PROP(Props, _BaseColor);
// Lambert diffuse + ambient
Light mainLight = GetMainLight();
float3 normalWS = normalize(IN.normalWS);
float NdotL = saturate(dot(normalWS, mainLight.direction));
float3 diffuse = baseColor.rgb * mainLight.color.rgb * NdotL;
float3 ambient = baseColor.rgb * 0.25;
return half4(ambient + diffuse, baseColor.a);
}
ENDHLSL
}
// ----------------------------------------------------------
// ShadowCaster Pass
// ----------------------------------------------------------
Pass
{
Name "ShadowCaster"
Tags { "LightMode" = "ShadowCaster" }
ZWrite On
ZTest LEqual
ColorMask 0
Cull Back
HLSLPROGRAM
#pragma vertex vertShadow
#pragma fragment fragShadow
#pragma multi_compile_instancing
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
// ShadowCaster๋ ์์ ๋ฐ์ดํฐ๊ฐ ํ์ ์์ง๋ง,
// ์ธ์คํด์ฑ ๋ฒํผ๋ฅผ ์ ์ธํด์ผ UNITY_SETUP_INSTANCE_ID๊ฐ ์ ์ ๋์ํฉ๋๋ค.
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 positionHCS : SV_POSITION;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
Varyings vertShadow(Attributes IN)
{
Varyings OUT;
UNITY_SETUP_INSTANCE_ID(IN);
UNITY_TRANSFER_INSTANCE_ID(IN, OUT);
OUT.positionHCS = TransformObjectToHClip(IN.positionOS.xyz);
return OUT;
}
half4 fragShadow(Varyings IN) : SV_Target
{
return 0;
}
ENDHLSL
}
}
}
๋ฉ์ ๋ด๋ถ์ ํํฐํด์ ๊ท ์ผํ๊ฒ ์ฑ์ฐ๋ ๋ฐฉ๋ฒ์ ๋๋ค.
๋ฐ์ด๋ฉ ๋ฐ์ค ๋๋น ๋ฉ์ ๋ถํผ๊ฐ ์์์๋ก ๋ฒ๋ ค์ง๋ ํ๋ณด์ ์ด ๋ง์์ง๋๋ค. ๊ตฌ ํํ๋ ํจ์จ์ด ๋๊ณ (์ฝ 52%), ๋ฉ์ํ๊ฑฐ๋ ๊ตฌ๋ฉ์ด ๋ง์ ํํ๋ ๋ฎ์ต๋๋ค.
ํ๋ณด์ ์ด ๋ฉ์ ๋ด๋ถ์ธ์ง ํ์ ํ๋ ์๊ณ ๋ฆฌ์ฆ์
๋๋ค.
์์์ ์ P์์ ์์์ ๋ฐฉํฅ์ผ๋ก ๊ด์ ์ ๋ฐ์ฌํด ๋ฉ์ ํ๋ฉด๊ณผ ๊ต์ฐจํ๋ ํ์๋ฅผ ์
๋๋ค.
๊ต์ฐจ ํ์ ํ์ โ ๋ด๋ถ
๊ต์ฐจ ํ์ ์ง์ โ ์ธ๋ถ (๋๋ ๋ฉ์ ํ๋ฉด ์)
์ด ์ฝ๋์์๋ +Y ๋ฐฉํฅ์ผ๋ก ๋ฐ์ฌํ๊ณ ์๋ ๊ณต๊ฐ์ผ๋ก ๋ณํ๋ ์ผ๊ฐํ ๋ฐฐ์ด ์ ์ฒด๋ฅผ ์ํํฉ๋๋ค.
๊ด์ ๊ณผ ์ผ๊ฐํ์ ๊ต์ฐจ ์ฌ๋ถ๋ฅผ ๊ณ์ฐํ๋ ์๊ณ ๋ฆฌ์ฆ์
๋๋ค. ๊ทธ๋ํฝ์ค ๊ต์ฌ์์ ๊ด์ -์ผ๊ฐํ ๊ต์ฐจ์ ํ์ค์ผ๋ก ์ฌ์ฉ๋ฉ๋๋ค.
ํ๋ ฌ ์ญ๋ณํ ์์ด ๋ฒกํฐ ์ธ์ ยท๋ด์ ์ฐ์ฐ๋ง์ผ๋ก ๊ต์ฐจ์ ์ ๋ฌด๊ฒ์ค์ฌ ์ขํ (u, v)์ ๊ด์ ๊ฑฐ๋ฆฌ t๋ฅผ ๋์์ ๊ณ์ฐํฉ๋๋ค.
edge1 = B - A
edge2 = C - A
h = Cross(direction, edge2)
det = Dot(edge1, h) โ 0์ ๊ฐ๊น์ฐ๋ฉด ๊ด์ ์ด ์ผ๊ฐํ๊ณผ ํํ
u = Dot(origin-A, h) / det โ ๋ฌด๊ฒ์ค์ฌ ์ขํ u
v = Dot(direction, Cross(origin-A, edge1)) / det โ ๋ฌด๊ฒ์ค์ฌ ์ขํ v
t = Dot(edge2, Cross(origin-A, edge1)) / det โ ๊ต์ฐจ ๊ฑฐ๋ฆฌ
์กฐ๊ฑด: 0 โค u โค 1, 0 โค v, u+v โค 1, t > 0 โ ๊ต์ฐจ
t > 0 ์กฐ๊ฑด์ผ๋ก ๊ด์ ๋ค์ชฝ(๋ฐ๋ ๋ฐฉํฅ)์ ๊ต์ฐจ๋ฅผ ์ ์ธํฉ๋๋ค.
์ผ๊ฐํ์ ๋ก์ปฌ ๊ณต๊ฐ์ด ์๋ ์๋ ๊ณต๊ฐ์ผ๋ก ๋ณํํด์ ์บ์ฑํฉ๋๋ค.
Matrix4x4 l2w = meshTransform.localToWorldMatrix;
_triA[i] = l2w.MultiplyPoint3x4(verts[tris[i * 3 + 0]]);
FBX Import ์ Unity๊ฐ ์๋์ผ๋ก ์ ์ฉํ๋ ์ค์ผ์ผ(localScale = (0.01, 0.01, 0.01) ๋ฑ)์ด ํ์ ๊ณผ์ ์ ๊ทธ๋๋ก ๋ฐ์๋ฉ๋๋ค. ๋ก์ปฌ ๊ณต๊ฐ์ผ๋ก ํ์ ํ๋ฉด ์ค์ผ์ผ ๋ถ์ผ์น๋ก ๋ด๋ถ/์ธ๋ถ ํ์ ์ด ์ ๋ฉด ์ค๋ฅ๊ฐ ๋ฉ๋๋ค.
ํ๋ณด์ ์ด ์ผ๊ฐํ ์ฃ์ง๋ฅผ ์ ํํ ํต๊ณผํ๋ฉด ๊ต์ฐจ ํ์๊ฐ 0 ๋๋ 2๋ก ๊ณ์ฐ๋์ด ์ธ๋ถ๋ก ์คํ๋ฉ๋๋ค. ์ด๋ฅผ ๋ฐฉ์งํ๊ธฐ ์ํด ํ์ ์ง์ ํ๋ณด์ ์ 0.0001 ์์ค์ ๋ฏธ์ธ ๋๋ค ์คํ์ ์ ์ถ๊ฐํฉ๋๋ค.
Vector3 jittered = candidate + new Vector3(
(Random.value - 0.5f) * 0.0001f,
(Random.value - 0.5f) * 0.0001f,
(Random.value - 0.5f) * 0.0001f
);
์ ์ฅ๋๋worldBasePos๋ ์งํฐ๊ฐ ์๋ ์๋ ์ขํ๋ฅผ ์ฌ์ฉํฉ๋๋ค.
Unity์์ GameObject ์์ด ๋ฉ์๋ฅผ ๋ ๋๋งํ๋ API์ ๋๋ค. ์ฌ ๊ณ์ธต(Hierarchy)์ ์ค๋ธ์ ํธ๋ฅผ ๋ฑ๋กํ์ง ์๊ณ ์์นยท์์ ๋ฐฐ์ด๋ง GPU์ ์ ๋ฌํฉ๋๋ค.
Graphics.DrawMeshInstanced(
mesh, // ๋ ๋๋งํ ๋ฉ์
0, // ์๋ธ๋ฉ์ ์ธ๋ฑ์ค
material, // ๋จธํฐ๋ฆฌ์ผ
batchMatrices, // ์ธ์คํด์ค๋ณ TRS ํ๋ ฌ ๋ฐฐ์ด
batch, // ์ด๋ฒ ๋ฐฐ์น์ ์ธ์คํด์ค ์
block // ์ธ์คํด์ค๋ณ ์์ (MaterialPropertyBlock)
);
ํ ๋ฒ ํธ์ถ์ ์ต๋ 1023๊ฐ๊น์ง๋ง ์ฒ๋ฆฌํ๋ฏ๋ก GPUInstanceRenderer๊ฐ while ๋ฃจํ๋ก ๋ฐฐ์น๋ฅผ ๋ถํ ํฉ๋๋ค.
URP์ SRP Batcher๋ GPU Instancing๊ณผ ๋์์ ๋์ํ์ง ์์ต๋๋ค. MaterialPropertyBlock์ผ๋ก ์ฃผ์ ํ๋ ์์ ๋ฐ์ดํฐ๋ฅผ SRP Batcher๊ฐ ๋ฎ์ด์ฐ๋ ๋ฌธ์ ๊ฐ ์์ต๋๋ค.
์ปค์คํ
์
ฐ์ด๋์์ UNITY_INSTANCING_BUFFER_START/END ๋ธ๋ก์ผ๋ก ์์ ํ๋กํผํฐ๋ฅผ ์ ์ธํ๋ฉด SRP Batcher ๋์ GPU Instancing ์ ์ฉ ๋ฒํผ์์ ๋ฐ์ดํฐ๋ฅผ ์ฝ์ต๋๋ค.
UNITY_INSTANCING_BUFFER_START(Props)
UNITY_DEFINE_INSTANCED_PROP(float4, _BaseColor)
UNITY_INSTANCING_BUFFER_END(Props)
// ์
ฐ์ด๋ ๋ด๋ถ์์ ์ธ์คํด์ค๋ณ ์์์ ์ฝ์ต๋๋ค
float4 baseColor = UNITY_ACCESS_INSTANCED_PROP(Props, _BaseColor);
๊ฐ ํํฐํด์ ์์นยทํ์ ยทํฌ๊ธฐ๋ฅผ ํ๋์ ํ๋ ฌ๋ก ํํํฉ๋๋ค.
_matrices[i] = Matrix4x4.TRS(
animPos, // Translation: ์๋ ์์น
Quaternion.identity, // Rotation: ํ์ ์์
Vector3.one * particleSize // Scale: ๊ท ์ผ ํฌ๊ธฐ
);
GPU๋ ์ด ํ๋ ฌ ํ๋๋ก ํด๋น ์ธ์คํด์ค์ ๋ชจ๋ ๊ณต๊ฐ ๋ณํ์ ์ฒ๋ฆฌํฉ๋๋ค.
| ๋ ์ด์ด | ํ๋ผ๋ฏธํฐ | ์ญํ |
|---|---|---|
| Layer 1 | baseJitter | ํญ์ ๋์ํ๋ ๋ฏธ์ธ ์ง๋. floatAmplitude = 0์ด์ด๋ ํํฐํด์ ์ด์์๋ ๋๋์ผ๋ก ์์ง์
๋๋ค. |
| Layer 2 | floatAmplitude | ์ถ๊ฐ์ ์ธ ๋๋ฒ์ ์ด๋. 0์ด๋ฉด ๋นํ์ฑ. |
floatAmplitude = 0์ด๋ฉด Layer 1์ ์ค์ผ์ผ์ 2๋ฐฐ๋ก ํค์ ์ฃผ ์์ง์์ ๋ด๋นํ๊ฒ ํฉ๋๋ค.
Unity์ Mathf.PerlinNoise()๋ 2D์
๋๋ค. ์ธ ์ถ ๊ฐ๊ฐ ๋
๋ฆฝ์ ์ธ ์์ง์์ ๋ง๋ค๊ธฐ ์ํด ์ถ๋ง๋ค ๋ค๋ฅธ UV ์กฐํฉ๊ณผ ํฐ ์์ ์คํ์
์ ์ฌ์ฉํฉ๋๋ค.
float nx = SampleNoise(pos.x, pos.z, phase, t); // X์ถ ์ด๋
float ny = SampleNoise(pos.y, pos.x, phase + 31.4f, t); // Y์ถ ์ด๋
float nz = SampleNoise(pos.z, pos.y, phase + 72.8f, t); // Z์ถ ์ด๋
phase๋ ํํฐํด๋ง๋ค ๋ค๋ฅธ ๋๋ค ๊ฐ์ด๋ฏ๋ก ๋ชจ๋ ํํฐํด์ด ๋์ผํ๊ฒ ์์ง์ด๋ ๋์กฐ ํ์์ด ์์ต๋๋ค.
SampleNoise()๋ 0~1 ๋ฒ์์ธ Perlin ์ถ๋ ฅ์ -1~+1๋ก ์ ๊ทํํฉ๋๋ค.
float SampleNoise(float a, float b, float phase, float t)
{
return (Mathf.PerlinNoise(a * noiseScale + t + phase, b * noiseScale + phase) - 0.5f) * 2f;
}
ํํฐํด์ด ๋ถ์ ์ค ๋ฐ์ด๋ฉ ๋ฐ์ค ๋ฐ์ผ๋ก ๋๊ฐ๋ฉด basePos ๋ฐฉํฅ์ผ๋ก 70% ๋ณต์ํฉ๋๋ค. ์์ ํ ํด๋จํํ๋ฉด ๊ฒฝ๊ณ์์ ํํฐํด์ด ๋ฑ๋ฑํ๊ฒ ๋ฉ์ถ๋ ๋๋์ด ๋๋ฏ๋ก Lerp๋ก ๋ถ๋๋ฝ๊ฒ ๋์ด๋น๊น๋๋ค.
| ํ๋ผ๋ฏธํฐ | ๋ถ๋ฅ | ์ค๋ช |
|---|---|---|
targetMeshFilter | ๊ตฌ์กฐ | ํํฐํด์ ์ฑ์ธ FBX ์ค๋ธ์ ํธ์ MeshFilter |
particleCount | ๊ตฌ์กฐ | ๋ฐฐ์นํ ํํฐํด ์ด ์ |
colorSeed | ๊ตฌ์กฐ | ์์ ๋ฌด์์ ์๋ |
particleSize | ์๊ฐ | ํํฐํด ํ๋์ ํฌ๊ธฐ |
floatSpeed | ์๊ฐ | ๋ ธ์ด์ฆ ์๊ฐ ์งํ ์๋ |
noiseScale | ์๊ฐ | ๋ ธ์ด์ฆ ๊ณต๊ฐ ์ฃผํ์ (ํด์๋ก ํํฐํด ๊ฐ ์์ง์์ด ์ ๊ฐ๊ฐ) |
floatAmplitude | ์๊ฐ | ๋๋ฒ์ ๋ถ์ ํฌ๊ธฐ. 0์ด์ด๋ ๊ธฐ๋ณธ ์ง๋์ ๋์ํจ |
baseJitter | ์๊ฐ | ํญ์ ์ ์ฉ๋๋ ์ต์ ์ง๋ ํฌ๊ธฐ |
hueBase | ์๊ฐ | ์์ Hue ์์๊ฐ |
hueRange | ์๊ฐ | ํํฐํด ๊ฐ Hue ๋ถํฌ ๋ฒ์ |
saturation | ์๊ฐ | ์ฑ๋ |
brightness | ์๊ฐ | ๋ช ๋ |
๊ตฌ์กฐ ํ๋ผ๋ฏธํฐ๊ฐ ๋ณ๊ฒฝ๋๋ฉด Generate()๊ฐ ์ฌ์คํ๋ฉ๋๋ค. ์๊ฐ ํ๋ผ๋ฏธํฐ๋ Animate(0f)๋ง ์ฌ์คํ๋์ด ์ฆ์ ๋ฐ์๋ฉ๋๋ค.
Generate() ์๊ฐ์ด ๊ธธ์ด์ง๋๋ค. ์ค์๊ฐ ๋ ๋๋ง ์๋์๋ ์ํฅ์ด ์์ต๋๋ค.Apply Transforms๋ฅผ ์ฒดํฌํ๋ฉด ์ค์ผ์ผ ๋ฌธ์ ๋ฅผ ๋ฐฉ์งํ ์ ์์ต๋๋ค.