๐ŸซงArt_012 Swirl Artwork

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

Unity GenArt

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

๋ธ”๋ Œ๋”์—์„œ ๊ตฌํ˜„ํ•œ ์œ ์‚ฌ ์•„ํŠธ์›Œํฌ

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

ํŒŒ์ผ ๋ฐฐ์น˜:

SwirlUnlit.shader โ†’ Assets/Shaders/
SwirlArtwork.cs โ†’ Assets/Scripts/
์”ฌ์˜ ๋นˆ GameObject์— SwirlArtwork ์ปดํฌ๋„ŒํŠธ ์ถ”๊ฐ€

Code

๐Ÿ“„SwirlArtwork.cs

// SwirlArtwork.cs
// ๋‚˜์„ ํ˜• ์Šค์›œ ํŒŒํ‹ฐํด ์•„ํŠธ์›. [ExecuteAlways]๋กœ ์—๋””ํ„ฐ์—์„œ๋„ ํ”„๋ฆฌ๋ทฐ ํ‘œ์‹œ.
// Assets/Scripts/ ํด๋”์— ๋ฐฐ์น˜ ํ›„ ์”ฌ์˜ ๋นˆ GameObject์— ์ปดํฌ๋„ŒํŠธ ์ถ”๊ฐ€.
// ์š”๊ตฌ์‚ฌํ•ญ: Unity 6.0 + URP, Custom/SwirlUnlit ์…ฐ์ด๋” ํ•„์š”.

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.InputSystem;
#if UNITY_EDITOR
using UnityEditor;
#endif

[ExecuteAlways]
[AddComponentMenu("GenArt/Swirl Artwork")]
public class SwirlArtwork : MonoBehaviour
{
  // === Spawn & Motion ===

  [Header("Spawn & Motion")]
  [Tooltip("ํŒŒํ‹ฐํด ์ตœ๋Œ€ ๊ฐœ์ˆ˜. ์ดˆ๊ณผ์‹œ ๊ฐ€์žฅ ์˜ค๋ž˜๋œ ํŒŒํ‹ฐํด ํŒŒ๊ดด.")]
  [Range(10, 2000)] public int maxCount = 1000;

  [Tooltip("ํŒŒํ‹ฐํด์ด ํŒŒ๊ดด๋˜๋Š” ์ตœ๋Œ€ ๋†’์ด (Y์ถ•).")]
  [Range(1f, 30f)] public float maxHeight = 10f;

  [Tooltip("์ดˆ๋‹น ํŒŒํ‹ฐํด ์ƒ์„ฑ ๊ฐœ์ˆ˜.")]
  [Range(1f, 60f)] public float spawnRate = 15f;

  [Tooltip("ํŒŒํ‹ฐํด ์ตœ์†Œ ์ƒ์Šน ์†๋„ (์œ ๋‹›/์ดˆ).")]
  [Range(0.02f, 3f)] public float minSpeed = 0.1f;

  [Tooltip("ํŒŒํ‹ฐํด ์ตœ๋Œ€ ์ƒ์Šน ์†๋„ (์œ ๋‹›/์ดˆ).")]
  [Range(0.02f, 5f)] public float maxSpeed = 0.6f;

  [Tooltip("ํŒŒํ‹ฐํด ์ตœ์†Œ ํฌ๊ธฐ.")]
  [Range(0.01f, 0.3f)] public float minSize = 0.04f;

  [Tooltip("ํŒŒํ‹ฐํด ์ตœ๋Œ€ ํฌ๊ธฐ.")]
  [Range(0.05f, 0.8f)] public float maxSize = 0.18f;

  // === Spiral Shape ===

  [Header("Spiral Shape")]
  [Tooltip("๋‚˜์„ ์˜ ๋ฐ˜์ง€๋ฆ„.")]
  [Range(0.1f, 6f)] public float spiralRadius = 1.2f;

  [Tooltip("maxHeight๊นŒ์ง€ ๊ฐ๊ธฐ๋Š” ํšŒ์ „ ์ˆ˜.")]
  [Range(1f, 12f)] public float spiralTurns = 4f;

  [Tooltip("๋‚˜์„  ๊ฒฝ๋กœ๋กœ๋ถ€ํ„ฐ ๋žœ๋ค ์ด๊ฒฉ ๋ฒ”์œ„ (spiralRadius์˜ ๋ฐฐ์œจ).")]
  [Range(0f, 1f)] public float jitterRange = 0.25f;

