

[GPU ํํฐํด ๋ฒ์ (GenArtBoilerplate)๊ณผ์ ์ฐจ์ด]
using UnityEngine;
using System.Collections.Generic;
#if UNITY_EDITOR
using UnityEditor; // EditorApplication.delayCall ์ฌ์ฉ์ ์ํด ์๋ํฐ ์ ์ฉ์ผ๋ก ์ํฌํธ
#endif
[ExecuteAlways] // ์๋ํฐ ๋ชจ๋(Play ์ )์์๋ Update ๋ฑ Unity ์ด๋ฒคํธ๊ฐ ํธ์ถ๋๊ฒ ํฉ๋๋ค
public class BoilerplateSculpture : MonoBehaviour
{
// ============================================================
// ์ธ์คํํฐ ํ๋ผ๋ฏธํฐ
// ============================================================
// ----- ๊ตฌ์กฐ ํ๋ผ๋ฏธํฐ ----------------------------------------
// ์ด ๊ฐ๋ค์ด ๋ฐ๋๋ฉด ๋ชจ๋ GameObject๋ฅผ ์ญ์ ํ๊ณ ๋ค์ ์์ฑํฉ๋๋ค.
[Header("๊ตฌ์กฐ ์ค์ (๋ณ๊ฒฝ ์ ์ค๋ธ์ ํธ ์ฌ์์ฑ)")]
public int countX = 5; // X์ถ ํ๋ธ ๊ฐ์
public int countY = 5; // Y์ถ ํ๋ธ ๊ฐ์
public int countZ = 5; // Z์ถ ํ๋ธ ๊ฐ์
public float spacing = 1.2f; // ํ๋ธ ๊ฐ๊ฒฉ
public float cubeSize = 0.8f; // ํ๋ธ ํฌ๊ธฐ (spacing๋ณด๋ค ์์ผ๋ฉด ํ์ด ์๊น๋๋ค)
public int colorSeed = 0; // ์์ ๋ฌด์์ ์๋ (๊ฐ์ ์๋ = ๊ฐ์ ์์ ๋ฐฐ์น)
// ----- ์ ๋๋ฉ์ด์
ํ๋ผ๋ฏธํฐ ----------------------------------
// ์ด ๊ฐ๋ค์ด ๋ฐ๋๋ฉด ์ฌ์์ฑ ์์ด ApplyAnimation(0f)๋ง ํธ์ถํฉ๋๋ค.
[Header("์ ๋๋ฉ์ด์
์ค์ (์ค์๊ฐ ๋ฐ์)")]
public float pulseSpeed = 1.0f; // ๋งฅ๋ ์๋
public float pulseIntensity = 0.15f; // ๋งฅ๋ ํฌ๊ธฐ (ํ๋ธ ํฌ๊ธฐ ๋ณํ๋)
public float pulseFrequency = 1.5f; // ํ๋ธ ๊ฐ ์์ ์ฐจ์ด (ํด์๋ก ํ๋ ๋๋)
// ----- ์์ ํ๋ผ๋ฏธํฐ ----------------------------------------
[Header("์์ ์ค์ (์ค์๊ฐ ๋ฐ์)")]
[Range(0f, 1f)] public float hueMin = 0.55f; // ์์ ๋ฒ์ ์ต์๊ฐ (ํ๋ ๊ณ์ด)
[Range(0f, 1f)] public float hueMax = 0.75f; // ์์ ๋ฒ์ ์ต๋๊ฐ (๋ณด๋ผ ๊ณ์ด)
[Range(0f, 1f)] public float saturation = 0.80f; // ์ฑ๋
[Range(0f, 1f)] public float brightness = 0.90f; // ๋ฐ๊ธฐ
public float emissionIntensity = 0.5f; // ๋ฐ๊ด ๊ฐ๋
// ----- ์ฌ์ง ์ค์ --------------------------------------------
[Header("์ฌ์ง ์ค์ ")]
public Material baseMaterial; // null์ด๋ฉด ์๋ ์์ฑ
// ============================================================
// ๋ด๋ถ ๋ฐ์ดํฐ ๊ตฌ์กฐ
// ============================================================
// ํ๋ธ ํ๋์ ๋ฐ์ดํฐ (Generate ์ ๊ฒฐ์ , ์ดํ ApplyAnimation์์ ์ฐธ์กฐ)
struct CubeData
{
public Transform tr; // ํ๋ธ์ Transform (์์น/ํฌ๊ธฐ ์กฐ์์ ์ฌ์ฉ)
public Material mat; // ํ๋ธ ์ ์ฉ ๋จธํฐ๋ฆฌ์ผ ์ธ์คํด์ค (์์ ๊ฐ๋ณ ์ ์ฉ)
public Vector3 basePos; // ์์ฑ ์ ๊ฒฐ์ ๋ ๊ธฐ์ค ์์น
public float hue; // ์์ฑ ์ ๊ฒฐ์ ๋ ๊ณ ์ ์์ hue
public float phase; // ์์ฑ ์ ๊ฒฐ์ ๋ ๋งฅ๋ ์์ ์คํ์
(ํ๋ธ๋ง๋ค ๋ค๋ฅธ ํ์ด๋ฐ)
}
List<CubeData> _cubes = new List<CubeData>(); // ์ ์ฒด ํ๋ธ ๋ฐ์ดํฐ ๋ชฉ๋ก
// ๋ ๋ ํ์ดํ๋ผ์ธ ์ข
๋ฅ (OnEnable์์ ์๋ ๊ฐ์ง)
enum RenderPipeline { BuiltIn, URP, HDRP }
RenderPipeline _pipeline;
// ============================================================
// ๊ตฌ์กฐ ํ๋ผ๋ฏธํฐ ์บ์
// OnValidate์์ ์ค์ ๋ก ๊ฐ์ด ๋ฐ๋์๋์ง ๋น๊ตํ๊ธฐ ์ํ ์ด์ ๊ฐ ์ ์ฅ์
// ============================================================
int _cachedCountX;
int _cachedCountY;
int _cachedCountZ;
float _cachedSpacing;
float _cachedCubeSize;
int _cachedColorSeed;
#if UNITY_EDITOR
// QueueGenerate ์ค๋ณต ํธ์ถ ๋ฐฉ์ง ํ๋๊ทธ (์๋ํฐ ์ ์ฉ)
bool _generateQueued = false;
#endif
// ============================================================
// Unity ์ด๋ฒคํธ ๋ฉ์๋
// ============================================================
// OnEnable: ์ปดํฌ๋ํธ ํ์ฑํ ์ ํธ์ถ
void OnEnable()
{
_pipeline = DetectPipeline(); // ๋ ๋ ํ์ดํ๋ผ์ธ ์๋ ๊ฐ์ง
if (baseMaterial == null)
baseMaterial = CreateFallbackMaterial(); // ๋จธํฐ๋ฆฌ์ผ ์๋ ์์ฑ
Generate(); // ํ๋ธ GameObject ์์ฑ
CacheStructureParams(); // ๊ตฌ์กฐ ํ๋ผ๋ฏธํฐ ์บ์ ์ ์ฅ
}
// OnValidate: ์ธ์คํํฐ์์ ๊ฐ์ด ๋ฐ๋ ๋๋ง๋ค ํธ์ถ
// ์ฃผ์: ์ด ์ฝ๋ฐฑ ์์์ SetParent(), CreatePrimitive()๋ฅผ ์ง์ ํธ์ถํ๋ฉด
// Unity ๊ฒฝ๊ณ ๊ฐ ๋ฐ์ํฉ๋๋ค โ QueueGenerate()๋ก ์ฐํํฉ๋๋ค
void OnValidate()
{
#if UNITY_EDITOR
_pipeline = DetectPipeline();
if (baseMaterial == null)
baseMaterial = CreateFallbackMaterial();
if (StructureParamsChanged())
{
// ๊ตฌ์กฐ ํ๋ผ๋ฏธํฐ๊ฐ ๋ฐ๋์์ ๋ โ ๋ค์ ํ๋ ์์ผ๋ก ์ฌ์์ฑ ์์ฝ
CacheStructureParams();
QueueGenerate();
}
else
{
// ์ ๋๋ฉ์ด์
/์์ ํ๋ผ๋ฏธํฐ๋ง ๋ฐ๋์์ ๋ โ ์ฆ์ ์๊ฐ ๊ฐฑ์
ApplyAnimation(0f);
}
#endif
}
#if UNITY_EDITOR
// QueueGenerate: Generate()๋ฅผ ํ ํ๋ ์ ๋ค๋ก ๋ฏธ๋ฃจ์ด ์คํํฉ๋๋ค.
// OnValidate ์ปจํ
์คํธ์์ ์ฌ ๊ทธ๋ํ๋ฅผ ์ง์ ์์ ํ๋ฉด Unity ๋ด๋ถ ์ง๋ ฌํ์
// ์ถฉ๋ํ๊ธฐ ๋๋ฌธ์ EditorApplication.delayCall๋ก ์์ ํ ํ์ด๋ฐ์ ์คํํฉ๋๋ค.
void QueueGenerate()
{
if (_generateQueued) return; // ์ด๋ฏธ ์์ฝ๋์ด ์์ผ๋ฉด ์ค๋ณต ์์ฝํ์ง ์์
_generateQueued = true;
EditorApplication.delayCall += () => // ๋ค์ ์๋ํฐ ๋ฃจํ ํ์ด๋ฐ์ ์คํ
{
_generateQueued = false;
if (this == null) return; // ์ค๋ธ์ ํธ๊ฐ ์ญ์ ๋ ๊ฒฝ์ฐ ์์ ํ๊ฒ ๊ฑด๋๋
Generate();
};
}
#endif
// Update: ๋งค ํ๋ ์ ํธ์ถ
void Update()
{
#if UNITY_EDITOR
// ์๋ํฐ Scene ๋ทฐ: ์ ๋๋ฉ์ด์
์์ด t=0 ์ ์ง ์ํ ์ ์ง
if (!Application.isPlaying) return;
#endif
// Play ๋ชจ๋: ์๊ฐ ๊ธฐ๋ฐ ์ ๋๋ฉ์ด์
์ ์ฉ
ApplyAnimation(Time.time * pulseSpeed);
}
// ============================================================
// ๊ตฌ์กฐ ํ๋ผ๋ฏธํฐ ์บ์ ์ ํธ
// ============================================================
// ์ด์ ์บ์ ๊ฐ๊ณผ ํ์ฌ ๊ฐ์ ๋น๊ตํฉ๋๋ค. ํ๋๋ผ๋ ๋ค๋ฅด๋ฉด true ๋ฐํ
bool StructureParamsChanged()
{
return countX != _cachedCountX
|| countY != _cachedCountY
|| countZ != _cachedCountZ
|| spacing != _cachedSpacing
|| cubeSize != _cachedCubeSize
|| colorSeed != _cachedColorSeed;
}
// ํ์ฌ ๊ฐ์ ์บ์์ ์ ์ฅํฉ๋๋ค
void CacheStructureParams()
{
_cachedCountX = countX;
_cachedCountY = countY;
_cachedCountZ = countZ;
_cachedSpacing = spacing;
_cachedCubeSize = cubeSize;
_cachedColorSeed = colorSeed;
}
// ============================================================
// ์์ฑ
// ์ค์ GameObject ํ๋ธ๋ฅผ ์์ฑํ๊ณ this.transform ํ์์ ๋ฐฐ์นํฉ๋๋ค.
// ============================================================
void Generate()
{
Clear(); // ๊ธฐ์กด ํ๋ธ๋ฅผ ์ ๋ถ ์ญ์ ํฉ๋๋ค
Random.InitState(colorSeed); // ์๋ ๊ณ ์ โ ์ฌ์์ฑํด๋ ๋์ผํ ์์ ๋ฐฐ์น ์ฌํ
// ---- [์ํธ์ํฌ ๋ก์ง] ์ฌ๊ธฐ๋ฅผ ์์ ํ์ธ์ ----
// ์์: 3์ฐจ์ ๊ทธ๋ฆฌ๋ ๋ฐฐ์น
// ๊ฐ ์ถ์ ๊ทธ๋ฆฌ๋๋ฅผ ๋ก์ปฌ ์์ ๊ธฐ์ค์ผ๋ก ์ค์ ์ ๋ ฌํฉ๋๋ค.
// offset ์์ด ๋ฐฐ์นํ๋ฉด (0,0,0)~(max,max,max) ๋ฒ์๊ฐ ๋์ด ํ์ชฝ์ผ๋ก ์น์ฐ์นฉ๋๋ค.
float offsetX = (countX - 1) * spacing * 0.5f;
float offsetY = (countY - 1) * spacing * 0.5f;
float offsetZ = (countZ - 1) * spacing * 0.5f;
for (int x = 0; x < countX; x++)
{
for (int y = 0; y < countY; y++)
{
for (int z = 0; z < countZ; z++)
{
// offset์ ๋นผ์ ๊ทธ๋ฆฌ๋ ์ค์ฌ์ด (0,0,0)์ด ๋๋๋ก ํฉ๋๋ค
Vector3 pos = new Vector3(
x * spacing - offsetX,
y * spacing - offsetY,
z * spacing - offsetZ
);
// ํ๋ธ๋ง๋ค hueMin~hueMax ๋ฒ์ ์์์ ๋ฌด์์ hue๋ฅผ ๋ถ์ฌํฉ๋๋ค
float hue = Random.Range(hueMin, hueMax);
// ํ๋ธ๋ง๋ค ๋งฅ๋ ์์์ ๋ค๋ฅด๊ฒ ์ค์ ๋์์ ์์ง์ด์ง ์๊ฒ ํฉ๋๋ค
// x/y/z ์ขํ๋ฅผ ๋ชจ๋ ํ์ฉํด 3์ฐจ์์ ์ผ๋ก ์์ ์ฐจ์ด๋ฅผ ๋ง๋ญ๋๋ค
float phase = (x + y + z) * pulseFrequency;
SpawnCube(pos, hue, phase);
}
}
}
// ---- [์ํธ์ํฌ ๋ก์ง ๋] ----
ApplyAnimation(0f); // ์์ฑ ์งํ t=0 ๊ธฐ์ค ์ด๊ธฐ ์ํ ์ ์ฉ
}
// ํ๋ธ GameObject ํ๋๋ฅผ ์์ฑํ๊ณ _cubes ๋ฆฌ์คํธ์ ๋ฑ๋กํฉ๋๋ค
void SpawnCube(Vector3 pos, float hue, float phase)
{
// ํ๋ธ ํ๋ฆฌ๋ฏธํฐ๋ธ ์์ฑ
GameObject go = GameObject.CreatePrimitive(PrimitiveType.Cube);
// ์ด ์คํฌ๋ฆฝํธ๊ฐ ๋ถ์ ์ค๋ธ์ ํธ์ ์์์ผ๋ก ์ค์ ํฉ๋๋ค
// โ ๋ถ๋ชจ๋ฅผ ์ด๋/ํ์ ํ๋ฉด ์ ์ฒด ์กฐํ๋ฌผ์ด ํจ๊ป ์์ง์
๋๋ค
go.transform.SetParent(transform);
go.transform.localPosition = pos;
go.transform.localScale = Vector3.one * cubeSize;
// ํ๋ธ๋ง๋ค ๋
๋ฆฝ์ ์ธ ๋จธํฐ๋ฆฌ์ผ ์ธ์คํด์ค๋ฅผ ๋ง๋ค์ด์ผ
// ์์์ ๊ฐ๋ณ์ ์ผ๋ก ์ ์ดํ ์ ์์ต๋๋ค.
// baseMaterial์ ์ง์ ์์ ํ๋ฉด ๋ชจ๋ ํ๋ธ ์์์ด ๋์์ ๋ฐ๋๋๋ค.
Material mat = Instantiate(baseMaterial);
mat.EnableKeyword("_EMISSION");
mat.globalIlluminationFlags = MaterialGlobalIlluminationFlags.RealtimeEmissive;
go.GetComponent<Renderer>().material = mat;
// ๋ฐ์ดํฐ๋ฅผ ๋ฆฌ์คํธ์ ์ ์ฅํด์ ApplyAnimation์์ ์ฐธ์กฐํฉ๋๋ค
_cubes.Add(new CubeData
{
tr = go.transform,
mat = mat,
basePos = pos,
hue = hue,
phase = phase
});
}
// ๋ชจ๋ ์์ ํ๋ธ๋ฅผ ์ญ์ ํ๊ณ ๋ฆฌ์คํธ๋ฅผ ๋น์๋๋ค
void Clear()
{
_cubes.Clear(); // ๋ฐ์ดํฐ ๋ฆฌ์คํธ ์ด๊ธฐํ
// DestroyImmediate: ์๋ํฐ ๋ชจ๋์์๋ Destroy ๋์ ์ด๊ฒ์ ์ฌ์ฉํด์ผ
// ๊ฐ์ ํ๋ ์ ์์์ ์ญ์ ๊ฐ ์๋ฃ๋ฉ๋๋ค.
// Destroy๋ Play ๋ชจ๋ ์ ์ฉ์ด๋ฉฐ ์๋ํฐ์์๋ ๋ค์ ํ๋ ์๊น์ง ์ง์ฐ๋ฉ๋๋ค.
while (transform.childCount > 0)
DestroyImmediate(transform.GetChild(0).gameObject);
}
// ============================================================
// ์ ๋๋ฉ์ด์
// t = 0 โ Scene ์๋ํฐ ์ ์ง ์ํ
// t = Time.time * pulseSpeed โ Play ๋ชจ๋ ์ ๋๋ฉ์ด์
// ============================================================
void ApplyAnimation(float t)
{
for (int i = 0; i < _cubes.Count; i++)
{
CubeData c = _cubes[i];
// DestroyImmediate ์งํ ๋ฆฌ์คํธ ์ ๋ฆฌ ์ ์ ๋ฌดํจํ๋ ์ฐธ์กฐ์
// ์ ๊ทผํ๋ฉด MissingReferenceException์ด ๋ฐ์ํ๋ฏ๋ก null ์ฒดํฌํฉ๋๋ค
if (c.tr == null || c.mat == null) continue;
// ---- [์ํธ์ํฌ ๋ก์ง] ์ฌ๊ธฐ๋ฅผ ์์ ํ์ธ์ ----
// ์์: ๊ฐ ํ๋ธ๊ฐ ์์ ์ ์์์ผ๋ก ๋งฅ๋ (ํฌ๊ธฐ ๋ณํ)
// ํ๋ธ๋ง๋ค phase๊ฐ ๋ฌ๋ผ์ ์๋ก ๋ค๋ฅธ ํ์ด๋ฐ์ผ๋ก ์ปค์ง๊ณ ์์์ง๋๋ค
float pulse = Mathf.Sin(t + c.phase) * pulseIntensity;
float scale = cubeSize + pulse; // ๊ธฐ๋ณธ ํฌ๊ธฐ ยฑ ๋งฅ๋๋
float normalized = (pulse / pulseIntensity + 1f) * 0.5f; // 0~1 (์์ ๋ณ์กฐ์ฉ)
c.tr.localScale = Vector3.one * Mathf.Max(0.01f, scale); // ํฌ๊ธฐ๊ฐ ์์๊ฐ ๋์ง ์๊ฒ ๋ณดํธ
// ๋งฅ๋ ์ํ์ ๋ฐ๋ผ ๋ฐ๊ธฐ๋ฅผ ๋ณ์กฐํฉ๋๋ค
// ์ปค์ง ๋ ๋ฐ์์ง๊ณ ์์์ง ๋ ์ด๋์์ง๋ ํจ๊ณผ
float animBrightness = brightness * Mathf.Lerp(0.4f, 1.0f, normalized);
Color albedo = Color.HSVToRGB(c.hue, saturation, animBrightness);
Color emission = Color.HSVToRGB(c.hue, 1f, normalized) * emissionIntensity;
// ---- [์ํธ์ํฌ ๋ก์ง ๋] ----
ApplyColor(c.mat, albedo, emission);
}
}
// ============================================================
// ํ์ดํ๋ผ์ธ๋ณ ์์ ์ ์ฉ
// ์
ฐ์ด๋ ํ๋กํผํฐ ์ด๋ฆ์ด ํ์ดํ๋ผ์ธ๋ง๋ค ๋ค๋ฅด๊ธฐ ๋๋ฌธ์ ๋ถ๊ธฐ ์ฒ๋ฆฌํฉ๋๋ค.
// URP: _BaseColor, _EmissionColor
// HDRP: _BaseColor, _EmissiveColor
// Built-in: _Color, _EmissionColor
// ============================================================
void ApplyColor(Material mat, Color albedo, Color emission)
{
switch (_pipeline)
{
case RenderPipeline.URP:
mat.SetColor("_BaseColor", albedo);
mat.SetColor("_EmissionColor", emission);
break;
case RenderPipeline.HDRP:
mat.SetColor("_BaseColor", albedo);
mat.SetColor("_EmissiveColor", emission);
break;
default: // Built-in
mat.SetColor("_Color", albedo);
mat.SetColor("_EmissionColor", emission);
break;
}
}
// ============================================================
// ์ ํธ๋ฆฌํฐ
// ============================================================
// ํ๋ก์ ํธ์ ๋ ๋ ํ์ดํ๋ผ์ธ์ ์๋ ๊ฐ์งํฉ๋๋ค
RenderPipeline DetectPipeline()
{
var pipeline = UnityEngine.Rendering.GraphicsSettings.currentRenderPipeline;
if (pipeline == null) return RenderPipeline.BuiltIn; // Built-in์ ์์
์ด null
string name = pipeline.GetType().Name;
if (name.Contains("Universal")) return RenderPipeline.URP;
if (name.Contains("HighDefinition") || name.Contains("HDRP")) return RenderPipeline.HDRP;
return RenderPipeline.BuiltIn;
}
// ์ธ์คํํฐ์์ baseMaterial์ด ํ ๋น๋์ง ์์ ๊ฒฝ์ฐ ์๋์ผ๋ก ์์ฑํฉ๋๋ค.
// ํ๋ก์ ํธ์ ๋ ๋ ํ์ดํ๋ผ์ธ์ ๋ง๋ ์
ฐ์ด๋๋ฅผ ์์๋๋ก ํ์ํฉ๋๋ค.
Material CreateFallbackMaterial()
{
string[] candidates = {
"Universal Render Pipeline/Lit", // URP
"HDRP/Lit", // HDRP
"Standard" // Built-in
};
foreach (string shaderName in candidates)
{
Shader s = Shader.Find(shaderName);
if (s != null)
{
Material mat = new Material(s);
mat.EnableKeyword("_EMISSION");
mat.globalIlluminationFlags = MaterialGlobalIlluminationFlags.RealtimeEmissive;
Debug.Log($"[GenArtSculpture] ์
ฐ์ด๋ ์๋ ๊ฐ์ง: {shaderName}");
return mat;
}
}
// ์ด๋ค ์
ฐ์ด๋๋ ์ฐพ์ง ๋ชปํ ๊ฒฝ์ฐ (๋งค์ฐ ๋๋ฌธ ์ํฉ)
Debug.LogWarning("[GenArtSculpture] Inspector์์ baseMaterial์ ์ง์ ํ ๋นํด์ฃผ์ธ์.");
return new Material(Shader.Find("Diffuse"));
}
}