๐ŸซงArt_014 Wave Flow Generator

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

Unity GenArt

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

Code

๐Ÿ“„ Art_014_Wave_Flow_Generator.cs

using UnityEngine;
using UnityEngine.Rendering;

#if UNITY_EDITOR
using UnityEditor;
#endif

// ============================================================
// WaveFlowGenerator
// ์—ฌ๋Ÿฌ ๊ฐœ์˜ ๋ฆฌ๋ณธ ๋ (Band)๊ฐ€ ํŒŒ๋™์น˜๋ฉฐ ํ๋ฅด๋Š” ์ œ๋„ˆ๋ ˆ์ดํ‹ฐ๋ธŒ ์•„ํŠธ์›Œํฌ.
// ์ด ์˜ค๋ธŒ์ ํŠธ ์ˆ˜ = bandCount x segmentCount
// [ExecuteAlways] ๋กœ ์—๋””ํ„ฐ Scene ๋ทฐ์—์„œ๋„ ๋ฏธ๋ฆฌ๋ณด๊ธฐ๊ฐ€ ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค.
// ============================================================
[ExecuteAlways]
public class WaveFlowGenerator : MonoBehaviour
{
  // -----------------------------------------------------------
  // Inspector ํŒŒ๋ผ๋ฏธํ„ฐ
  // -----------------------------------------------------------

  [Header("๊ตฌ์กฐ ์„ค์ • (๋ณ€๊ฒฝ ์‹œ ์žฌ์ƒ์„ฑ)")]
  public int bandCount = 40;    // ๋ฆฌ๋ณธ ๋  ๊ฐœ์ˆ˜
  public int segmentCount = 30;   // ๋  ๋‹น ์ฟผ๋“œ ์„ธ๊ทธ๋จผํŠธ ์ˆ˜
  public float bandSpread = 5f;    // ๋ฐด๋“œ ์ „์ฒด X์ถ• ํŽผ์นจ ๋„ˆ๋น„
  public float flowLength = 10f;   // ํ๋ฆ„ ๋ฐฉํ–ฅ(Y์ถ•) ์ „์ฒด ๊ธธ์ด
  public float segmentWidth = 0.07f; // ์ฟผ๋“œ ํ•˜๋‚˜์˜ ๋„ˆ๋น„ (๋ฆฌ๋ณธ ๋‘๊ป˜)

  [Header("์›จ์ด๋ธŒ ํ˜•ํƒœ")]
  public float waveAmp1 = 0.15f;    // 1์ฐจ ์›จ์ด๋ธŒ ์ง„ํญ (์ฃผ์š” ๊ตด๊ณก)
  public float waveFreq1 = 0.5f;    // 1์ฐจ ์›จ์ด๋ธŒ ์ฃผํŒŒ์ˆ˜
  public float waveAmp2 = 0.4f;    // 2์ฐจ ์›จ์ด๋ธŒ ์ง„ํญ (์„ธ๋ถ€ ๊ตด๊ณก)
  public float waveFreq2 = 2.0f;    // 2์ฐจ ์›จ์ด๋ธŒ ์ฃผํŒŒ์ˆ˜
  public float twistAmount = 1.5f;    // ๋ฐด๋“œ ๊ฐ„ ์œ„์ƒ ์ฐจ์ด (์ „์ฒด ๋น„ํ‹€๋ฆผ๋Ÿ‰)

  [Header("์• ๋‹ˆ๋ฉ”์ด์…˜")]
  public float flowSpeed = 0.15f;     // ํ๋ฆ„ ์ง„ํ–‰ ์†๋„
  public float waveSpeed = 0.8f;      // ์›จ์ด๋ธŒ ์ง„๋™ ์†๋„

  [Header("์ƒ‰์ƒ")]
  [Range(0f, 1f)] public float hueStart = 0.0f;
  [Range(0f, 1f)] public float hueEnd = 0.0f;
  [Range(0f, 1f)] public float saturation = 0.0f;
  [Range(0f, 1f)] public float brightness = 1.0f;
  [Range(0f, 3f)] public float emissionIntensity = 0.0f;

  // -----------------------------------------------------------
  // ๊ตฌ์กฐ ํŒŒ๋ผ๋ฏธํ„ฐ ์บ์‹œ
  // -----------------------------------------------------------

  private int _cachedBandCount;
  private int _cachedSegmentCount;
  private float _cachedBandSpread;
  private float _cachedFlowLength;
  private float _cachedSegmentWidth;

  // -----------------------------------------------------------
  // ์„ธ๊ทธ๋จผํŠธ ๋ฐ์ดํ„ฐ
  // -----------------------------------------------------------

  struct SegData
  {
    public Transform tr;
    public Material mat;
    public float tBand;       // 0~1 (๋ฐด๋“œ ํšก๋ฐฉํ–ฅ ์œ„์น˜)
    public float tSeg;        // 0~1 (์„ธ๊ทธ๋จผํŠธ ํ๋ฆ„ ๋ฐฉํ–ฅ ์œ„์น˜)
    public float phaseOffset; // ๋ฐด๋“œ๋ณ„ ๊ณ ์œ  ์œ„์ƒ
  }

  SegData[] _segs = new SegData[0];
  Mesh _sharedQuadMesh;

  // segmentCount ์—์„œ ๊ณ„์‚ฐ๋˜๋Š” ์ฟผ๋“œ ๋†’์ด
  private float _segHeight;

#if UNITY_EDITOR
    bool _generateQueued = false;
#endif

  // -----------------------------------------------------------
  // Unity ์ด๋ฒคํŠธ
  // -----------------------------------------------------------

  void OnEnable()
  {
    if (_segs.Length == 0)
      Generate();
    CacheStructureParams();
  }

  void OnDisable() => Clear();
  void OnDestroy() => Clear();

  void OnValidate()
  {
#if UNITY_EDITOR
        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
        if (!Application.isPlaying) return;
#endif
    ApplyAnimation(Time.time);
  }

