๐ŸซงArt_022 Paper Swarm

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

Unity GenArt

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

Code

๐Ÿ“’GenerativePaperSwarm.cs

// GenerativePaperSwarm.cs
using UnityEngine;
using UnityEngine.InputSystem;

public class GenerativePaperSwarm : MonoBehaviour
{
    [Header("Particle Settings")]
    public int   particleCount = 500;
    public float spawnRadius   = 5f;

    [Header("Movement")]
    public float moveSpeed     = 3f;
    public float swarmStrength = 2f;
    public float noiseSpeed    = 1.0f;

    [Header("Steering")]
    public float steeringSpeed = 3f;   // ๋‚ฎ์„์ˆ˜๋ก ๋ฐฉํ–ฅ์ „ํ™˜ ๋А๋ฆฌ๊ณ  ์œ ์—ฐ (๊ถŒ์žฅ: 2~4)

    [Header("Orbit")]
    public float orbitRadius = 3f;
    public float orbitSpeed  = 1.5f;
    [Range(0f, 1f)]
    public float orbitBlend  = 0.85f;

    [Header("Wave (Tail)")]
    public float waveAmplitude  = 0.4f;
    public float waveFreqA      = 1.1f;
    public float waveFreqB      = 2.7f;
    public float waveFreqC      = 5.3f;
    public float waveSpeedMult  = 1.0f;

    [Header("Target")]
    public Vector3 targetPosition;

    [Header("Rendering")]
    public Mesh     particleMesh;
    public Material material;

    ComputeBuffer positionBuffer;
    ComputeBuffer velocityBuffer;
    ComputeBuffer argsBuffer;

    Vector3[] positions;
    Vector3[] velocities;
    float[]   phases;

    Material runtimeMaterial;
    Camera   cam;

    void OnEnable() => Init();

    void Init()
    {
        if (material == null || particleMesh == null)
        {
            Debug.LogError("[PaperSwarm] Material ๋˜๋Š” Mesh๊ฐ€ Inspector์— ํ• ๋‹น๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.");
            return;
        }

        cam             = Camera.main;
        runtimeMaterial = new Material(material);

        positions  = new Vector3[particleCount];
        velocities = new Vector3[particleCount];
        phases     = new float[particleCount];

        for (int i = 0; i < particleCount; i++)
        {
            positions[i]  = transform.position + Random.insideUnitSphere * spawnRadius;
            velocities[i] = Random.insideUnitSphere;
            phases[i]     = Random.Range(0f, Mathf.PI * 2f);
        }

        positionBuffer = new ComputeBuffer(particleCount, 12);
        velocityBuffer = new ComputeBuffer(particleCount, 12);

        positionBuffer.SetData(positions);
        velocityBuffer.SetData(velocities);

        uint[] args = new uint[5]
        {
            particleMesh.GetIndexCount(0),
            (uint)particleCount,
            0, 0, 0
        };

        argsBuffer = new ComputeBuffer(
            1, args.Length * sizeof(uint), ComputeBufferType.IndirectArguments);
        argsBuffer.SetData(args);

        runtimeMaterial.SetBuffer("_Positions",  positionBuffer);
        runtimeMaterial.SetBuffer("_Velocities", velocityBuffer);
    }

    void Update()
    {
        if (runtimeMaterial == null || positionBuffer == null) return;

        HandleInput();
        UpdateParticles();

        runtimeMaterial.SetBuffer("_Positions",  positionBuffer);
        runtimeMaterial.SetBuffer("_Velocities", velocityBuffer);

        Graphics.DrawMeshInstancedIndirect(
            particleMesh,
            0,
            runtimeMaterial,
            new Bounds(transform.position, Vector3.one * 100f),
            argsBuffer
        );
    }

    void HandleInput()
    {
        if (Mouse.current == null) return;
        if (!Mouse.current.leftButton.wasPressedThisFrame) return;

        Ray   ray   = cam.ScreenPointToRay(Mouse.current.position.ReadValue());
        Plane plane = new Plane(Vector3.up, Vector3.zero);

        if (plane.Raycast(ray, out float enter))
            targetPosition = ray.GetPoint(enter);
    }