  // === Color ===

  [Header("Color")]
  [Tooltip("๋†’์ด 0 (๋ฐ”๋‹ฅ) ์˜ ์ƒ‰์ƒ.")]
  public Color bottomColor = new Color(1f, 0.35f, 0.65f); // pink
  [Tooltip("๋†’์ด maxHeight (๊ผญ๋Œ€๊ธฐ) ์˜ ์ƒ‰์ƒ.")]
  public Color topColor = new Color(0.25f, 0.45f, 1f);    // blue

  // === Mouse Repulsion ===

  [Header("Mouse Repulsion")]
  [Tooltip("๋งˆ์šฐ์Šค ๋ฐ˜๋ฐœ ์˜ํ–ฅ ๋ฐ˜๊ฒฝ (ํ”ฝ์…€ ๋‹จ์œ„). ํ™”๋ฉด ํ•ด์ƒ๋„ ๊ธฐ์ค€์œผ๋กœ ์ง๊ด€์ ์œผ๋กœ ์กฐ์ ˆ.")]
  [Range(20f, 600f)] public float repulsionRadius = 150f;

  [Tooltip("๋ฐ˜๋ฐœ๋ ฅ ์ตœ๋Œ€ ์„ธ๊ธฐ.")]
  [Range(1f, 60f)] public float repulsionStrength = 14f;

  [Tooltip("๊ฐ€์šฐ์‹œ์•ˆ ๋ถ„ํฌ์˜ ์‹œ๊ทธ๋งˆ ๊ฐ’ (ํ”ฝ์…€). ํด์ˆ˜๋ก ๋ฐ˜๋ฐœ์ด ๋„“์€ ๋ฒ”์œ„์—์„œ ๋ถ€๋“œ๋Ÿฝ๊ฒŒ ํผ์ง.")]
  [Range(10f, 400f)] public float gaussianSigma = 60f;

  [Tooltip("๋‚˜์„  ๊ฒฝ๋กœ๋กœ ๋ณต์›ํ•˜๋Š” ์Šคํ”„๋ง ๊ฐ•๋„.")]
  [Range(1f, 100f)] public float springStrength = 28f;

  [Tooltip("์†๋„ ๊ฐ์‡  ์ƒ์ˆ˜ (์ง€์ˆ˜ ๊ฐ์‡ ). ํด์ˆ˜๋ก ๋น ๋ฅด๊ฒŒ ์ •์ง€.")]
  [Range(0.1f, 20f)] public float dampingDecay = 5f;

  // ----- ๋‚ด๋ถ€ ๊ตฌ์กฐ์ฒด -----

  private struct Particle
  {
    public Vector3 basePos;       // ๋‚˜์„  ๊ฒฝ๋กœ ์œ„์˜ ๋ชฉํ‘œ ์œ„์น˜
    public Vector3 pos;           // ์‹ค์ œ ํ˜„์žฌ ์œ„์น˜ (๋ฐ˜๋ฐœ๋กœ ์ดํƒˆ ๊ฐ€๋Šฅ)
    public Vector3 vel;           // ํ˜„์žฌ ์†๋„
    public float t;             // 0~1 (๋†’์ด ์ง„ํ–‰๋„)
    public float speed;         // t ๋‹จ์œ„ ์†๋„ (= worldSpeed / maxHeight)
    public float size;          // ์Šคํ”ผ์–ด ์Šค์ผ€์ผ
    public float radialJitter;  // ๋ฐ˜์ง€๋ฆ„ ๊ณ ์ • ์˜คํ”„์…‹ (์Šคํฐ์‹œ ๊ฒฐ์ •)
    public float angularJitter; // ๊ฐ๋„ ๊ณ ์ • ์˜คํ”„์…‹ (์Šคํฐ์‹œ ๊ฒฐ์ •)
  }

  // === ๋Ÿฐํƒ€์ž„ ์ƒํƒœ ===

  private readonly List<Particle> _p = new();
  private float _spawnTimer;
  private Vector2 _mouseScreen;

  private Mesh _mesh;
  private Material _mat;
  private MaterialPropertyBlock _mpb;

  // DrawMeshInstanced ๋ฐฐ์น˜ ์ตœ๋Œ€์น˜ = 1023
  private const int BATCH = 1023;
  private readonly Matrix4x4[] _mBuf = new Matrix4x4[BATCH];
  private readonly Vector4[] _cBuf = new Vector4[BATCH];