  // -----------------------------------------------------------
  // ๊ตฌ์กฐ ํŒŒ๋ผ๋ฏธํ„ฐ ๋ณ€๊ฒฝ ๊ฐ์ง€
  // -----------------------------------------------------------

  bool StructureParamsChanged()
  {
    return bandCount != _cachedBandCount
        || segmentCount != _cachedSegmentCount
        || bandSpread != _cachedBandSpread
        || flowLength != _cachedFlowLength
        || segmentWidth != _cachedSegmentWidth;
  }

  void CacheStructureParams()
  {
    _cachedBandCount = bandCount;
    _cachedSegmentCount = segmentCount;
    _cachedBandSpread = bandSpread;
    _cachedFlowLength = flowLength;
    _cachedSegmentWidth = segmentWidth;
  }

  // -----------------------------------------------------------
  // ์ƒ์„ฑ
  // -----------------------------------------------------------

  void Generate()
  {
    Clear();

    Shader litShader = Shader.Find("Universal Render Pipeline/Lit");
    if (litShader == null)
    {
      Debug.LogError("[WaveFlowGenerator] URP Lit ์…ฐ์ด๋”๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. URP ํ”„๋กœ์ ํŠธ์ธ์ง€ ํ™•์ธํ•˜์„ธ์š”.");
      return;
    }

    _sharedQuadMesh = GetBuiltinQuadMesh();
    if (_sharedQuadMesh == null)
    {
      Debug.LogError("[WaveFlowGenerator] Quad Mesh๋ฅผ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.");
      return;
    }

    // ์„ธ๊ทธ๋จผํŠธ ์ฟผ๋“œ ๋†’์ด: ํ๋ฆ„ ๊ธธ์ด๋ฅผ ์„ธ๊ทธ๋จผํŠธ ์ˆ˜๋กœ ๊ท ๋“ฑ ๋ถ„ํ• 
    _segHeight = flowLength / segmentCount;

    int total = bandCount * segmentCount;
    _segs = new SegData[total];

    for (int b = 0; b < bandCount; b++)
    {
      float tBand = (bandCount > 1) ? (float)b / (bandCount - 1) : 0f;
      float phase = tBand * twistAmount * Mathf.PI * 2f;

      for (int s = 0; s < segmentCount; s++)
      {
        float tSeg = (segmentCount > 1) ? (float)s / (segmentCount - 1) : 0f;
        int idx = b * segmentCount + s;

        _segs[idx] = SpawnSegment(idx, tBand, tSeg, phase, litShader);
      }
    }

    ApplyAnimation(0f);
  }

  // ์ฟผ๋“œ๋ฅผ ์ˆ˜๋™์œผ๋กœ ์กฐ๋ฆฝํ•ด BoxCollider ์ƒ์„ฑ์„ ๋ง‰์Œ
  SegData SpawnSegment(int index, float tBand, float tSeg, float phaseOffset, Shader shader)
  {
    GameObject go = new GameObject($"Seg_{index}");
    go.transform.SetParent(transform, false);
    go.transform.localScale = new Vector3(segmentWidth, _segHeight, 1f);

    MeshFilter mf = go.AddComponent<MeshFilter>();
    mf.sharedMesh = _sharedQuadMesh;

    MeshRenderer mr = go.AddComponent<MeshRenderer>();
    mr.shadowCastingMode = ShadowCastingMode.Off;
    mr.receiveShadows = false;

    Material mat = new Material(shader);
    mat.EnableKeyword("_EMISSION");
    mat.globalIlluminationFlags = MaterialGlobalIlluminationFlags.RealtimeEmissive;
    mr.material = mat;

    return new SegData
    {
      tr = go.transform,
      mat = mat,
      tBand = tBand,
      tSeg = tSeg,
      phaseOffset = phaseOffset
    };
  }

  // ์ž„์‹œ ์˜ค๋ธŒ์ ํŠธ๋กœ ๋นŒํŠธ์ธ Quad Mesh ํš๋“ ํ›„ ์ฆ‰์‹œ ํŒŒ๊ดด
  Mesh GetBuiltinQuadMesh()
  {
    GameObject temp = GameObject.CreatePrimitive(PrimitiveType.Quad);
    Mesh mesh = temp.GetComponent<MeshFilter>().sharedMesh;
    DestroyImmediate(temp);
    return mesh;
  }

  // -----------------------------------------------------------
  // ์ •๋ฆฌ
  // -----------------------------------------------------------

  void Clear()
  {
    if (_segs != null)
    {
      for (int i = 0; i < _segs.Length; i++)
      {
        if (_segs[i].mat != null)
          DestroyImmediate(_segs[i].mat);
      }
    }

    _segs = new SegData[0];

    while (transform.childCount > 0)
    {
#if UNITY_EDITOR
            DestroyImmediate(transform.GetChild(0).gameObject);
#else
      Destroy(transform.GetChild(0).gameObject);
#endif
    }
  }

  // -----------------------------------------------------------
  // ์• ๋‹ˆ๋ฉ”์ด์…˜
  // -----------------------------------------------------------

  void ApplyAnimation(float t)
  {
    float invSegCount = 1f / segmentCount;

    for (int i = 0; i < _segs.Length; i++)
    {
      ref SegData seg = ref _segs[i];
      if (seg.tr == null || seg.mat == null) continue;

      // ํ˜„์žฌ ์œ„์น˜ ๋ฐ ๋‹ค์Œ ์œ„์น˜ (ํƒ„์  ํŠธ ๊ณ„์‚ฐ์šฉ)
      Vector3 pos0 = CalcPosition(seg.tBand, seg.tSeg, seg.phaseOffset, t);
      Vector3 pos1 = CalcPosition(seg.tBand, seg.tSeg + invSegCount, seg.phaseOffset, t);

      seg.tr.localPosition = pos0;

      // ์ฟผ๋“œ ๋กœ์ปฌ Y๋ฅผ ํ๋ฆ„ ๋ฐฉํ–ฅ ํƒ„์  ํŠธ์— ์ •๋ ฌ
      Vector3 tangent = pos1 - pos0;
      if (tangent.sqrMagnitude > 0.0001f)
        seg.tr.localRotation = Quaternion.FromToRotation(Vector3.up, tangent.normalized);

      // ์ƒ‰์ƒ
      // tBand ๋กœ Hue ๊ฒฐ์ •, tSeg ์œผ๋กœ ๋ฐ๊ธฐ ๋ณ€์กฐ (๋๋‹จ์„ ์•ฝ๊ฐ„ ์–ด๋‘ก๊ฒŒ)
      float hue = Mathf.Lerp(hueStart, hueEnd, seg.tBand);
      float brightnessVar = brightness * (0.55f + 0.45f * Mathf.Abs(Mathf.Sin(seg.tSeg * Mathf.PI)));
      Color albedo = Color.HSVToRGB(hue, saturation, brightnessVar);
      Color emission = Color.HSVToRGB(hue, 1f, brightnessVar) * emissionIntensity;

      seg.mat.SetColor("_BaseColor", albedo);
      seg.mat.SetColor("_EmissionColor", emission);
    }
  }