    void UpdateParticles()
    {
        float t = Time.time;

        // ํ”„๋ ˆ์ž„๋ ˆ์ดํŠธ ๋…๋ฆฝ์  ์ง€์ˆ˜ ๊ฐ์‡  ๊ณ„์ˆ˜ (๋งค ํ”„๋ ˆ์ž„ 1ํšŒ ๊ณ„์‚ฐ)
        float smooth = 1f - Mathf.Exp(-steeringSpeed * Time.deltaTime);

        for (int i = 0; i < particleCount; i++)
        {
            Vector3 toTarget = targetPosition - positions[i];
            float   dist     = toTarget.magnitude;

            // โ”€โ”€ 1. ์ ‘๊ทผ ์†๋„ (๊ฑฐ๋ฆฌ ๊ธฐ๋ฐ˜ ํด๋žจํ”„ โ†’ ๊ทผ๊ฑฐ๋ฆฌ ๊ธ‰๊ฐ€์† ๋ฐฉ์ง€) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
            float   approachSpeed = Mathf.Min(moveSpeed, dist * 0.8f);
            Vector3 approachVel   = toTarget.normalized * approachSpeed;

            // โ”€โ”€ 2. ๊ถค๋„ ์šด๋™ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
            float   orbitT      = Mathf.Clamp01(1f - dist / orbitRadius);
                    orbitT      = Mathf.SmoothStep(0f, 1f, orbitT) * orbitBlend;

            Vector3 toNorm      = toTarget.normalized;
            Vector3 tangent     = Vector3.Cross(Vector3.up, toNorm).normalized * orbitSpeed;
            float   radialCorr  = (dist - orbitRadius) * 0.8f;
            Vector3 orbitVel    = tangent + toNorm * radialCorr;

            Vector3 desired = Vector3.Lerp(approachVel, orbitVel, orbitT);

            // โ”€โ”€ 3. ์ง‘๋‹จ ์Šค์›œ ๋…ธ์ด์ฆˆ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
            Vector3 swarm = new Vector3(
                Mathf.PerlinNoise(i * 0.1f,          t * noiseSpeed),
                Mathf.PerlinNoise(t * noiseSpeed,    i * 0.1f),
                Mathf.PerlinNoise(i * 0.05f + 100f,  t * noiseSpeed)
            ) - Vector3.one * 0.5f;
            swarm *= swarmStrength;

            // โ”€โ”€ 4. ์†๋„ ์—…๋ฐ์ดํŠธ (ํ”„๋ ˆ์ž„๋ ˆ์ดํŠธ ๋…๋ฆฝ Lerp) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
            velocities[i] = Vector3.Lerp(velocities[i], desired + swarm, smooth);

            // โ”€โ”€ 5. ๊ผฌ๋ฆฌ ํ”๋“ค๋ฆผ ๋…ธ์ด์ฆˆ (velocity ์˜ค์—ผ ๋ฐฉ์ง€ ์œ„ํ•ด ์œ„์น˜์— ์ง์ ‘ ๊ฐ€์‚ฐ) โ”€
            float p   = phases[i];
            float spd = velocities[i].magnitude;
            float amp = waveAmplitude * Mathf.Clamp(spd / Mathf.Max(moveSpeed, 0.01f), 0.2f, 1.5f);

            Vector3 wave = new Vector3(
                Mathf.Sin(t * waveFreqA * waveSpeedMult + p)         * amp
              + Mathf.Sin(t * waveFreqB * waveSpeedMult + p * 1.3f)  * amp * 0.5f
              + Mathf.Sin(t * waveFreqC * waveSpeedMult + p * 2.1f)  * amp * 0.25f,

                Mathf.Cos(t * waveFreqA * waveSpeedMult + p * 0.7f)  * amp * 0.4f,

                Mathf.Sin(t * waveFreqB * waveSpeedMult + p * 1.7f)  * amp * 0.6f
              + Mathf.Cos(t * waveFreqC * waveSpeedMult + p * 0.9f)  * amp * 0.3f
            );

            positions[i] += (velocities[i] + wave) * Time.deltaTime;
        }

        positionBuffer.SetData(positions);
        velocityBuffer.SetData(velocities);
    }

    void OnDisable()
    {
        positionBuffer?.Release();
        velocityBuffer?.Release();
        argsBuffer?.Release();

        if (runtimeMaterial != null)
            Destroy(runtimeMaterial);
    }
}

๐Ÿ“’PaperSwarmLit.shader

Material ๋งŒ๋“ค๊ณ  Custom ์Šฌ๋กฏ์—์„œ ์ฐพ์•„ ์—ฐ๊ฒฐ

