๐ŸซงArt_004 Radial Energy Field [Game Object]

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

Unity GenArt

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

GameObject์— ๋ถ™์—ฌ์„œ ์‚ฌ์šฉ

์ฃผ์š” ํ•จ์ˆ˜ ๋ฐ ๊ธฐ๋Šฅ ์„ค๋ช…

1. MaterialPropertyBlock ์˜ ์—ญํ• 

"๋ฉ”๋ชจ๋ฆฌ๋ฅผ ์•„๋ผ๋ฉด์„œ ์ƒ‰์ƒ์„ ๋ฐ”๊พธ๋Š” ๋งˆ๋ฒ• ์ƒ์ž"

  • ๊ธฐ์กด ๋ฐฉ์‹์˜ ๋ฌธ์ œ์  (renderer.material ์ง์ ‘ ์ˆ˜์ •):

    • ์œ ๋‹ˆํ‹ฐ์—์„œ renderer.material์— ์ ‘๊ทผํ•˜์—ฌ ์ƒ‰์ƒ์„ ๋ฐ”๊พธ๋ฉด, ์œ ๋‹ˆํ‹ฐ๋Š” ์ž๋™์œผ๋กœ ํ•ด๋‹น ๋จธํ‹ฐ๋ฆฌ์–ผ์„ ๋ณต์ œ(Clone)ํ•˜์—ฌ ์ƒˆ๋กœ์šด ๋ฉ”๋ชจ๋ฆฌ ๊ณต๊ฐ„์„ ํ• ๋‹นํ•ฉ๋‹ˆ๋‹ค.
    • ์ด ์Šคํฌ๋ฆฝํŠธ์ฒ˜๋Ÿผ ๊ตฌ์ฒด๊ฐ€ ์ˆ˜๋ฐฑ ๊ฐœ๋ผ๋ฉด, ์ˆ˜๋ฐฑ ๊ฐœ์˜ ๋ณต์ œ๋œ ๋จธํ‹ฐ๋ฆฌ์–ผ์ด ์ƒ์„ฑ๋˜์–ด ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰์ด ๊ธ‰์ฆํ•˜๊ณ , GPU๊ฐ€ ์ด๋ฅผ ์ฒ˜๋ฆฌํ•˜๊ธฐ ์œ„ํ•ด ๋“œ๋กœ์šฐ์ฝœ(Draw Call)์ด ๋ถ„๋ฆฌ๋˜์–ด ์„ฑ๋Šฅ์ด ๊ธ‰๊ฒฉํžˆ ์ €ํ•˜๋ฉ๋‹ˆ๋‹ค (๋ฐฐ์นญ Breaking).
  • MaterialPropertyBlock์˜ ํ•ด๊ฒฐ์ฑ…:

    • ๋จธํ‹ฐ๋ฆฌ์–ผ ์ž์ฒด๋ฅผ ๋ณต์ œํ•˜์ง€ ์•Š๊ณ , "์ด ์˜ค๋ธŒ์ ํŠธ๋งŒ ์ƒ‰์ƒ์„ ์ด๋ ‡๊ฒŒ ๋ฐ”๊ฟ”์„œ ๊ทธ๋ ค์ค˜"๋ผ๋Š” ์ž„์‹œ ๋ฐ์ดํ„ฐ ๋ธ”๋ก์„ ๋ Œ๋”๋Ÿฌ์— ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค.
    • ๋จธํ‹ฐ๋ฆฌ์–ผ์€ ํ•˜๋‚˜๋งŒ ๊ณต์œ ํ•˜๋ฏ€๋กœ ๋ฉ”๋ชจ๋ฆฌ๊ฐ€ ๋‚ญ๋น„๋˜์ง€ ์•Š๊ณ , GPU ์ž…์žฅ์—์„œ๋„ ์ƒํƒœ ๋ณ€๊ฒฝ์ด ์ตœ์†Œํ™”๋˜์–ด ์„ฑ๋Šฅ์ด ํ›จ์”ฌ ์ข‹์Šต๋‹ˆ๋‹ค.
  • ์ฝ”๋“œ์—์„œ์˜ ์ ์šฉ:


// 1. ๋ธ”๋ก ๋ฐ์ดํ„ฐ ์ƒ์„ฑ (ํ•œ ๋ฒˆ๋งŒ ์ƒ์„ฑํ•ด๋‘๊ณ  ์žฌ์‚ฌ์šฉ)
_propBlock.SetColor("_BaseColor", albedo); 