  // ์„ธ๊ทธ๋จผํŠธ ์œ„์น˜ ๊ณ„์‚ฐ (ApplyAnimation ์—์„œ ํƒ„์  ํŠธ์šฉ์œผ๋กœ ๋‘ ๋ฒˆ ํ˜ธ์ถœ)
  Vector3 CalcPosition(float tBand, float tSeg, float phase, float t)
  {
    // tFlow: ์‹œ๊ฐ„์— ๋”ฐ๋ผ ํ๋ฆ„ ๋ฐฉํ–ฅ์œผ๋กœ ์ง€์† ์ด๋™
    float tFlow = tSeg + t * flowSpeed;

    // 1์ฐจ ์›จ์ด๋ธŒ: X ๋ฐฉํ–ฅ ์ฃผ์š” ๊ตด๊ณก
    float wave1X = Mathf.Sin(tFlow * waveFreq1 * Mathf.PI * 2f + phase) * waveAmp1;
    // 2์ฐจ ์›จ์ด๋ธŒ: X ๋ฐฉํ–ฅ ์„ธ๋ถ€ ๊ตด๊ณก + ๋…๋ฆฝ ์ง„๋™
    float wave2X = Mathf.Sin(tFlow * waveFreq2 * Mathf.PI * 2f + phase * 0.7f + t * waveSpeed) * waveAmp2;

    // Z ๊นŠ์ด ์›จ์ด๋ธŒ: ์ฝ”์‚ฌ์ธ (X์™€ 90๋„ ์œ„์ƒ ์ฐจ์ด โ†’ ๋‚˜์„ ๊ฐ)
    float wave1Z = Mathf.Cos(tFlow * waveFreq1 * Mathf.PI * 2f + phase) * waveAmp1 * 0.4f;
    float wave2Z = Mathf.Cos(tFlow * waveFreq2 * Mathf.PI * 2f + phase * 0.7f + t * waveSpeed) * waveAmp2 * 0.3f;

    float x = (tBand - 0.5f) * bandSpread + wave1X + wave2X;
    float y = (tSeg - 0.5f) * flowLength;
    float z = wave1Z + wave2Z;

    return new Vector3(x, y, z);
  }
}

์‹คํ–‰ ๊ตฌ์กฐ ๋ฐ ํ•จ์ˆ˜ ์„ค๋ช…

๐Ÿ“ ์ „์ฒด ์‹คํ–‰ ๊ตฌ์กฐ ์š”์•ฝ

์ด ์ฝ”๋“œ๋Š” ํฌ๊ฒŒ 4๊ฐœ์˜ ๊ตฌํš์œผ๋กœ ๋‚˜๋‰ฉ๋‹ˆ๋‹ค:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ 1. Unity ์ด๋ฒคํŠธ (๋ผ์ดํ”„์‚ฌ์ดํด ๊ด€๋ฆฌ)      โ”‚ โ† ์ƒํƒœ ๋ณ€ํ™” ๊ฐ์ง€, ์ดˆ๊ธฐํ™”, ์ •๋ฆฌ
โ”‚    โ€ข OnEnable/Disable/Destroy        โ”‚
โ”‚    โ€ข OnValidate, Update              โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ 2. ๊ตฌ์กฐ ํŒŒ๋ผ๋ฏธํ„ฐ ๊ด€๋ฆฌ (๋ถ„๊ธฐ ๋กœ์ง)        โ”‚ โ† "์žฌ์ƒ์„ฑ ํ•„์š”ํ•œ์ง€?" ํŒ๋‹จ
โ”‚    โ€ข StructureParamsChanged()        โ”‚
โ”‚    โ€ข CacheStructureParams()          โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ 3. ์˜ค๋ธŒ์ ํŠธ ์ƒ์„ฑ (๊ฒŒ์ž„ ์˜ค๋ธŒ์ ํŠธ ๊ตฌ์ถ•)     โ”‚ โ† ์‹ค์ œ GameObject, Mesh, Material ์ƒ์„ฑ
โ”‚    โ€ข Generate(), SpawnSegment()      โ”‚
โ”‚    โ€ข GetBuiltinQuadMesh()            โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ 4. ์• ๋‹ˆ๋ฉ”์ด์…˜ (ํ”„๋ ˆ์ž„๋ณ„ ์‹œ๊ฐ ์—…๋ฐ์ดํŠธ)    โ”‚ โ† ์œ„์น˜/ํšŒ์ „/์ƒ‰์ƒ ์‹ค์‹œ๊ฐ„ ๊ณ„์‚ฐ
โ”‚    โ€ข ApplyAnimation(), CalcPosition()โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

๐Ÿ” 1. Unity ์ด๋ฒคํŠธ ํ•จ์ˆ˜ (๊ด€๋ฆฌ์šฉ ์ฝ”๋“œ)

void OnEnable()

