
[์ธํฐ๋์ ]
using UnityEngine;
using System.Collections.Generic;
using UnityEngine.InputSystem;
#if UNITY_EDITOR
using UnityEditor;
#endif
[ExecuteAlways]
public class BoilerplateSculptureInteraction : MonoBehaviour
{
// ============================================================
// ์ธ์คํํฐ ํ๋ผ๋ฏธํฐ
// ============================================================
[Header("๊ตฌ์กฐ ์ค์ (๋ณ๊ฒฝ ์ ์ค๋ธ์ ํธ ์ฌ์์ฑ)")]
public int countX = 5;
public int countY = 5;
public int countZ = 5;
public float spacing = 1.2f;
public float cubeSize = 0.8f;
public int colorSeed = 0;
[Header("๋งฅ๋ ์ ๋๋ฉ์ด์
(์ค์๊ฐ ๋ฐ์)")]
public float pulseSpeed = 1.0f;
public float pulseIntensity = 0.12f;
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 float explodeRadius = 3.0f; // ํด๋ฆญ ํญ๋ฐ ์ํฅ ๋ฐ๊ฒฝ
public float explodeForce = 8.0f; // ํ๊ฒจ๋ด๋ ํ์ ํฌ๊ธฐ
public float springStrength = 12.0f; // ์คํ๋ง ๋ณต์๋ ฅ (ํด์๋ก ๋น ๋ฅด๊ฒ ๋์์ด)
public float damping = 6.0f; // ๋ํ (ํด์๋ก ๋น ๋ฅด๊ฒ ์ ์ง)
[Header("์ฌ์ง ์ค์ ")]
public Material baseMaterial;
// ============================================================
// ๋ด๋ถ ๋ฐ์ดํฐ ๊ตฌ์กฐ
// ============================================================
struct CubeData
{
public Transform tr;
public Material mat;
public Vector3 basePos; // ์๋ ๋ก์ปฌ ์์น (์คํ๋ง ๋ชฉํ ์ง์ )
public float hue;
public float phase;
// ์คํ๋ง ๋ฌผ๋ฆฌ ์ํ (Play ๋ชจ๋์์๋ง ์ฌ์ฉ)
public Vector3 velocity; // ํ์ฌ ์ด๋ ์๋
public Vector3 offset; // basePos๋ก๋ถํฐ ํ์ฌ ๋ณ์
}
List<CubeData> _cubes = new List<CubeData>();
enum RenderPipeline { BuiltIn, URP, HDRP }
RenderPipeline _pipeline;
// ============================================================
// ๊ตฌ์กฐ ํ๋ผ๋ฏธํฐ ์บ์
// ============================================================
int _cachedCountX;
int _cachedCountY;
int _cachedCountZ;
float _cachedSpacing;
float _cachedCubeSize;
int _cachedColorSeed;
#if UNITY_EDITOR
bool _generateQueued = false;
#endif
// ============================================================
// Unity ์ด๋ฒคํธ
// ============================================================
void OnEnable()
{
_pipeline = DetectPipeline();
if (baseMaterial == null)
baseMaterial = CreateFallbackMaterial();
Generate();
CacheStructureParams();
}
void OnValidate()
{
#if UNITY_EDITOR
_pipeline = DetectPipeline();
if (baseMaterial == null)
baseMaterial = CreateFallbackMaterial();
if (StructureParamsChanged())
{
CacheStructureParams();
QueueGenerate();
}
else
{
ApplyAnimation(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 UNITY_EDITOR
// Scene ๋ทฐ: ๋งฅ๋ ์ ๋๋ฉ์ด์
๋ง ์ ์ง ์ํ๋ก ํ์
if (!Application.isPlaying)
{
ApplyAnimation(0f);
return;
}
#endif
// Play ๋ชจ๋: ์คํ๋ง ๋ฌผ๋ฆฌ โ ๋งฅ๋ โ ์ธํฐ๋์
์
๋ ฅ ์์๋ก ์ฒ๋ฆฌ
UpdateSpringPhysics();
ApplyAnimation(Time.time * pulseSpeed);
HandleInput();
}
// ============================================================
// ๊ตฌ์กฐ ํ๋ผ๋ฏธํฐ ์บ์ ์ ํธ
// ============================================================
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;
}
// ============================================================
// ์์ฑ
// ============================================================
void Generate()
{
Clear();
Random.InitState(colorSeed);
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++)
{
Vector3 pos = new Vector3(x * spacing - offsetX,
y * spacing - offsetY,
z * spacing - offsetZ);
float hue = Random.Range(hueMin, hueMax);
float phase = (x + y + z) * pulseFrequency;
SpawnCube(pos, hue, phase);
}
// ---- [์ํธ์ํฌ ๋ก์ง ๋] ----
ApplyAnimation(0f);
}
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;
// ํ๋ธ๋ง๋ค ๋
๋ฆฝ์ ์ธ ๋จธํฐ๋ฆฌ์ผ ์ธ์คํด์ค (์์ ๊ฐ๋ณ ์ ์ด์ฉ)
Material mat = Instantiate(baseMaterial);
mat.EnableKeyword("_EMISSION");
mat.globalIlluminationFlags = MaterialGlobalIlluminationFlags.RealtimeEmissive;
go.GetComponent<Renderer>().material = mat;
_cubes.Add(new CubeData
{
tr = go.transform,
mat = mat,
basePos = pos,
hue = hue,
phase = phase,
velocity = Vector3.zero, // ์ด๊ธฐ ์๋ 0
offset = Vector3.zero // ์ด๊ธฐ ๋ณ์ 0
});
}
void Clear()
{
_cubes.Clear();
while (transform.childCount > 0)
DestroyImmediate(transform.GetChild(0).gameObject);
}
// ============================================================
// ์คํ๋ง ๋ฌผ๋ฆฌ
// F = -springStrength * offset - damping * velocity
// ์คํ๋ง ํ: ๋ณ์์ ๋น๋กํด์ ์๋ ์์น๋ก ๋น๊น๋๋ค
// ๋ํ ํ: ์๋์ ๋น๋กํด์ ์ด๋์ ๊ฐ์ ํฉ๋๋ค
// ============================================================
void UpdateSpringPhysics()
{
float dt = Time.deltaTime; // ํ๋ ์ ๊ฐ๊ฒฉ (๋ฌผ๋ฆฌ ๊ณ์ฐ์ ํ์)
for (int i = 0; i < _cubes.Count; i++)
{
CubeData c = _cubes[i];
if (c.tr == null) continue;
// ์คํ๋ง ํ = -(๋ณต์๋ ฅ ๊ณ์ ร ๋ณ์) - (๋ํ ๊ณ์ ร ์๋)
Vector3 springForce = -springStrength * c.offset;
Vector3 dampingForce = -damping * c.velocity;
Vector3 acceleration = springForce + dampingForce;
// ์ค์ผ๋ฌ ์ ๋ถ: ์๋์ ๋ณ์๋ฅผ ํ ํ๋ ์์ฉ ๋์ ํฉ๋๋ค
c.velocity += acceleration * dt;
c.offset += c.velocity * dt;
// ๋ณ์๊ฐ ๋งค์ฐ ์์์ง๋ฉด ์์ ํ ์ ์ง ์ฒ๋ฆฌ (์์น ์ง๋ ๋ฐฉ์ง)
if (c.offset.magnitude < 0.001f && c.velocity.magnitude < 0.001f)
{
c.offset = Vector3.zero;
c.velocity = Vector3.zero;
}
// ๋ก์ปฌ ์ขํ: ๊ธฐ์ค ์์น + ๋ฌผ๋ฆฌ ๋ณ์ (ApplyAnimation์ Y ๋ณํ๋ ๋ณ๋ ๊ฐ์ฐ)
c.tr.localPosition = c.basePos + c.offset;
// struct๋ ๊ฐ ํ์
์ด๋ฏ๋ก ์์ ํ ๋ฐ๋์ ๋ฆฌ์คํธ์ ๋ค์ ์๋๋ค
_cubes[i] = c;
}
}
// ============================================================
// ์ฌ์ฉ์ ์
๋ ฅ ์ฒ๋ฆฌ
// ============================================================
void HandleInput()
{
Mouse mouse = Mouse.current;
if (mouse == null) return;
// New Input System: wasReleasedThisFrame โ ํด๋ฆญ ์๋ฃ ์์ ๊ฐ์ง
if (!mouse.leftButton.wasReleasedThisFrame) return;
// 2D ์คํฌ๋ฆฐ ์ขํ๋ฅผ ์ฝ์ด์ ๋ ์ด์บ์คํธ์ ์ฌ์ฉํฉ๋๋ค
Vector2 screenPos = mouse.position.ReadValue();
Ray ray = Camera.main.ScreenPointToRay(screenPos);
RaycastHit hit;
Vector3 worldHitPoint;
if (Physics.Raycast(ray, out hit))
worldHitPoint = hit.point;
else
worldHitPoint = ray.GetPoint(10f);
Vector3 localHitPoint = transform.InverseTransformPoint(worldHitPoint);
Explode(localHitPoint);
}
// ํญ๋ฐ: ํด๋ฆญ ์ง์ ๋ฐ๊ฒฝ ๋ด ํ๋ธ์ ๋ฐฉ์ฌํ์ผ๋ก ์๋๋ฅผ ๋ถ์ฌํฉ๋๋ค
public void Explode(Vector3 localCenter)
{
for (int i = 0; i < _cubes.Count; i++)
{
CubeData c = _cubes[i];
if (c.tr == null) continue;
// ํด๋ฆญ ์ง์ ~ ํ๋ธ ๊ธฐ์ค ์์น๊น์ง์ ๋ฒกํฐ
Vector3 diff = c.basePos - localCenter;
float distance = diff.magnitude;
// ์ํฅ ๋ฐ๊ฒฝ ๋ฐ์ด๋ฉด ๊ฑด๋๋
if (distance > explodeRadius) continue;
// ๊ฑฐ๋ฆฌ์ ๋ฐ๋น๋กํ๋ ํ (๊ฐ๊น์ธ์๋ก ๊ฐํ๊ฒ ํ๊ฒจ๋)
// Mathf.Max๋ก 0 ๋๋๊ธฐ ๋ฐฉ์ง
float strength = explodeForce * (1f - distance / explodeRadius);
Vector3 direction = diff.normalized;
// ๊ธฐ์กด ์๋์ ํญ๋ฐ ์๋๋ฅผ ๊ฐ์ฐํฉ๋๋ค (์ฐ์ ํด๋ฆญ ์ ๋์ ๋จ)
c.velocity += direction * strength;
_cubes[i] = c;
}
}
// ============================================================
// ์ ๋๋ฉ์ด์
(๋งฅ๋)
// ์คํ๋ง ๋ฌผ๋ฆฌ๋ก ์ด๋ฏธ localPosition์ด ์ค์ ๋ ์ํ์์
// Y์ถ ๋งฅ๋๋ง offset์ ๊ฐ์ฐํฉ๋๋ค
// ============================================================
void ApplyAnimation(float t)
{
for (int i = 0; i < _cubes.Count; i++)
{
CubeData c = _cubes[i];
if (c.tr == null || c.mat == null) continue;
// ---- [์ํธ์ํฌ ๋ก์ง] ----
float pulse = Mathf.Sin(t + c.phase) * pulseIntensity;
float normalized = (pulse / pulseIntensity + 1f) * 0.5f;
// ํฌ๊ธฐ ๋งฅ๋: ์คํ๋ง ๋ฌผ๋ฆฌ์ ๋
๋ฆฝ์ ์ผ๋ก ํฌ๊ธฐ๋ง ๋ณํ
float scale = cubeSize + pulse;
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);
}
}
// ============================================================
// ํ์ดํ๋ผ์ธ๋ณ ์์ ์ ์ฉ
// ============================================================
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:
mat.SetColor("_Color", albedo);
mat.SetColor("_EmissionColor", emission);
break;
}
}
// ============================================================
// ์ ํธ๋ฆฌํฐ
// ============================================================
RenderPipeline DetectPipeline()
{
var pipeline = UnityEngine.Rendering.GraphicsSettings.currentRenderPipeline;
if (pipeline == null) return RenderPipeline.BuiltIn;
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;
}
Material CreateFallbackMaterial()
{
string[] candidates = {
"Universal Render Pipeline/Lit",
"HDRP/Lit",
"Standard"
};
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"));
}
}