// PaperSwarmLit.shader
Shader "Custom/PaperSwarmLit"
{
    Properties
    {
        _ColorA          ("Color A",        Color)      = (1,0,0,1)
        _ColorB          ("Color B",        Color)      = (0,0,1,1)
        _BendStrength    ("Bend Strength",  Range(0,1)) = 0.15
        _WaveSpeed       ("Wave Speed",     Float)      = 2.0
        _WaveFrequency   ("Wave Frequency", Float)      = 8.0
        _Smoothness      ("Smoothness",     Range(0,1)) = 0.3
    }

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

        Pass
        {
            Name "ForwardLit"

            HLSLPROGRAM

            #pragma vertex   vert
            #pragma fragment frag
            #pragma multi_compile_instancing

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

            StructuredBuffer<float3> _Positions;
            StructuredBuffer<float3> _Velocities;

            float4 _ColorA;
            float4 _ColorB;
            float  _BendStrength;
            float  _WaveSpeed;
            float  _WaveFrequency;
            float  _Smoothness;

            struct appdata
            {
                float3 vertex : POSITION;
                float2 uv     : TEXCOORD0;
                uint   id     : SV_InstanceID;
            };

            struct v2f
            {
                float4 positionHCS : SV_POSITION;
                float2 uv          : TEXCOORD0;
                float3 normalWS    : TEXCOORD1;
            };

            v2f vert(appdata v)
            {
                v2f o;

                float3 pos = _Positions[v.id];
                float3 vel = _Velocities[v.id];

                // โ”€โ”€ 1. ์†๋„ ๋ฐฉํ–ฅ์œผ๋กœ ๋ฉ”์‹œ ํšŒ์ „ ์ •๋ ฌ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
                float3 forward = normalize(vel + float3(0.0001, 0, 0));
                float3 up      = float3(0, 1, 0);
                float3 right   = normalize(cross(up, forward));
                       up      = cross(forward, right);

                float3 local   = v.vertex;
                float3 rotated = local.x * right
                               + local.y * up
                               + local.z * forward;

                // โ”€โ”€ 2. ๋ ˆ์ด์–ด๋“œ wave (๊ผฌ๋ฆฌ ํŽ„๋Ÿญ์ž„) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
                float time  = _Time.y * _WaveSpeed;
                float phase = v.id * 0.37;   // ํŒŒํ‹ฐํด๋ณ„ ์œ„์ƒ (์†Œ์ˆ˜ โ†’ ๊ฒน์นจ ๋ฐฉ์ง€)

                float wave =
                    sin(time * 1.0 + v.uv.y * _WaveFrequency        + phase)       * _BendStrength
                  + sin(time * 2.3 + v.uv.y * _WaveFrequency * 0.5  + phase * 1.6) * _BendStrength * 0.4
                  + sin(time * 5.1 + v.uv.y * _WaveFrequency * 0.2  + phase * 3.1) * _BendStrength * 0.15;

                // uv.y๊ฐ€ ํด์ˆ˜๋ก(๊ผฌ๋ฆฌ ๋) ์ง„ํญ ๊ฐ•ํ™”
                wave *= (0.3 + v.uv.y * 0.7);

                rotated.x += wave;
                rotated.z += cos(time * 1.7 + v.uv.y * 5.0 + phase)       * _BendStrength * 0.5
                           + cos(time * 3.9 + v.uv.y * 2.0 + phase * 2.0) * _BendStrength * 0.2;

                // โ”€โ”€ 3. ์›”๋“œ โ†’ ํด๋ฆฝ ๋ณ€ํ™˜ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
                float3 worldPos = rotated + pos;
                o.positionHCS   = TransformWorldToHClip(worldPos);
                o.normalWS      = up;
                o.uv            = v.uv;

                return o;
            }

            half4 frag(v2f i) : SV_Target
            {
                // UV ๊ทธ๋ผ๋””์–ธํŠธ ์ปฌ๋Ÿฌ
                float  t   = saturate(i.uv.y);
                float3 col = lerp(_ColorA.rgb, _ColorB.rgb, t);

                // URP ๋ฉ”์ธ ๋ผ์ดํŠธ ๋””ํ“จ์ฆˆ
                Light  mainLight = GetMainLight();
                float  NdotL     = saturate(dot(normalize(i.normalWS), mainLight.direction));
                float  lighting  = 0.4 + NdotL * 0.6;

                col *= lighting * mainLight.color;

                return float4(col, 1.0);
            }

            ENDHLSL
        }
    }
}

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

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