void OnEnable()
{
    if (_segs.Length == 0) Generate();  // ์ตœ์ดˆ ์‹คํ–‰ ์‹œ ๊ตฌ์กฐ ์ƒ์„ฑ
    CacheStructureParams();              // ํ˜„์žฌ ํŒŒ๋ผ๋ฏธํ„ฐ๊ฐ’ ์บ์‹ฑ
}
  • ์˜๋ฏธ: ์ปดํฌ๋„ŒํŠธ๊ฐ€ ํ™œ์„ฑํ™”๋  ๋•Œ ๋˜๋Š” GameObject ๊ฐ€ ์ผœ์งˆ ๋•Œ ์ž๋™์œผ๋กœ ํ˜ธ์ถœ
  • ์ž‘๋™:
    1. _segs ๋ฐฐ์—ด์ด ๋น„์–ด์žˆ์œผ๋ฉด Generate() ๋ฅผ ํ˜ธ์ถœํ•ด ์˜ค๋ธŒ์ ํŠธ๋“ค์„ ์ฒ˜์Œ ์ƒ์„ฑ
    2. ๊ตฌ์กฐ ํŒŒ๋ผ๋ฏธํ„ฐ๋“ค(bandCount, segmentCount ๋“ฑ)์„ _cached~ ๋ณ€์ˆ˜์— ์ €์žฅ
  • ๋ชฉ์ : "์ดˆ๊ธฐํ™” ์‹œ์ "์„ ๊ด€๋ฆฌํ•˜์—ฌ ๋ถˆํ•„์š”ํ•œ ์žฌ์ƒ์„ฑ์„ ๋ฐฉ์ง€

void OnDisenable() / void OnDestroy()

void OnDisable() => Clear();
void OnDestroy() => Clear();
  • ์˜๋ฏธ: ์ปดํฌ๋„ŒํŠธ ๋น„ํ™œ์„ฑํ™”/์†Œ๋ฉธ ์‹œ ํ˜ธ์ถœ
  • ์ž‘๋™: Clear() ๋ฅผ ํ˜ธ์ถœํ•ด ์ƒ์„ฑ๋œ ๋ชจ๋“  ์ž์‹ ์˜ค๋ธŒ์ ํŠธ์™€ ๋จธํ‹ฐ๋ฆฌ์–ผ์„ ์ •๋ฆฌ
  • ๋ชฉ์ : ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ ๋ฐฉ์ง€, ์—๋””ํ„ฐ/๋Ÿฐํƒ€์ž„์—์„œ ์ž”์—ฌ๋ฌผ ์ œ๊ฑฐ

void OnValidate() โœจ (์—๋””ํ„ฐ ์ „์šฉ)

void OnValidate()
{
#if UNITY_EDITOR
    if (StructureParamsChanged())  // ๊ตฌ์กฐ ํŒŒ๋ผ๋ฏธํ„ฐ ๋ณ€๊ฒฝ ๊ฐ์ง€
    {
        CacheStructureParams();
        QueueGenerate();           // ์žฌ์ƒ์„ฑ ํ ๋“ฑ๋ก (๋น„๋™๊ธฐ)
    }
    else
    {
        ApplyAnimation(0f);        // ์ƒ‰์ƒ/์• ๋‹ˆ ํŒŒ๋ผ๋ฏธํ„ฐ๋งŒ ๋ณ€๊ฒฝ โ†’ ์ฆ‰์‹œ ๋ฏธ๋ฆฌ๋ณด๊ธฐ
    }
#endif
}
  • ์˜๋ฏธ: ์ธ์ŠคํŽ™ํ„ฐ์—์„œ ๊ฐ’์ด ๋ณ€๊ฒฝ๋  ๋•Œ๋งˆ๋‹ค ์—๋””ํ„ฐ์—์„œ ์ž๋™ ํ˜ธ์ถœ
  • ์ž‘๋™:
    • ๊ตฌ์กฐ ํŒŒ๋ผ๋ฏธํ„ฐ(bandCount, segmentCount ๋“ฑ) ๋ณ€๊ฒฝ โ†’ QueueGenerate() ๋กœ ์žฌ์ƒ์„ฑ ์˜ˆ์•ฝ
    • ์‹œ๊ฐ ํŒŒ๋ผ๋ฏธํ„ฐ(์ƒ‰์ƒ, ์ง„ํญ, ์†๋„ ๋“ฑ) ๋ณ€๊ฒฝ โ†’ ์ฆ‰์‹œ ApplyAnimation(0f) ์œผ๋กœ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ๊ฐฑ์‹ 
  • ๋ชฉ์ : ์—๋””ํ„ฐ์—์„œ ์‹ค์‹œ๊ฐ„ ํ”„๋ฆฌ๋ทฐ ์ œ๊ณต + ๋ถˆํ•„์š”ํ•œ ์žฌ์ƒ์„ฑ ๋ฐฉ์ง€

void QueueGenerate() โœจ (์—๋””ํ„ฐ ์ „์šฉ)

void QueueGenerate()
{
    if (_generateQueued) return;
    _generateQueued = true;
    EditorApplication.delayCall += () =>  // ๋‹ค์Œ ์—๋””ํ„ฐ ํ”„๋ ˆ์ž„์œผ๋กœ ์ง€์—ฐ
    {
        _generateQueued = false;
        if (this == null) return;
        Generate();  // ์‹ค์ œ ์žฌ์ƒ์„ฑ ์‹คํ–‰
    };
}
  • ์˜๋ฏธ: ์žฌ์ƒ์„ฑ ์ž‘์—…์„ ์—๋””ํ„ฐ์˜ ๋‹ค์Œ ํ”„๋ ˆ์ž„์œผ๋กœ ๋ฏธ๋ฃธ
  • ์ž‘๋™:
    1. _generateQueued ํ”Œ๋ž˜๊ทธ๋กœ ์ค‘๋ณต ํ ๋ฐฉ์ง€
    2. EditorApplication.delayCall ๋กœ ์ž‘์—… ์Šค์ผ€์ค„๋ง
    3. ์‹ค์ œ Generate() ์‹คํ–‰ ์ „์— null ์ฒดํฌ๋กœ ์•ˆ์ „์„ฑ ํ™•๋ณด
      ๋ชฉ์ : ์ธ์ŠคํŽ™ํ„ฐ์—์„œ ์Šฌ๋ผ์ด๋”๋ฅผ ๋น ๋ฅด๊ฒŒ ์›€์ง์ผ ๋•Œ ๋งค ํ”„๋ ˆ์ž„๋งˆ๋‹ค ์žฌ์ƒ์„ฑ๋˜๋Š” ์„ฑ๋Šฅ ์ €ํ•˜ ๋ฐฉ์ง€

