๐Ÿ“”Boilerplate (Cube Interaction)

BamgasiJMยท2026๋…„ 3์›” 16์ผ

Unity GenArt

๋ชฉ๋ก ๋ณด๊ธฐ
4/41
post-thumbnail

Boilerplate Code for Generative Art

[์‚ฌ์šฉ ๋ฐฉ๋ฒ•]

  1. ์ด ํŒŒ์ผ์„ ๋ณต์‚ฌํ•ด์„œ ์ƒˆ ์ด๋ฆ„์œผ๋กœ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค.
  2. ํด๋ž˜์Šค ์ด๋ฆ„์„ ํŒŒ์ผ๋ช…๊ณผ ๋™์ผํ•˜๊ฒŒ ๋ณ€๊ฒฝํ•ฉ๋‹ˆ๋‹ค.
  3. [์•„ํŠธ์›Œํฌ ๋กœ์ง] ์„น์…˜๋งŒ ์ˆ˜์ •ํ•ด์„œ ์ƒˆ๋กœ์šด ์•„ํŠธ์›Œํฌ๋ฅผ ๋งŒ๋“ญ๋‹ˆ๋‹ค.
  4. ๋‚˜๋จธ์ง€ ์„น์…˜([์ธํ”„๋ผ ์ฝ”๋“œ])์€ ์ˆ˜์ •ํ•˜์ง€ ์•Š์•„๋„ ๋ฉ๋‹ˆ๋‹ค.

[์ฝ”๋“œ์˜ ๊ธฐ๋Šฅ]

[์ธํ„ฐ๋ž™์…˜]

  • ๋งˆ์šฐ์Šค ํด๋ฆญ: ํด๋ฆญ ์ง€์  ๊ทผ์ฒ˜ ํ๋ธŒ๋ฅผ ํŠ•๊ฒจ๋ƒ…๋‹ˆ๋‹ค
  • ํ๋ธŒ๋Š” ์Šคํ”„๋ง ํž˜์œผ๋กœ ์›๋ž˜ ์œ„์น˜๋กœ ๋Œ์•„์˜ต๋‹ˆ๋‹ค
  • ๋Œํ•‘์œผ๋กœ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ์ •์ง€ํ•ฉ๋‹ˆ๋‹ค

๐Ÿ“„BoilerplateSculptureInteraction.cs

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"));
    }
}

profile
Coding Art with Blender / oF / Processing / p5.js / nannou

0๊ฐœ์˜ ๋Œ“๊ธ€