// 2. ๋ Œ๋”๋Ÿฌ์— ์ „๋‹ฌ (๋จธํ‹ฐ๋ฆฌ์–ผ ์›๋ณธ์€ ๊ฑด๋“œ๋ฆฌ์ง€ ์•Š์Œ)
s.renderer.SetPropertyBlock(_propBlock);

2. Material CreateURPMaterial()

"์„ค์ • ๊ท€์ฐฎ์„ ๋•Œ ์‰์ด๋”๋ฅผ ์•Œ์•„์„œ ์ฐพ์•„์ฃผ๋Š” ์ž๋™ํ™” ํ•จ์ˆ˜"

์ด ๋ฉ”์„œ๋“œ๋Š” ์Šคํฌ๋ฆฝํŠธ๊ฐ€ ์‹คํ–‰๋  ๋•Œ ์ธ์ŠคํŽ™ํ„ฐ์— ๋จธํ‹ฐ๋ฆฌ์–ผ์ด ์—†๋‹ค๋ฉด, URP ํ™˜๊ฒฝ์— ๋งž๋Š” ํ‘œ์ค€ ๋จธํ‹ฐ๋ฆฌ์–ผ์„ ์ฝ”๋“œ๋กœ ์ง์ ‘ ์ƒ์„ฑํ•˜๋Š” ์—ญํ• ์„ ํ•ฉ๋‹ˆ๋‹ค.

  • ์ƒ์„ธ ๊ธฐ๋Šฅ ๋ถ„์„:
  1. Shader.Find("Universal Render Pipeline/Lit"): ์œ ๋‹ˆํ‹ฐ URP ํ”„๋กœ์ ํŠธ์˜ ํ‘œ์ค€ ์‰์ด๋”๋ฅผ ์ฐพ์Šต๋‹ˆ๋‹ค. ์ด๋Š” Unity 6.0 URP ํ™˜๊ฒฝ์—์„œ ๊ฐ€์žฅ ํ˜ธํ™˜์„ฑ์ด ์ข‹์€ ์‰์ด๋”์ž…๋‹ˆ๋‹ค.
  2. new Material(shader): ์ฐพ์€ ์‰์ด๋”๋กœ ์ƒˆ๋กœ์šด ๋จธํ‹ฐ๋ฆฌ์–ผ ์ธ์Šคํ„ด์Šค๋ฅผ ๋งŒ๋“ญ๋‹ˆ๋‹ค.
  3. mat.EnableKeyword("_EMISSION"): ๋จธํ‹ฐ๋ฆฌ์–ผ์˜ ๋ฐœ๊ด‘(Emission) ๊ธฐ๋Šฅ์„ ์ผญ๋‹ˆ๋‹ค. ์ด ํ‚ค์›Œ๋“œ๊ฐ€ ์ผœ์ ธ์•ผ _EmissionColor ํ”„๋กœํผํ‹ฐ๊ฐ€ ์ž‘๋™ํ•ฉ๋‹ˆ๋‹ค.
  4. ๋ฐ˜ํ™˜: ์ƒ์„ฑ๋œ ๋จธํ‹ฐ๋ฆฌ์–ผ์„ baseMaterial ๋ณ€์ˆ˜์— ํ• ๋‹นํ•ด ์ค๋‹ˆ๋‹ค.
  • ์‚ฌ์šฉ ์ด์œ :
    ์‚ฌ์šฉ์ž๊ฐ€ ๋งค๋ฒˆ ํ”„๋กœ์ ํŠธ ์ฐฝ์—์„œ ๋จธํ‹ฐ๋ฆฌ์–ผ์„ ๋งŒ๋“ค๊ณ  ์‰์ด๋”๋ฅผ ์„ค์ •ํ•˜๊ณ , Emission์„ ์ผœ๋Š” ๋ฒˆ๊ฑฐ๋กœ์›€์„ ์—†์• ๊ธฐ ์œ„ํ•จ์ž…๋‹ˆ๋‹ค. ํ”„๋ฆฌํŒน(Prefab)๋งŒ ๋ฐฐ์น˜ํ•ด๋„ ์•Œ์•„์„œ ์ œ๋Œ€๋กœ ๋œ ๋จธํ‹ฐ๋ฆฌ์–ผ์ด ์ ์šฉ๋ฉ๋‹ˆ๋‹ค.

3. MaterialGlobalIlluminationFlags

"๋น›์„ ๋‚ด๋ฟœ๋Š” ๋…€์„์ด ์ฃผ๋ณ€๋„ ๋ฐํžˆ๊ฒŒ ๋งŒ๋“œ๋Š” ์„ค์ •"