void Update()

void Update()
{
#if UNITY_EDITOR
    if (!Application.isPlaying) return;  // ์—๋””ํ„ฐ์—์„  ํ”Œ๋ ˆ์ด ๋ชจ๋“œ์ผ ๋•Œ๋งŒ ์‹คํ–‰
#endif
    ApplyAnimation(Time.time);  // ๊ฒŒ์ž„ ์‹œ๊ฐ„์— ๊ธฐ๋ฐ˜ํ•œ ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ ์šฉ
}
  • ์˜๋ฏธ: ๋งค ํ”„๋ ˆ์ž„ ํ˜ธ์ถœ๋˜๋Š” ๋ฉ”์ธ ๋ฃจํ”„
  • ์ž‘๋™:
    -์—๋””ํ„ฐ์—์„œ๋Š” Application.isPlaying ์ด ์ฐธ์ผ ๋•Œ๋งŒ ์‹คํ–‰ (ํ”„๋ฆฌ๋ทฐ์™€ ํ”Œ๋ ˆ์ด ๋ชจ๋“œ ๋ถ„๋ฆฌ)
    -Time.time ์„ ApplyAnimation() ์— ์ „๋‹ฌํ•ด ์‹œ๊ฐ„์— ๋”ฐ๋ฅธ ์›จ์ด๋ธŒ ์• ๋‹ˆ๋ฉ”์ด์…˜ ๊ณ„์‚ฐ
  • ๋ชฉ์ : ๋Ÿฐํƒ€์ž„์—์„œ ๋ถ€๋“œ๋Ÿฌ์šด ์• ๋‹ˆ๋ฉ”์ด์…˜ ๊ตฌ๋™

โš™๏ธ ๊ตฌ์กฐ ํŒŒ๋ผ๋ฏธํ„ฐ ๊ด€๋ฆฌ ํ•จ์ˆ˜ (๋ถ„๊ธฐ ๋กœ์ง)

bool StructureParamsChanged()

bool StructureParamsChanged()
{
    return bandCount    != _cachedBandCount
        || segmentCount != _cachedSegmentCount
        || bandSpread   != _cachedBandSpread
        || flowLength   != _cachedFlowLength
        || segmentWidth != _cachedSegmentWidth;
}
  • ์˜๋ฏธ: "์˜ค๋ธŒ์ ํŠธ ๊ตฌ์กฐ๋ฅผ ๋‹ค์‹œ ๋งŒ๋“ค์–ด์•ผ ํ•˜๋‚˜?" ๋ฅผ ํŒ๋‹จํ•˜๋Š” ๊ฐ์ง€๊ธฐ
  • ์ž‘๋™: ํ˜„์žฌ ๊ณต๊ฐœ ํŒŒ๋ผ๋ฏธํ„ฐ์™€ ์บ์‹œ๋œ ์ด์ „ ๊ฐ’์„ ๋น„๊ตํ•˜์—ฌ ๋ณ€๊ฒฝ ์—ฌ๋ถ€ ๋ฐ˜ํ™˜
  • ๋ชฉ์ : OnValidate() ์—์„œ ์žฌ์ƒ์„ฑ ํ•„์š” ์—ฌ๋ถ€๋ฅผ ๊ฒฐ์ •ํ•˜๋Š” ํ•ต์‹ฌ ๋กœ์ง

void CacheStructureParams()

void CacheStructureParams()
{
    _cachedBandCount     = bandCount;
    _cachedSegmentCount  = segmentCount;
    // ... ๋‚˜๋จธ์ง€ ํŒŒ๋ผ๋ฏธํ„ฐ๋„ ๋™์ผํ•˜๊ฒŒ ์บ์‹ฑ
}
  • ์˜๋ฏธ: ํ˜„์žฌ ๊ตฌ์กฐ ํŒŒ๋ผ๋ฏธํ„ฐ ๊ฐ’์„ "์ด์ „ ๊ฐ’"์œผ๋กœ ์ €์žฅ
  • ์ž‘๋™: ๊ณต๊ฐœ ํ•„๋“œ ๊ฐ’์„ ๋น„๊ณต๊ฐœ _cached~ ํ•„๋“œ์— ๋ณต์‚ฌ
  • ๋ชฉ์ : ๋‹ค์Œ ๋ณ€๊ฒฝ ๊ฐ์ง€๋ฅผ ์œ„ํ•œ ๊ธฐ์ค€์  ์„ค์ •

๐Ÿ—๏ธ ์˜ค๋ธŒ์ ํŠธ ์ƒ์„ฑ ํ•จ์ˆ˜ (๊ฒŒ์ž„ ์˜ค๋ธŒ์ ํŠธ ๊ตฌ์ถ•)

void Generate()