  // === Unity ๋ผ์ดํ”„์‚ฌ์ดํด ===

  void OnEnable()
  {
    _p.Clear();
    _spawnTimer = 0f;
    InitResources();

    if (!Application.isPlaying)
      BuildEditorPreview();

#if UNITY_EDITOR
        SceneView.RepaintAll();
#endif
  }

  void OnDisable()
  {
    CoreUtils.Destroy(_mat);
    _mat = null;
  }

  void Update()
  {
    if (_mat == null || _mesh == null) return;

    if (Application.isPlaying)
    {
      float dt = Time.deltaTime;
      Spawn(dt);
      var _mouse = Mouse.current;
      _mouseScreen = _mouse != null ? _mouse.position.ReadValue() : Vector2.zero;
      Simulate(dt);
    }

    Render();

#if UNITY_EDITOR
        if (!Application.isPlaying)
            SceneView.RepaintAll();
#endif
  }

  // === ๋ฆฌ์†Œ์Šค ์ดˆ๊ธฐํ™” ===

  // ๋ฉ”์‹œยท๋จธํ‹ฐ๋ฆฌ์–ผ ์ƒ์„ฑ (์—†์„ ๋•Œ๋งŒ ์‹คํ–‰)
  void InitResources()
  {
    if (_mesh == null)
    {
      // Resources API๋กœ ๋‚ด์žฅ ๊ตฌ์ฒด ๋ฉ”์‹œ ์ง์ ‘ ์ฐธ์กฐ (GameObject ์ƒ์„ฑ ๋ถˆํ•„์š”)
      _mesh = Resources.GetBuiltinResource<Mesh>("Sphere.fbx");
    }

    if (_mat == null)
    {
      var sh = Shader.Find("Custom/SwirlUnlit");
      if (sh == null)
      {
        Debug.LogError("[SwirlArtwork] ์…ฐ์ด๋” 'Custom/SwirlUnlit'๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. " +
                       "Assets/Shaders/SwirlUnlit.shader ๊ฐ€ ํ”„๋กœ์ ํŠธ์— ์žˆ๋Š”์ง€ ํ™•์ธํ•˜์„ธ์š”.");
        return;
      }
      _mat = new Material(sh) { enableInstancing = true };
    }

    _mpb ??= new MaterialPropertyBlock();
  }

  // === ์—๋””ํ„ฐ ํ”„๋ฆฌ๋ทฐ ===

  // ์žฌ์ƒ ์ •์ง€ ์ƒํƒœ์—์„œ ๋‚˜์„  ๋ถ„ํฌ ์Šค๋ƒ…์ƒท ์ƒ์„ฑ
  void BuildEditorPreview()
  {
    _p.Clear();
    // ์ตœ๋Œ€ 600๊ฐœ๋กœ ์ œํ•œํ•ด ์—๋””ํ„ฐ ๋ถ€ํ•˜ ์™„ํ™”
    int count = Mathf.Min(maxCount, 600);
    for (int i = 0; i < count; i++)
    {
      float t = (float)i / count;
      _p.Add(MakeParticle(t));
    }
  }

  // === ํŒŒํ‹ฐํด ์ƒ์„ฑ ===

  // t (0~1) ์œ„์น˜์— ํŒŒํ‹ฐํด ํ•˜๋‚˜ ์ƒ์„ฑ
  Particle MakeParticle(float t)
  {
    var p = new Particle
    {
      t = t,
      speed = Random.Range(minSpeed, maxSpeed) / Mathf.Max(maxHeight, 0.001f),
      size = Random.Range(minSize, maxSize),
      radialJitter = Random.Range(-jitterRange, jitterRange) * spiralRadius,
      angularJitter = Random.Range(-jitterRange, jitterRange) * Mathf.PI
    };
    p.basePos = SpiralPos(p);
    p.pos = p.basePos;
    p.vel = Vector3.zero;
    return p;
  }

  // tยทjitter ๊ฐ’์œผ๋กœ ๋‚˜์„  ๊ธฐ์ค€ ์œ„์น˜ ๊ณ„์‚ฐ
  Vector3 SpiralPos(in Particle p)
  {
    float y = p.t * maxHeight;
    float angle = p.t * spiralTurns * Mathf.PI * 2f + p.angularJitter;
    float radius = spiralRadius + p.radialJitter;
    return new Vector3(
        radius * Mathf.Sin(angle),
        y,
        radius * Mathf.Cos(angle)
    );
  }

  // === ์Šคํฐ ===