์ด ์†์„ฑ์€ ๋จธํ‹ฐ๋ฆฌ์–ผ์ด ๊ธ€๋กœ๋ฒŒ ์ผ๋ฃจ๋ฏธ๋„ค์ด์…˜(Global Illumination, GI) ์‹œ์Šคํ…œ๊ณผ ์–ด๋–ป๊ฒŒ ์ƒํ˜ธ์ž‘์šฉํ• ์ง€๋ฅผ ๊ฒฐ์ •ํ•ฉ๋‹ˆ๋‹ค.

  • ์„ค์ •๊ฐ’ RealtimeEmissive์˜ ์˜๋ฏธ:

    • ์ด ๋จธํ‹ฐ๋ฆฌ์–ผ์€ ์‹ค์‹œ๊ฐ„(Realtime)์œผ๋กœ ๋น›์„ ๋‚ด๋ฟœ๋Š”๋‹ค๋Š” ๋œป์ž…๋‹ˆ๋‹ค.
    • ๋‹จ์ˆœํžˆ ์˜ค๋ธŒ์ ํŠธ ์ž์ฒด๊ฐ€ ๋น›๋‚˜๋Š” ๊ฒƒ์— ๊ทธ์น˜์ง€ ์•Š๊ณ , ์ฃผ๋ณ€ ๋ฒฝ์ด๋‚˜ ๋ฐ”๋‹ฅ, ๋‹ค๋ฅธ ์˜ค๋ธŒ์ ํŠธ๋“ค์—๊ฒŒ๋„ ๋น›์„ ๋น„์ถ”๋Š” ์˜ํ–ฅ์„ ์ค๋‹ˆ๋‹ค. (์˜ˆ: ์—๋„ˆ์ง€ ํ•„๋“œ ์•„๋ž˜ ๋ฐ”๋‹ฅ์ด ํ‘ธ๋ฅด๊ฒŒ ๋ฌผ๋“œ๋Š” ํšจ๊ณผ)
  • ์ฝ”๋“œ์—์„œ์˜ ์—ญํ• :

mat.globalIlluminationFlags = MaterialGlobalIlluminationFlags.RealtimeEmissive;

์ด ์ฝ”๋“œ๊ฐ€ ์—†๋‹ค๋ฉด ๊ตฌ์ฒด๋Š” ์Šค์Šค๋กœ ๋น›๋‚˜ ๋ณด์ผ ๋ฟ, ์ฃผ๋ณ€ ํ™˜๊ฒฝ์—๋Š” ์•„๋ฌด๋Ÿฐ ๋น›์˜ ์˜ํ–ฅ์„ ์ฃผ์ง€ ์•Š๋Š” '์ž์ฒด ๋ฐœ๊ด‘(Self-Illuminated)' ์ƒํƒœ๋กœ๋งŒ ๋‚จ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ์ด ์ฝ”๋“œ๋ฅผ ํ†ตํ•ด ์—๋„ˆ์ง€ ํ•„๋“œ๊ฐ€ ์‹ค์ œ ๊ด‘์›์ฒ˜๋Ÿผ ํ–‰๋™ํ•˜๋„๋ก ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.

  • ์„ฑ๋Šฅ ๊ด€๋ จ ์ฐธ๊ณ ํ•  ์‚ฌํ•ญ:
    • ์‹ค์‹œ๊ฐ„ GI๋Š” ๊ณ„์‚ฐ ๋น„์šฉ์ด ๋น„์Œ‰๋‹ˆ๋‹ค. ๋งŒ์•ฝ ์ˆ˜๋ฐฑ ๊ฐœ์˜ ๊ตฌ์ฒด๊ฐ€ ๋ชจ๋‘ ์ฃผ๋ณ€์— ๋น›์„ ๋น„์ถ˜๋‹ค๋ฉด ์„ฑ๋Šฅ์— ๋ถ€๋‹ด์ด ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
    • ์„ฑ๋Šฅ์ด ์ค‘์š”ํ•œ ๋ชจ๋ฐ”์ผ์ด๋‚˜ ์ €์‚ฌ์–‘ ํ™˜๊ฒฝ์—์„œ๋Š” None์œผ๋กœ ์„ค์ •ํ•˜์—ฌ ์ฃผ๋ณ€ ์กฐ๋ช… ์˜ํ–ฅ์„ ๋„๊ฑฐ๋‚˜, Light Probe๋ฅผ ํ™œ์šฉํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ์ตœ์ ํ™”ํ•˜๊ธฐ๋„ ํ•ฉ๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ PC/์ฝ˜์†” ๊ธ‰์˜ ๋น„์ฃผ์–ผ ํ€„๋ฆฌํ‹ฐ๋ฅผ ์œ„ํ•ด์„œ๋Š” RealtimeEmissive๊ฐ€ ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