void Generate()
{
    Clear();  // ๊ธฐ์กด ์˜ค๋ธŒ์ ํŠธ ์ •๋ฆฌ
    // 1. ์…ฐ์ด๋” ๋กœ๋”ฉ (URP Lit)
    Shader litShader = Shader.Find("Universal Render Pipeline/Lit");
    // 2. ๊ธฐ๋ณธ ์ฟผ๋“œ ๋ฉ”์‰ฌ ํš๋“
    _sharedQuadMesh = GetBuiltinQuadMesh();
    // 3. ์„ธ๊ทธ๋จผํŠธ ๋†’์ด ๊ณ„์‚ฐ
    _segHeight = flowLength / segmentCount;
    // 4. ์„ธ๊ทธ๋จผํŠธ ๋ฐฐ์—ด ํ• ๋‹น
    int total = bandCount * segmentCount;
    _segs = new SegData[total];
    // 5. ์ด์ค‘ ๋ฃจํ”„๋กœ ๋ชจ๋“  ์„ธ๊ทธ๋จผํŠธ ์ƒ์„ฑ
    for (int b = 0; b < bandCount; b++)
    {
        float tBand = (bandCount > 1) ? (float)b / (bandCount - 1) : 0f;
        float phase = tBand * twistAmount * Mathf.PI * 2f;  // ๋ฐด๋“œ๋ณ„ ์œ„์ƒ ์˜คํ”„์…‹ 
        for (int s = 0; s < segmentCount; s++)
        {
            float tSeg = (segmentCount > 1) ? (float)s / (segmentCount - 1) : 0f;
            int idx = b * segmentCount + s;
            _segs[idx] = SpawnSegment(idx, tBand, tSeg, phase, litShader);
        }
    }
    ApplyAnimation(0f);  // ์ดˆ๊ธฐ ์‹œ๊ฐ ์ƒํƒœ ์ ์šฉ
}
  • ์˜๋ฏธ: ์ „์ฒด ๋ฆฌ๋ณธ ๊ตฌ์กฐ๋ฅผ ์ฒ˜์Œ๋ถ€ํ„ฐ ๊ตฌ์ถ•ํ•˜๋Š” ๋ฉ”์ธ ์ƒ์„ฑ ํ•จ์ˆ˜
  • ์ž‘๋™:
    1. ๊ธฐ์กด ์˜ค๋ธŒ์ ํŠธ ์ •๋ฆฌ (Clear())
    2. URP Lit ์…ฐ์ด๋”์™€ ๊ธฐ๋ณธ Quad Mesh ์ค€๋น„
    3. bandCount ร— segmentCount ๋งŒํผ SpawnSegment() ํ˜ธ์ถœ
    4. ๊ฐ ์„ธ๊ทธ๋จผํŠธ์— tBand(0~1, ๊ฐ€๋กœ ์œ„์น˜), tSeg(0~1, ์„ธ๋กœ ์œ„์น˜), phase(์œ„์ƒ์ฐจ) ํŒŒ๋ผ๋ฏธํ„ฐ ๋ถ€์—ฌ
  • ๋ชฉ์ : ํŒŒ๋ผ๋ฏธํ„ฐ์— ๊ธฐ๋ฐ˜ํ•œ ์ œ๋„ˆ๋ ˆ์ดํ‹ฐ๋ธŒ ์•„ํŠธ์›Œํฌ์˜ ๋ฌผ๋ฆฌ์  ๊ตฌ์กฐ ์ƒ์„ฑ

SegData SpawnSegment(...)

SegData SpawnSegment(int index, float tBand, float tSeg, float phaseOffset, Shader shader)
{
    	// 1. GameObject ์ƒ์„ฑ ๋ฐ ๊ณ„์ธต ์„ค์ •
    GameObject go = new GameObject($"Seg_{index}");
    go.transform.SetParent(transform, false);
    go.transform.localScale = new Vector3(segmentWidth, _segHeight, 1f);
    	// 2. MeshFilter: ๊ณต์œ  ์ฟผ๋“œ ๋ฉ”์‰ฌ ํ• ๋‹น
    MeshFilter mf = go.AddComponent<MeshFilter>();
    mf.sharedMesh = _sharedQuadMesh;
       // 3. MeshRenderer: ๊ทธ๋ฆผ์ž ์„ค์ • ๋„๊ธฐ
    MeshRenderer mr = go.AddComponent<MeshRenderer>();
    mr.shadowCastingMode = ShadowCastingMode.Off;
    mr.receiveShadows = false;
        // 4. Material: ๋ฐœ๊ด‘ ํ‚ค์›Œ๋“œ ํ™œ์„ฑํ™”
    Material mat = new Material(shader);
    mat.EnableKeyword("_EMISSION");
    mat.globalIlluminationFlags = MaterialGlobalIlluminationFlags.RealtimeEmissive;
    mr.material = mat;
        // 5. ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ์ฒด๋กœ ๋ฐ˜ํ™˜
    return new SegData { tr = go.transform, mat = mat, tBand = tBand, tSeg = tSeg, phaseOffset = phaseOffset };
}
  • ์˜๋ฏธ: ๋‹จ์ผ ์„ธ๊ทธ๋จผํŠธ(์ฟผ๋“œ) ์˜ค๋ธŒ์ ํŠธ๋ฅผ ์ƒ์„ฑํ•˜๊ณ  ์„ค์ •ํ•˜๋Š” ํŒฉํ† ๋ฆฌ ํ•จ์ˆ˜
  • ์ž‘๋™:
    • GameObject ์ƒ์„ฑ โ†’ Transform ์„ค์ • โ†’ MeshFilter/Renderer ์ถ”๊ฐ€ โ†’ Material ์ƒ์„ฑ
    • ๋ฐœ๊ด‘(_EMISSION) ํ‚ค์›Œ๋“œ ํ™œ์„ฑํ™”๋กœ ๋„ค์˜จ ๋А๋‚Œ ๊ตฌํ˜„
    • ์ƒ์„ฑ๋œ ์ฐธ์กฐ๋“ค๊ณผ ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ SegData ๊ตฌ์กฐ์ฒด๋กœ ๋ฌถ์–ด ๋ฐ˜ํ™˜
  • ๋ชฉ์ : ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ์„ธ๊ทธ๋จผํŠธ ์ƒ์„ฑ ๋กœ์ง ์บก์Аํ™”

Mesh GetBuiltinQuadMesh()