  void Spawn(float dt)
  {
    if (_p.Count >= maxCount) return;

    _spawnTimer += dt;
    float interval = 1f / Mathf.Max(spawnRate, 0.001f);

    while (_spawnTimer >= interval && _p.Count < maxCount)
    {
      _spawnTimer -= interval;
      _p.Add(MakeParticle(0f));
    }
    // ๋ˆ„์  ๋ฐฉ์ง€: ํ”„๋ ˆ์ž„ ๋“œ๋กญ์œผ๋กœ ํƒ€์ด๋จธ๊ฐ€ ํญ๋ฐœํ•˜๋ฉด ๋ฆฌ์…‹
    if (_spawnTimer > interval * 4f)
      _spawnTimer = 0f;
  }

  // === ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ===

  void Simulate(float dt)
  {
    // ํ”„๋ ˆ์ž„๋ ˆ์ดํŠธ ๋…๋ฆฝ ์ง€์ˆ˜ ๊ฐ์‡ : vel *= exp(-decay * dt)
    float dampFactor = Mathf.Exp(-dampingDecay * dt);

    var cam = Camera.main;
    bool hasCam = cam != null;

    for (int i = _p.Count - 1; i >= 0; i--)
    {
      var p = _p[i];

      // ๋†’์ด ์ง„ํ–‰
      p.t += p.speed * dt;

      // ๋†’์ด ์ดˆ๊ณผ ๋˜๋Š” maxCount ์ดˆ๊ณผ์‹œ ํŒŒ๊ดด
      if (p.t >= 1f || _p.Count > maxCount)
      {
        _p.RemoveAt(i);
        continue;
      }

      // ํ˜„์žฌ t์— ๋งž๋Š” ๋‚˜์„  ๊ธฐ์ค€ ์œ„์น˜ ๊ฐฑ์‹ 
      p.basePos = SpiralPos(p);

      // ๋‚˜์„  ๊ฒฝ๋กœ ๋ณต์› ์Šคํ”„๋ง ํž˜
      p.vel += (p.basePos - p.pos) * (springStrength * dt);

      // ๋งˆ์šฐ์Šค ๋ฐ˜๋ฐœ๋ ฅ: ์Šคํฌ๋ฆฐ ํ”ฝ์…€ ๊ฑฐ๋ฆฌ ๊ธฐ๋ฐ˜ (์นด๋ฉ”๋ผ ๊ฐ๋„ ๋…๋ฆฝ)
      if (hasCam)
      {
        Vector3 sp = cam.WorldToScreenPoint(p.pos);
        if (sp.z > 0f) // ์นด๋ฉ”๋ผ ์•ž์— ์žˆ๋Š” ํŒŒํ‹ฐํด๋งŒ ์ฒ˜๋ฆฌ
        {
          float screenDist = Vector2.Distance(new Vector2(sp.x, sp.y), _mouseScreen);
          if (screenDist < repulsionRadius && screenDist > 0.5f)
          {
            float g = Mathf.Exp(-(screenDist * screenDist)
                                / (2f * gaussianSigma * gaussianSigma));
            // ํŒŒํ‹ฐํด ๊นŠ์ด ๊ธฐ์ค€์œผ๋กœ ๋งˆ์šฐ์Šค๋ฅผ ์›”๋“œ ์—ญํˆฌ์˜ โ†’ ์ •ํ™•ํ•œ 3D ๋ฐ˜๋ฐœ ๋ฐฉํ–ฅ
            Vector3 mWorld = cam.ScreenToWorldPoint(
                                 new Vector3(_mouseScreen.x, _mouseScreen.y, sp.z));
            Vector3 away = (p.pos - mWorld).normalized;
            p.vel += away * (repulsionStrength * g * dt);
          }
        }
      }

      // ๊ฐ์‡  ์ ์šฉ ํ›„ ์œ„์น˜ ์ ๋ถ„
      p.vel *= dampFactor;
      p.pos += p.vel * dt;

      _p[i] = p;
    }
  }

  // === ๋ Œ๋”๋ง ===