์ฝ”๋“œ

๐Ÿ“„Art_004_Radial_Energy_Field.cs

using UnityEngine;
using System.Collections.Generic;

#if UNITY_EDITOR
using UnityEditor;
#endif

[ExecuteAlways]
public class RadialEnergyField : MonoBehaviour
{
    [Header("๊ทธ๋ฆฌ๋“œ ๊ตฌ์กฐ (๋ณ€๊ฒฝ ์‹œ ์˜ค๋ธŒ์ ํŠธ ์žฌ์ƒ์„ฑ)")]
    public int ringCount = 15;
    public int baseSegments = 6;
    public float ringSpacing = 1.2f;

    [Header("์—๋„ˆ์ง€ ํŒŒ๋™ ์„ค์ •")]
    public float waveHeight = 2.0f;
    public float waveSpeed = 1.4f;
    public float waveFrequency = 1.5f;

    [Header("ํšŒ์ „ ํ•„๋“œ ์„ค์ •")]
    public float swirlStrength = 0.4f;
    public float radialPulse = 0.6f;

    [Header("ํฌ๊ธฐ ์„ค์ •")]
    public float scaleMin = 0.15f;
    public float scaleMax = 0.9f;

    [Header("์ƒ‰์ƒ ์„ค์ • (์ง์ ‘ ์ˆ˜์ • ๊ฐ€๋Šฅ)")]
    [Tooltip("ํŒŒ๋™์˜ ์ตœ์†Œ ์ƒ‰์ƒ (์•ŒํŒŒ ํฌํ•จ)")]
    public Color colorMin = new Color(0.0f, 0.5f, 1.0f, 1.0f); 
    [Tooltip("ํŒŒ๋™์˜ ์ตœ๋Œ€ ์ƒ‰์ƒ (์•ŒํŒŒ ํฌํ•จ)")]
    public Color colorMax = new Color(1.0f, 1.0f, 1.0f, 1.0f); 
    [Tooltip("๋ฐœ๊ด‘ ๊ฐ•๋„")]
    public float emissionIntensity = 1f;

    [Header("์žฌ์งˆ ์„ค์ • (URP)")]
    [Tooltip("๋น„์›Œ๋‘๋ฉด ์ž๋™์œผ๋กœ URP Lit ๋จธํ‹ฐ๋ฆฌ์–ผ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.")]
    public Material baseMaterial;

    // ๋‚ด๋ถ€ ์บ์‹ฑ ๋ณ€์ˆ˜
    private int _cachedRingCount;
    private int _cachedBaseSegments;
    private float _cachedRingSpacing;

    struct SphereData
    {
        public Transform tr;
        public Renderer renderer;
        public float radius;
        public float angle;
        public Vector3 basePos;
    }

    List<SphereData> spheres = new List<SphereData>();
    MaterialPropertyBlock _propBlock;

#if UNITY_EDITOR
    bool _generateQueued = false;
#endif

    void OnEnable()
    {
        transform.position = Vector3.zero;
        transform.rotation = Quaternion.identity;

        if (baseMaterial == null)
            baseMaterial = CreateURPMaterial();

        _propBlock = new MaterialPropertyBlock();
        Generate();
        CacheStructureParams();
    }

    void OnValidate()
    {
#if UNITY_EDITOR
        if (baseMaterial == null)
            baseMaterial = CreateURPMaterial();

        if (StructureParamsChanged())
        {
            QueueGenerate();
            CacheStructureParams();
        }
        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
        if (!Application.isPlaying) return;
#endif
        ApplyAnimation(Time.time * waveSpeed);
    }

    bool StructureParamsChanged()
    {
        return ringCount != _cachedRingCount
            || baseSegments != _cachedBaseSegments
            || ringSpacing != _cachedRingSpacing;
    }

    void CacheStructureParams()
    {
        _cachedRingCount = ringCount;
        _cachedBaseSegments = baseSegments;
        _cachedRingSpacing = ringSpacing;
    }

    // ---------------------------------------------------
    // ์ƒ์„ฑ ๋กœ์ง
    // ---------------------------------------------------

    void Generate()
    {
        Clear();
        SpawnSphere(Vector3.zero, 0, 0);

        for (int ring = 1; ring <= ringCount; ring++)
        {
            float radius = ring * ringSpacing;
            int segments = baseSegments * ring;

            for (int s = 0; s < segments; s++)
            {
                float angle = (float)s / segments * Mathf.PI * 2f;
                Vector3 pos = new Vector3(Mathf.Cos(angle) * radius, 0, Mathf.Sin(angle) * radius);
                SpawnSphere(pos, radius, angle);
            }
        }
        ApplyAnimation(0f);
    }