Mesh GetBuiltinQuadMesh()
{
    GameObject temp = GameObject.CreatePrimitive(PrimitiveType.Quad);
    Mesh mesh = temp.GetComponent<MeshFilter>().sharedMesh;
    DestroyImmediate(temp);  // ์ž„์‹œ ์˜ค๋ธŒ์ ํŠธ๋Š” ์ฆ‰์‹œ ์‚ญ์ œ
    return mesh;  // ๋ฉ”์‰ฌ ์ฐธ์กฐ๋งŒ ๋ฐ˜ํ™˜ (Unity ๋‚ด์žฅ ๋ฆฌ์†Œ์Šค)
}
  • ์˜๋ฏธ: Unity ๋‚ด์žฅ Quad ๋ฉ”์‰ฌ๋ฅผ ์ฐธ์กฐ๋งŒ ์–ป์–ด์˜ค๋Š” ํ—ฌํผ
  • ์ž‘๋™: ์ž„์‹œ Quad ์ƒ์„ฑ โ†’ ๋ฉ”์‰ฌ ์ฐธ์กฐ ์ถ”์ถœ โ†’ ์ž„์‹œ ์˜ค๋ธŒ์ ํŠธ ์ฆ‰์‹œ ์‚ญ์ œ
  • ๋ชฉ์ : ๋ฉ”์‰ฌ ๋ฐ์ดํ„ฐ๋ฅผ ์ค‘๋ณต ์ƒ์„ฑํ•˜์ง€ ์•Š๊ณ  ๊ณต์œ ํ•˜์—ฌ ๋ฉ”๋ชจ๋ฆฌ ํšจ์œจํ™”

๐Ÿงน ์ •๋ฆฌ ํ•จ์ˆ˜

void Clear()

void Clear()
{
    // 1. ๋ชจ๋“  ๋จธํ‹ฐ๋ฆฌ์–ผ ์ˆ˜๋™ ์‚ญ์ œ (๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ ๋ฐฉ์ง€)
    if (_segs != null)
    {
        for (int i = 0; i < _segs.Length; i++)
        {
            if (_segs[i].mat != null)
                DestroyImmediate(_segs[i].mat);  // ์—๋””ํ„ฐ์šฉ
        }
    }
    
    // 2. ๋ฐฐ์—ด ์ดˆ๊ธฐํ™”
    _segs = new SegData[0];
    
    // 3. ๋ชจ๋“  ์ž์‹ GameObject ์‚ญ์ œ
    while (transform.childCount > 0)
    {
#if UNITY_EDITOR
        DestroyImmediate(transform.GetChild(0).gameObject);
#else
        Destroy(transform.GetChild(0).gameObject);  // ๋Ÿฐํƒ€์ž„์šฉ
#endif
    }
}
  • ์˜๋ฏธ: ์ƒ์„ฑ๋œ ๋ชจ๋“  ๋ฆฌ์†Œ์Šค๋ฅผ ์ •๋ฆฌํ•˜๋Š” ์†Œ๋ฉธ์ž ์—ญํ• 
  • ์ž‘๋™:
    • Material ์€ DestroyImmediate ๋กœ ์ฆ‰์‹œ ํ•ด์ œ (์—๋””ํ„ฐ์—์„œ ์ค‘์š”)
    • GameObject ๋Š” ์—๋””ํ„ฐ/๋Ÿฐํƒ€์ž„์— ๋”ฐ๋ผ DestroyImmediate / Destroy ๋ถ„๊ธฐ ์ฒ˜๋ฆฌ
  • ๋ชฉ์ : ์žฌ์ƒ์„ฑ ์‹œ ์ค‘๋ณต ์ƒ์„ฑ ๋ฐฉ์ง€, ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ ์ฐจ๋‹จ

๐ŸŽฌ ์• ๋‹ˆ๋ฉ”์ด์…˜ ํ•จ์ˆ˜ (์‹œ๊ฐ ์—…๋ฐ์ดํŠธ)

void ApplyAnimation(float t)

void ApplyAnimation(float t)
{
    float invSegCount = 1f / segmentCount;
    
    for (int i = 0; i < _segs.Length; i++)
    {
        ref SegData seg = ref _segs[i];
        if (seg.tr == null || seg.mat == null) continue;  // ์•ˆ์ „ ์ฒดํฌ
        
        // 1. ์œ„์น˜ ๊ณ„์‚ฐ: ํ˜„์žฌ์™€ ๋‹ค์Œ ์ง€์ ์œผ๋กœ ํƒ„์  ํŠธ(๋ฐฉํ–ฅ) ๊ตฌํ•จ
        Vector3 pos0 = CalcPosition(seg.tBand, seg.tSeg, seg.phaseOffset, t);
        Vector3 pos1 = CalcPosition(seg.tBand, seg.tSeg + invSegCount, seg.phaseOffset, t);
        
        seg.tr.localPosition = pos0;  // ์œ„์น˜ ์ ์šฉ
        
        // 2. ํšŒ์ „: ์ฟผ๋“œ์˜ Y ์ถ•์„ ํ๋ฆ„ ๋ฐฉํ–ฅ(ํƒ„์  ํŠธ)์— ์ •๋ ฌ
        Vector3 tangent = pos1 - pos0;
        if (tangent.sqrMagnitude > 0.0001f)
            seg.tr.localRotation = Quaternion.FromToRotation(Vector3.up, tangent.normalized);
        
        // 3. ์ƒ‰์ƒ: tBand ๋กœ Hue ๊ทธ๋ผ๋ฐ์ด์…˜, tSeg ๋กœ ๋ฐ๊ธฐ ๋ณ€์กฐ
        float hue = Mathf.Lerp(hueStart, hueEnd, seg.tBand);
        float brightnessVar = brightness * (0.55f + 0.45f * Mathf.Abs(Mathf.Sin(seg.tSeg * Mathf.PI)));
        Color albedo = Color.HSVToRGB(hue, saturation, brightnessVar);
        Color emission = Color.HSVToRGB(hue, 1f, brightnessVar) * emissionIntensity;
        
        seg.mat.SetColor("_BaseColor", albedo);
        seg.mat.SetColor("_EmissionColor", emission);
    }
}
  • ์˜๋ฏธ: ๋งค ํ”„๋ ˆ์ž„ ๋ชจ๋“  ์„ธ๊ทธ๋จผํŠธ์˜ ์‹œ๊ฐ ์ƒํƒœ๋ฅผ ์—…๋ฐ์ดํŠธํ•˜๋Š” ๋ฉ”์ธ ์• ๋‹ˆ๋ฉ”์ด์…˜ ํ•จ์ˆ˜
  • ์ž‘๋™:
    1. ์œ„์น˜: CalcPosition() ์œผ๋กœ 3D ์ขŒํ‘œ ๊ณ„์‚ฐ
    2. ํšŒ์ „: ํ˜„์žฌ-๋‹ค์Œ ์œ„์น˜์˜ ๋ฒกํ„ฐ ์ฐจ์ด๋กœ ํƒ„์  ํŠธ ๊ณ„์‚ฐ โ†’ ์ฟผ๋“œ๋ฅผ ํ๋ฆ„ ๋ฐฉํ–ฅ์— ์ •๋ ฌ
    3. ์ƒ‰์ƒ:
      - tBand ๋กœ hueStart~hueEnd ๋ฅผ ๋ณด๊ฐ„ํ•˜์—ฌ ๊ฐ€๋กœ ๋ฐฉํ–ฅ ๊ทธ๋ผ๋ฐ์ด์…˜
      - tSeg ๋กœ ์‚ฌ์ธ ํ•จ์ˆ˜๋ฅผ ์ด์šฉํ•ด ๋๋‹จ์ด ์–ด๋‘์›Œ์ง€๋Š” ๋ฐ๊ธฐ ๋ณ€์กฐ
      - _BaseColor(ํ‘œ๋ฉด) ์™€ _EmissionColor(๋ฐœ๊ด‘) ๋ถ„๋ฆฌ ์„ค์ •
      ๋ชฉ์ : ํŒŒ๋ผ๋ฏธํ„ฐ ๊ธฐ๋ฐ˜์˜ ์œ ๊ธฐ์ ์ธ ์›จ์ด๋ธŒ ์• ๋‹ˆ๋ฉ”์ด์…˜ ๊ตฌํ˜„