  // 1023 ๋‹จ์œ„ ๋ฐฐ์น˜๋กœ ๋‚˜๋ˆ  DrawMeshInstanced ํ˜ธ์ถœ
  void Render()
  {
    if (_mat == null || _mesh == null || _p.Count == 0) return;

    int total = _p.Count;
    int offset = 0;

    while (offset < total)
    {
      int n = Mathf.Min(BATCH, total - offset);

      for (int i = 0; i < n; i++)
      {
        var p = _p[offset + i];

        // TRS ๋งคํŠธ๋ฆญ์Šค: ์œ„์น˜ + ๊ท ๋“ฑ ์Šค์ผ€์ผ (ํšŒ์ „ ๋ถˆํ•„์š”)
        _mBuf[i] = Matrix4x4.TRS(p.pos, Quaternion.identity, Vector3.one * p.size);

        // ๋†’์ด ๊ธฐ๋ฐ˜ ์ƒ‰์ƒ lerp: 0 โ†’ bottomColor, maxHeight โ†’ topColor
        float tc = Mathf.Clamp01(p.pos.y / Mathf.Max(maxHeight, 0.001f));
        Color c = Color.Lerp(bottomColor, topColor, tc);
        _cBuf[i] = new Vector4(c.r, c.g, c.b, c.a);
      }

      _mpb.Clear();
      _mpb.SetVectorArray("_BaseColor", _cBuf);
      Graphics.DrawMeshInstanced(_mesh, 0, _mat, _mBuf, n, _mpb);

      offset += n;
    }
  }

  // SampleMouseWorld ์ œ๊ฑฐ โ€” Simulate() ๋‚ด๋ถ€์—์„œ Mouse.current ์ง์ ‘ ์ฒ˜๋ฆฌ

  // === ์—๋””ํ„ฐ ํ›… ===

#if UNITY_EDITOR
    // ์ธ์ŠคํŽ™ํ„ฐ ๊ฐ’ ๋ณ€๊ฒฝ์‹œ ์—๋””ํ„ฐ ํ”„๋ฆฌ๋ทฐ ์žฌ์ƒ์„ฑ
    void OnValidate()
    {
        if (Application.isPlaying) return;
        // OnValidate๋Š” ์ง๋ ฌํ™” ๋„์ค‘ ํ˜ธ์ถœ๋  ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ null ์ฒดํฌ ํ•„์ˆ˜
        if (_mesh == null || _mat == null)
            InitResources();
        if (_mesh != null && _mat != null)
        {
            BuildEditorPreview();
            SceneView.RepaintAll();
        }
    }
#endif
}

๐Ÿ“„SwirlUnlit.shader

// SwirlUnlit.shader
// URP ์–ธ๋ฆฟ ์…ฐ์ด๋”. UNITY_INSTANCING_BUFFER๋กœ ์ธ์Šคํ„ด์Šค๋ณ„ _BaseColor ์ง€์›.
// Assets/Shaders/ ํด๋”์— ๋ฐฐ์น˜.

Shader "Custom/SwirlUnlit"
{
    Properties
    {
        _BaseColor ("Base Color", Color) = (1, 1, 1, 1)
    }

    SubShader
    {
        Tags
        {
            "RenderType"      = "Opaque"
            "RenderPipeline"  = "UniversalPipeline"
            "Queue"           = "Geometry"
        }

        Pass
        {
            Name "SwirlForward"
            Tags { "LightMode" = "UniversalForward" }

            HLSLPROGRAM
            #pragma vertex   Vert
            #pragma fragment Frag
            #pragma multi_compile_instancing
            #pragma instancing_options assumeuniformscaling

            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

            // === Per-Instance Property Buffer ===
            UNITY_INSTANCING_BUFFER_START(UnityPerMaterial)
                UNITY_DEFINE_INSTANCED_PROP(float4, _BaseColor)
            UNITY_INSTANCING_BUFFER_END(UnityPerMaterial)

            struct Attributes
            {
                float4 posOS : POSITION;
                UNITY_VERTEX_INPUT_INSTANCE_ID
            };

            struct Varyings
            {
                float4 posHCS : SV_POSITION;
                float4 color  : COLOR0;
                UNITY_VERTEX_INPUT_INSTANCE_ID
            };

            Varyings Vert(Attributes IN)
            {
                UNITY_SETUP_INSTANCE_ID(IN);
                Varyings OUT;
                UNITY_TRANSFER_INSTANCE_ID(IN, OUT);

                OUT.posHCS = TransformObjectToHClip(IN.posOS.xyz);
                OUT.color  = UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial, _BaseColor);
                return OUT;
            }

            half4 Frag(Varyings IN) : SV_Target
            {
                UNITY_SETUP_INSTANCE_ID(IN);
                return half4(IN.color);
            }

            ENDHLSL
        }
    }
}

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

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