    void SpawnSphere(Vector3 pos, float radius, float angle)
    {
        GameObject go = GameObject.CreatePrimitive(PrimitiveType.Sphere);

        // ---------------------------------------------------
        // ์ฝœ๋ผ์ด๋” ๋ฐ ๋ฆฌ์ง€๋“œ๋ฐ”๋”” ์ถ”๊ฐ€ ๋กœ์ง
        // ---------------------------------------------------
        
        // 1. Collider: CreatePrimitive๊ฐ€ ์ž๋™์œผ๋กœ ์ƒ์„ฑํ•˜๋ฏ€๋กœ ์‚ญ์ œํ•˜์ง€ ์•Š์Œ.
        // ํ•„์š”ํ•˜๋‹ค๋ฉด ํŠธ๋ฆฌ๊ฑฐ ์„ค์ • (์˜ˆ: ํ”Œ๋ ˆ์ด์–ด๊ฐ€ ํ†ต๊ณผํ•  ๋•Œ ์ด๋ฒคํŠธ์šฉ)
        // SphereCollider col = go.GetComponent<SphereCollider>();
        // col.isTrigger = true; 

        // 2. Rigidbody ์ถ”๊ฐ€
        Rigidbody rb = go.AddComponent<Rigidbody>();
        rb.isKinematic = true; // ์Šคํฌ๋ฆฝํŠธ๊ฐ€ ์œ„์น˜๋ฅผ ์ œ์–ดํ•˜๋ฏ€๋กœ ๋ฌผ๋ฆฌ์  ํž˜์€ ๋ฐ›์ง€ ์•Š๋„๋ก ์„ค์ • (์ค‘์š”)
        rb.useGravity = false; // ์ค‘๋ ฅ ๊บผ๋‘๊ธฐ

        go.transform.SetParent(transform);
        go.transform.localPosition = pos;
        go.transform.localScale = Vector3.one * scaleMin;

        Renderer renderer = go.GetComponent<Renderer>();
        renderer.sharedMaterial = baseMaterial;

        spheres.Add(new SphereData
        {
            tr = go.transform,
            renderer = renderer,
            radius = radius,
            angle = angle,
            basePos = pos
        });
    }

    // ---------------------------------------------------
    // ์• ๋‹ˆ๋ฉ”์ด์…˜ ๋ฐ ์ƒ‰์ƒ ์ ์šฉ
    // ---------------------------------------------------

    void ApplyAnimation(float t)
    {
        if (_propBlock == null) _propBlock = new MaterialPropertyBlock();

        for (int i = 0; i < spheres.Count; i++)
        {
            SphereData s = spheres[i];
            if (s.tr == null || s.renderer == null) continue;

            float wave = Mathf.Sin(s.radius * waveFrequency - t) * waveHeight;
            float normalized = (wave / waveHeight + 1f) * 0.5f;

            float swirl = Mathf.Sin(t + s.radius) * swirlStrength;
            float angle = s.angle + swirl;
            float r = s.radius + Mathf.Sin(t + s.radius) * radialPulse;

            Vector3 p = new Vector3(Mathf.Cos(angle) * r, wave, Mathf.Sin(angle) * r);

            s.tr.localPosition = p;

            float scale = Mathf.Lerp(scaleMin, scaleMax, normalized);
            s.tr.localScale = Vector3.one * scale;

            Color albedo = Color.Lerp(colorMin, colorMax, normalized);
            Color emission = albedo * emissionIntensity;

            _propBlock.SetColor("_BaseColor", albedo);
            _propBlock.SetColor("_EmissionColor", emission);
            s.renderer.SetPropertyBlock(_propBlock);
        }
    }

    Material CreateURPMaterial()
    {
        Shader shader = Shader.Find("Universal Render Pipeline/Lit");
        if (shader == null)
        {
            Debug.LogError("URP Lit Shader๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.");
            return new Material(Shader.Find("Standard"));
        }

        Material mat = new Material(shader);
        mat.EnableKeyword("_EMISSION");
        mat.globalIlluminationFlags = MaterialGlobalIlluminationFlags.RealtimeEmissive;
        return mat;
    }

    void Clear()
    {
        foreach (Transform child in transform)
        {
            if (Application.isPlaying)
                Destroy(child.gameObject);
            else
                DestroyImmediate(child.gameObject);
        }
        spheres.Clear();
    }
}

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

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