

λ€μν λ°°μ΄ λͺ¨λλ₯Ό μ μ©ν μ μλ 보μΌλ¬νλ μ΄νΈ
using UnityEngine;
using System.Collections.Generic;
#if UNITY_EDITOR
using UnityEditor;
#endif
[ExecuteAlways]
public class GenArtArrangements : MonoBehaviour
{
// ============================================================
// λ°°μ΄ λͺ¨λ
// ============================================================
public enum ArrangementMode
{
Grid, // μ§κ΅ 격μ
HexGrid, // μ‘κ° κ²©μ
SineWave, // XμΆ μ¬μΈν λ³μ
NoiseField, // Perlin noise 3D λ³μ
Circle, // λμ¬μ
Spiral, // μλ₯΄ν€λ©λ°μ€ λμ
Phyllotaxis, // νΌλ³΄λμΉ/ν΄λ°λΌκΈ° λ°°μ΄
Lissajous, // 리μ¬μ£Ό 곑μ
}
// ============================================================
// μΈμ€νν° νλΌλ―Έν°
// ============================================================
[Header("λ°°μ΄ λͺ¨λ")]
public ArrangementMode mode = ArrangementMode.Grid;
// ----- ꡬ쑰 νλΌλ―Έν° (λ³κ²½ μ μ¬μμ±) -------------------------
[Header("ꡬ쑰 μ€μ (λ³κ²½ μ μ€λΈμ νΈ μ¬μμ±)")]
public int countX = 20;
public int countY = 20;
public float spacing = 1.0f;
public float objectSize = 0.3f;
[Header("Noise Field μ€μ ")]
public float noiseScale = 0.25f;
public float noiseAmplitude = 2.5f;
[Header("Sine Wave μ€μ ")]
public float sineFrequency = 1.0f;
public float sineAmplitude = 2.0f;
[Header("Phyllotaxis μ€μ ")]
[Tooltip("ν©κΈκ° = 137.5077Β°. μ΄μ§ λ°κΎΈλ©΄ μ ν λ€λ₯Έ ν¨ν΄μ΄ λμ΅λλ€")]
public float phyllotaxisAngle = 137.5077f;
public float phyllotaxisRadius = 0.4f;
[Header("Lissajous μ€μ ")]
public float lissA = 3f;
public float lissB = 2f;
public float lissDelta = 0f; // μμμ°¨ (λΌλμ)
public float lissScale = 8f;
// ----- μ λλ©μ΄μ
νλΌλ―Έν° -----------------------------------
[Header("μ λλ©μ΄μ
μ€μ (μ€μκ° λ°μ)")]
public float animSpeed = 1.0f;
public float waveHeight = 1.5f;
public float waveFrequency = 0.6f;
// ----- μ¬μ§ μ€μ --------------------------------------------
[Header("μ¬μ§ μ€μ ")]
public Mesh instanceMesh;
public Material instanceMaterial;
// ============================================================
// λ΄λΆ λ°μ΄ν°
// ============================================================
struct InstanceData
{
public Vector3 basePos;
public float noiseOffset; // μΈμ€ν΄μ€λ³ μμ (noise/wave λ³μ£Όμ©)
}
List<InstanceData> _instances = new List<InstanceData>();
Matrix4x4[] _matrices;
int _totalCount;
const int BATCH_SIZE = 1023;
bool _generated = false;
enum RenderPipeline { BuiltIn, URP, HDRP }
RenderPipeline _pipeline;
// ꡬ쑰 νλΌλ―Έν° μΊμ
ArrangementMode _cachedMode;
int _cachedCountX, _cachedCountY;
float _cachedSpacing, _cachedObjectSize;
float _cachedNoiseScale, _cachedNoiseAmplitude;
float _cachedSineFrequency, _cachedSineAmplitude;
float _cachedPhylAngle, _cachedPhylRadius;
float _cachedLissA, _cachedLissB, _cachedLissDelta, _cachedLissScale;
#if UNITY_EDITOR
bool _generateQueued = false;
#endif
// ============================================================
// Unity μ΄λ²€νΈ
// ============================================================
void OnEnable()
{
_pipeline = DetectPipeline();
EnsureResources();
Generate();
CacheStructureParams();
}
void OnValidate()
{
#if UNITY_EDITOR
_pipeline = DetectPipeline();
EnsureResources();
if (StructureParamsChanged())
{
CacheStructureParams();
QueueGenerate();
}
else
{
Animate(0f);
}
#endif
}
#if UNITY_EDITOR
void QueueGenerate()
{
if (_generateQueued) return;
_generateQueued = true;
EditorApplication.delayCall += () =>
{
_generateQueued = false;
if (this == null) return;
Generate();
};
}
#endif
void Update()
{
if (!_generated) return;
if (!Application.isPlaying)
{
Animate(0f);
RenderInstances();
return;
}
Animate(Time.time * animSpeed);
RenderInstances();
}
// ============================================================
// [μνΈμν¬ λ‘μ§] Generate β λ°°μ΄ κ³΅μ
// ============================================================
void Generate()
{
_instances.Clear();
_totalCount = countX * countY;
for (int y = 0; y < countY; y++)
for (int x = 0; x < countX; x++)
{
int idx = y * countX + x;
float fx = x - (countX - 1) * 0.5f; // μ€μ¬ κΈ°μ€ μ κ·ν
float fy = y - (countY - 1) * 0.5f;
Vector3 pos = Vector3.zero;
switch (mode)
{
// ββ μ§κ΅ 격μ βββββββββββββββββββββββββββββββββββββββββ
// κ°μ₯ κΈ°λ³Έ. spacingμΌλ‘ XZ νλ©΄μ κ· λ± λ°°μΉ
case ArrangementMode.Grid:
pos = new Vector3(fx * spacing, 0f, fy * spacing);
break;
// ββ μ‘κ° κ²©μ ββββββββββββββββββββββββββββββββββββββββ
// μ§μ νμ spacing * 0.5 λ§νΌ X μ€νμ
νκ³
// Y κ°κ²©μ β3/2 λ°°λ‘ μ€μ¬ μ μΌκ°ν νμΌμ ꡬμ±
case ArrangementMode.HexGrid:
{
float xOffset = (y % 2 == 0) ? 0f : spacing * 0.5f;
pos = new Vector3(
fx * spacing + xOffset,
0f,
fy * spacing * 0.866f); // 0.866 β β3/2
break;
}
// ββ XμΆ μ¬μΈν βββββββββββββββββββββββββββββββββββββββ
// X μμΉλ₯Ό μΈμλ‘ μ¬μΈνλ₯Ό κ³μ°ν΄ Y λ°©ν₯μΌλ‘ λ³μ
// Generate μμ t=0 μ€λ
μ·, Animateμμ μ€μκ° κ°±μ
case ArrangementMode.SineWave:
pos = new Vector3(fx * spacing, 0f, fy * spacing);
break;
// ββ Perlin Noise νλ ββββββββββββββββββββββββββββββββ
// κ° μΈμ€ν΄μ€ μμΉλ₯Ό seedλ‘ λ
Έμ΄μ¦λ₯Ό μνλ§ν΄ Y λ³μ
// noiseOffsetμ μν μ’νλ₯Ό μ μ₯ν΄ Animateμμ μ¬μ¬μ©
case ArrangementMode.NoiseField:
pos = new Vector3(fx * spacing, 0f, fy * spacing);
break;
// ββ λμ¬μ βββββββββββββββββββββββββββββββββββββββββββ
// y = λ°μ§λ¦ μΈλ±μ€, x = κ°λ μΈλ±μ€
// κ° λ§μ countXκ° μ€λΈμ νΈλ₯Ό κ· λ± λ°°μΉ
case ArrangementMode.Circle:
{
float radius = (y + 1) * spacing;
float angle = (float)x / countX * Mathf.PI * 2f;
pos = new Vector3(
Mathf.Cos(angle) * radius,
0f,
Mathf.Sin(angle) * radius);
break;
}
// ββ μλ₯΄ν€λ©λ°μ€ λμ βββββββββββββββββββββββββββββββββ
// κ°λμ λ°μ§λ¦μ΄ λͺ¨λ μΈλ±μ€μ λΉλ‘νμ¬ μ¦κ°
// countYλ₯Ό κ°λ νμλ‘ μ¬μ©
case ArrangementMode.Spiral:
{
float t = (float)idx / _totalCount;
float angle = t * Mathf.PI * 2f * countY;
float r = t * spacing * countX * 0.5f;
pos = new Vector3(Mathf.Cos(angle) * r, 0f, Mathf.Sin(angle) * r);
break;
}
// ββ νΌλ³΄λμΉ / ν΄λ°λΌκΈ° (Phyllotaxis) βββββββββββββββ
// ν©κΈκ°(137.5Β°)μ νμ νλ©΄μ λ°μ§λ¦μ βn μ λΉλ‘
// phyllotaxisAngleμ μ‘°κΈμ© λ°κΎΈλ©΄ μμ ν λ€λ₯Έ ν¨ν΄
case ArrangementMode.Phyllotaxis:
{
float angle = idx * phyllotaxisAngle * Mathf.Deg2Rad;
float r = phyllotaxisRadius * Mathf.Sqrt(idx);
pos = new Vector3(Mathf.Cos(angle) * r, 0f, Mathf.Sin(angle) * r);
break;
}
// ββ 리μ¬μ£Ό 곑μ (Lissajous) ββββββββββββββββββββββββββ
// x(t) = sin(aΒ·t + Ξ΄), z(t) = sin(bΒ·t)
// lissA, lissB λΉμ¨μ λ°λΌ λ€μν 곑μ ν¨ν΄ μμ±
// countX * countY κ°μ μ μ 곑μ μμ κ· λ± λΆν¬
case ArrangementMode.Lissajous:
{
float t = (float)idx / (_totalCount - 1) * Mathf.PI * 2f;
float px = Mathf.Sin(lissA * t + lissDelta) * lissScale;
float pz = Mathf.Sin(lissB * t) * lissScale;
// Y λ°©ν₯μΌλ‘ μ΄μ§ 무μμ λ³μλ₯Ό μ£Όλ©΄ μ
체κ°μ΄ μκΉ
float py = Mathf.Cos((lissA + lissB) * t) * lissScale * 0.2f;
pos = new Vector3(px, py, pz);
break;
}
}
_instances.Add(new InstanceData
{
basePos = pos,
noiseOffset = x * noiseScale + y * noiseScale * 1.3f // λ
Έμ΄μ¦ μν μ’ν
});
}
// GPU μ μ‘ λ°°μ΄ μ΄κΈ°ν
_matrices = new Matrix4x4[_totalCount];
_generated = true;
Animate(0f); // μ΄κΈ° μ€λ
μ·
}
// ============================================================
// [μνΈμν¬ λ‘μ§] Animate β λ§€ νλ μ λ³μ 곡μ
// ============================================================
void Animate(float t)
{
if (!_generated || _instances.Count == 0) return;
for (int i = 0; i < _instances.Count; i++)
{
InstanceData inst = _instances[i];
Vector3 pos = inst.basePos;
Vector3 scale = Vector3.one * objectSize;
switch (mode)
{
case ArrangementMode.Grid:
case ArrangementMode.HexGrid:
case ArrangementMode.Circle:
case ArrangementMode.Spiral:
case ArrangementMode.Phyllotaxis:
case ArrangementMode.Lissajous:
// κΈ°λ³Έ λ°°μ΄λ€: YμΆμΌλ‘ μμν μ¬μΈν 리ν
float ripple = Mathf.Sin(
(pos.x + pos.z) * waveFrequency + t) * waveHeight * 0.3f;
pos.y += ripple;
break;
case ArrangementMode.SineWave:
// X μμΉλ₯Ό μΈμλ‘ μ¬μΈν Y λ³μ
pos.y = Mathf.Sin(pos.x * waveFrequency + t) * sineAmplitude;
break;
case ArrangementMode.NoiseField:
// Perlin noiseλ‘ Y λ³μ + μ€μΌμΌ λ³μ‘°
float n = Mathf.PerlinNoise(inst.noiseOffset + t * 0.4f,
inst.noiseOffset * 0.7f + t * 0.2f);
pos.y = (n - 0.5f) * 2f * noiseAmplitude;
scale = Vector3.one * objectSize * Mathf.Lerp(0.3f, 1.4f, n);
break;
}
Vector3 worldPos = transform.TransformPoint(pos);
_matrices[i] = Matrix4x4.TRS(worldPos, transform.rotation, scale);
}
}
// ============================================================
// μΈνλΌ μ½λ β μμ λΆνμ
// ============================================================
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);
var batchMatrices = new Matrix4x4[batch];
System.Array.Copy(_matrices, index, batchMatrices, 0, batch);
Graphics.DrawMeshInstanced(
instanceMesh, 0, instanceMaterial, batchMatrices, batch);
index += batch;
total -= batch;
}
}
void EnsureResources()
{
if (instanceMesh == null)
{
var tmp = GameObject.CreatePrimitive(PrimitiveType.Cube);
instanceMesh = tmp.GetComponent<MeshFilter>().sharedMesh;
DestroyImmediate(tmp);
}
if (instanceMaterial == null)
{
Shader shader = null;
switch (_pipeline)
{
case RenderPipeline.URP: shader = Shader.Find("Universal Render Pipeline/Lit"); break;
case RenderPipeline.HDRP: shader = Shader.Find("HDRP/Lit"); break;
default: shader = Shader.Find("Standard"); break;
}
instanceMaterial = new Material(shader);
instanceMaterial.enableInstancing = true;
}
}
bool StructureParamsChanged() =>
mode != _cachedMode ||
countX != _cachedCountX ||
countY != _cachedCountY ||
spacing != _cachedSpacing ||
objectSize != _cachedObjectSize ||
noiseScale != _cachedNoiseScale ||
noiseAmplitude != _cachedNoiseAmplitude ||
sineFrequency != _cachedSineFrequency ||
sineAmplitude != _cachedSineAmplitude ||
phyllotaxisAngle != _cachedPhylAngle ||
phyllotaxisRadius != _cachedPhylRadius ||
lissA != _cachedLissA ||
lissB != _cachedLissB ||
lissDelta != _cachedLissDelta ||
lissScale != _cachedLissScale;
void CacheStructureParams()
{
_cachedMode = mode;
_cachedCountX = countX;
_cachedCountY = countY;
_cachedSpacing = spacing;
_cachedObjectSize = objectSize;
_cachedNoiseScale = noiseScale;
_cachedNoiseAmplitude = noiseAmplitude;
_cachedSineFrequency = sineFrequency;
_cachedSineAmplitude = sineAmplitude;
_cachedPhylAngle = phyllotaxisAngle;
_cachedPhylRadius = phyllotaxisRadius;
_cachedLissA = lissA;
_cachedLissB = lissB;
_cachedLissDelta = lissDelta;
_cachedLissScale = lissScale;
}
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;
}
}