Vector3 CalcPosition(float tBand, float tSeg, float phase, float t)

Vector3 CalcPosition(float tBand, float tSeg, float phase, float t)
{
    // 1. ํ๋ฆ„ ๋ฐฉํ–ฅ ์ด๋™: ์‹œ๊ฐ„์— ๋”ฐ๋ผ tSeg ๊ฐ€ ์ฆ๊ฐ€
    float tFlow = tSeg + t * flowSpeed;
    
    // 2. X ์ถ• ์›จ์ด๋ธŒ: 1 ์ฐจ(์ฃผ์š”) + 2 ์ฐจ(์„ธ๋ถ€) ์‚ฌ์ธ ํŒŒ ์ค‘์ฒฉ
    float wave1X = Mathf.Sin(tFlow * waveFreq1 * Mathf.PI * 2f + phase) * waveAmp1;
    float wave2X = Mathf.Sin(tFlow * waveFreq2 * Mathf.PI * 2f + phase * 0.7f + t * waveSpeed) * waveAmp2;
    
    // 3. Z ์ถ• ์›จ์ด๋ธŒ: ์ฝ”์‚ฌ์ธ (X ์™€ 90 ๋„ ์œ„์ƒ์ฐจ โ†’ ๋‚˜์„ ํ˜• ํšจ๊ณผ)
    float wave1Z = Mathf.Cos(tFlow * waveFreq1 * Mathf.PI * 2f + phase) * waveAmp1 * 0.4f;
    float wave2Z = Mathf.Cos(tFlow * waveFreq2 * Mathf.PI * 2f + phase * 0.7f + t * waveSpeed) * waveAmp2 * 0.3f;
    
    // 4. ์ตœ์ข… ์ขŒํ‘œ ์กฐํ•ฉ
    float x = (tBand - 0.5f) * bandSpread + wave1X + wave2X;  // ๊ฐ€๋กœ ์œ„์น˜ + ์›จ์ด๋ธŒ
    float y = (tSeg - 0.5f) * flowLength;                      // ์„ธ๋กœ ๊ธฐ๋ณธ ์œ„์น˜
    float z = wave1Z + wave2Z;                                 // ๊นŠ์ด ์›จ์ด๋ธŒ
    
    return new Vector3(x, y, z);
}
  • ์˜๋ฏธ: ๋‹จ์ผ ์„ธ๊ทธ๋จผํŠธ์˜ 3D ์œ„์น˜๋ฅผ ์ˆ˜ํ•™์ ์œผ๋กœ ๊ณ„์‚ฐํ•˜๋Š” ํ•ต์‹ฌ ํ•จ์ˆ˜
  • ์ž‘๋™:
    • tFlow: ์‹œ๊ฐ„์— ๋”ฐ๋ผ ํ๋ฅด๋Š” ํŒŒ๋ผ๋ฏธํ„ฐ (์• ๋‹ˆ๋ฉ”์ด์…˜์˜ ํ•ต์‹ฌ)
    • wave1X / wave2X: ์„œ๋กœ ๋‹ค๋ฅธ ์ฃผํŒŒ์ˆ˜/์ง„ํญ์˜ ์‚ฌ์ธํŒŒ ์ค‘์ฒฉ์œผ๋กœ ๋ณต์žกํ•œ ๊ตด๊ณก ์ƒ์„ฑ
    • wave1Z / wave2Z: ์ฝ”์‚ฌ์ธํŒŒ๋กœ X ์™€ ์œ„์ƒ์ฐจ๋ฅผ ์ฃผ์–ด 3 ์ฐจ์› ๋‚˜์„ ๊ฐ ํ‘œํ˜„
    • phase: ๋ฐด๋“œ๋งˆ๋‹ค ๋‹ค๋ฅธ ์œ„์ƒ ์˜คํ”„์…‹์œผ๋กœ ์ „์ฒด์ ์ธ ๋น„ํ‹€๋ฆผ ํšจ๊ณผ
  • ๋ชฉ์ : ํŒŒ๋ผ๋ฏธํ„ฐ ์กฐ์ ˆ๋งŒ์œผ๋กœ ๋‹ค์–‘ํ•œ ์œ ๊ธฐ์  ์›จ์ด๋ธŒ ํŒจํ„ด ์ƒ์„ฑ
profile
Coding Art with Blender / oF / Processing / p5.js / nannou

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