๐ŸซงArt_023 Paper Flow Field

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

Unity GenArt

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

Code

๐Ÿ“’PaperFlowField.cs

// PaperFlowField.cs
using UnityEngine;

public class PaperFlowField : MonoBehaviour
{
    [Header("Particles")]
    public int   particleCount = 2000;
    public float spawnRadius   = 10f;

    [Header("Flow Field")]
    public float flowScale    = 0.3f;
    public float flowSpeed    = 0.5f;
    public float flowStrength = 3.0f;

    [Header("Swirl")]
    public Vector3 center      = Vector3.zero;
    public float   swirlForce  = 1.5f;    // ์›ํ˜• ์†Œ์šฉ๋Œ์ด ๊ฐ•๋„
    public float   centerForce = 0.4f;    // ์ค‘์‹ฌ ๋ณต๊ท€ ๊ฐ•๋„ (๋„ˆ๋ฌด ํฌ๋ฉด ๋ญ‰์นจ)

    [Header("Velocity")]
    public float maxSpeed      = 4f;
    public float lerpSpeed     = 3f;      // ๋‚ฎ์„์ˆ˜๋ก ๋ถ€๋“œ๋Ÿฌ์šด ๋ฐฉํ–ฅ์ „ํ™˜

    [Header("Plane Constraint")]
    public float yDamping      = 4f;      // ์ˆ˜์ง ํ”๋“ค๋ฆผ ์–ต์ œ ๊ฐ•๋„
    public float yNoiseScale   = 0.05f;   // ๋ฏธ์„ธ Y ๋ถ€์œ ๊ฐ (0์ด๋ฉด ์™„์ „ ํ‰๋ฉด)

    [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 OnDisable() => ReleaseBuffers();

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

        ReleaseBuffers();

        cam             = Camera.main;
        // [FIX #2] ์›๋ณธ material ๋ณดํ˜ธ: runtimeMaterial ๋ถ„๋ฆฌ
        runtimeMaterial = new Material(material);

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

        for (int i = 0; i < particleCount; i++)
        {
            // [FIX #3] XZ ํ‰๋ฉด์— ๋ถ„์‚ฐ (insideUnitSphere โ†’ insideUnitCircle)
            Vector2 rnd = Random.insideUnitCircle * spawnRadius;
            positions[i]  = new Vector3(rnd.x, 0f, rnd.y) + transform.position;
            velocities[i] = new Vector3(Random.Range(-1f, 1f), 0f, Random.Range(-1f, 1f));
            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()
    {
        // [FIX #4] Edit ๋ชจ๋“œ ๋งค ํ”„๋ ˆ์ž„ Init() ์ œ๊ฑฐ โ†’ ๋ฌดํ•œ ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ ๋ฐฉ์ง€
        if (runtimeMaterial == null || positionBuffer == null) return;

        UpdateParticles();

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

        Graphics.DrawMeshInstancedIndirect(
            particleMesh,
            0,
            runtimeMaterial,
            new Bounds(center, Vector3.one * (spawnRadius * 3f)),
            argsBuffer
        );
    }

    void UpdateParticles()
    {
        float t      = Time.time;
        float smooth = 1f - Mathf.Exp(-lerpSpeed * Time.deltaTime);

        for (int i = 0; i < particleCount; i++)
        {
            Vector3 pos = positions[i];

            // โ”€โ”€ 1. Perlin ๋…ธ์ด์ฆˆ ํ”Œ๋กœ์šฐ ํ•„๋“œ (XZ ํ‰๋ฉด) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
            float nx = pos.x * flowScale;
            float nz = pos.z * flowScale;
            float nt = t * flowSpeed;

            Vector3 flow = new Vector3(
                Mathf.PerlinNoise(nx,        nt)       - 0.5f,
                0f,
                Mathf.PerlinNoise(nx + 31.7f, nt + 17.3f) - 0.5f
            ) * flowStrength;

            // โ”€โ”€ 2. ์›ํ˜• ์†Œ์šฉ๋Œ์ด (๊ณก์„  ๊ถค์ ) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
            Vector3 toCenter = pos - center;
            toCenter.y = 0f;
            Vector3 tangent = new Vector3(-toCenter.z, 0f, toCenter.x).normalized;
            Vector3 swirl   = tangent * swirlForce;

            // โ”€โ”€ 3. ์ค‘์‹ฌ ๋ณต๊ท€ (์˜์—ญ ์ดํƒˆ ๋ฐฉ์ง€) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
            float   dist    = toCenter.magnitude;
            float   pullT   = Mathf.Clamp01((dist - spawnRadius * 0.7f) / (spawnRadius * 0.3f));
            Vector3 attract = -toCenter.normalized * centerForce * pullT * dist;

            // โ”€โ”€ 4. ์ตœ์ข… ๋ชฉํ‘œ ์†๋„ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
            Vector3 desired = flow + swirl + attract;
            desired.y       = 0f;

            // ์ตœ๋Œ€ ์†๋„ ํด๋žจํ”„
            if (desired.magnitude > maxSpeed)
                desired = desired.normalized * maxSpeed;

            // ๋ถ€๋“œ๋Ÿฌ์šด ๋ฐฉํ–ฅ์ „ํ™˜ (ํ”„๋ ˆ์ž„๋ ˆ์ดํŠธ ๋…๋ฆฝ)
            velocities[i] = Vector3.Lerp(velocities[i], desired, smooth);

            // โ”€โ”€ 5. Y ํ‰๋ฉด ์œ ์ง€ + ๋ฏธ์„ธ ๋ถ€์œ ๊ฐ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
            float yNoise  = (Mathf.PerlinNoise(pos.x * 0.15f + phases[i], t * 0.4f) - 0.5f)
                            * yNoiseScale;
            velocities[i].y -= pos.y * yDamping * Time.deltaTime; // Y=0 ๋ณต๊ท€๋ ฅ

            positions[i]   += velocities[i] * Time.deltaTime;
            positions[i].y += yNoise;
        }

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

    void ReleaseBuffers()
    {
        positionBuffer?.Release(); positionBuffer = null;
        velocityBuffer?.Release(); velocityBuffer = null;
        argsBuffer?.Release();     argsBuffer     = null;

        if (runtimeMaterial != null)
        {
            if (Application.isPlaying) Destroy(runtimeMaterial);
            else                       DestroyImmediate(runtimeMaterial);
            runtimeMaterial = null;
        }
    }
}

๐Ÿ“’PaperFlowField.shader

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

// PaperFlowField.shader
Shader "Custom/PaperFlowField"
{
    Properties
    {
        _ColorA        ("Color A (base)",  Color)      = (0.95, 0.93, 0.88, 1)
        _ColorB        ("Color B (accent)",Color)      = (1.0,  0.45, 0.15, 1)
        _ColorC        ("Color C (dark)",  Color)      = (0.2,  0.18, 0.15, 1)
        _AccentRatio   ("Accent Ratio",    Range(0,1)) = 0.15
        _DarkRatio     ("Dark Ratio",      Range(0,1)) = 0.1

        _BendStrength  ("Bend Strength",   Range(0,1)) = 0.12
        _WaveSpeed     ("Wave Speed",      Float)      = 1.8
        _WaveFrequency ("Wave Frequency",  Float)      = 9.0
    }

    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;
            float4 _ColorC;
            float  _AccentRatio;
            float  _DarkRatio;

            float  _BendStrength;
            float  _WaveSpeed;
            float  _WaveFrequency;

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

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

            // ํŒŒํ‹ฐํด ID ๊ธฐ๋ฐ˜ ๊ฒฐ์ •๋ก ์  ๋žœ๋ค (uint โ†’ float ๋ช…์‹œ ์บ์ŠคํŒ…)
            float PaperHash(uint n)
            {
                return frac(sin((float)n * 127.1 + 311.7) * 43758.5453);
            }

            v2f vert(appdata v)
            {
                v2f o;

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

                // [FIX #5] velocity ์ •๊ทœํ™”: ์Šค์นผ๋ผ ๋ง์…ˆ ์ œ๊ฑฐ โ†’ ์•ˆ์ „ํ•œ normalize
                float velLen = length(vel);
                float3 forward = velLen > 0.0001
                    ? vel / velLen
                    : float3(1, 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;

                // โ”€โ”€ ๋ ˆ์ด์–ด๋“œ wave (๊ผฌ๋ฆฌ ๋์œผ๋กœ ๊ฐˆ์ˆ˜๋ก ๊ฐ•ํ•ด์ง) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
                float time  = _Time.y * _WaveSpeed;
                float phase = (float)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.55 + phase * 1.6) * _BendStrength * 0.45
                  + sin(time * 5.1 + v.uv.y * _WaveFrequency * 0.2  + phase * 3.1) * _BendStrength * 0.15;

                wave *= (0.2 + v.uv.y * 0.8);  // ๊ผฌ๋ฆฌ ๋ ๊ฐ€์ค‘์น˜

                rotated.x += wave;
                rotated.z += cos(time * 1.7 + v.uv.y * 4.0 + phase)       * _BendStrength * 0.4
                           + cos(time * 3.9 + v.uv.y * 1.5 + phase * 2.0) * _BendStrength * 0.15;

                // [FIX #6] TransformObjectToHClip โ†’ TransformWorldToHClip
                float3 worldPos = rotated + pos;
                o.positionHCS   = TransformWorldToHClip(worldPos);
                o.uv            = v.uv;
                o.normalWS      = up;
                o.colorSeed     = PaperHash(v.id);

                return o;
            }

            half4 frag(v2f i) : SV_Target
            {
                // ์ปฌ๋Ÿฌ ๋ถ„๋ฅ˜: ๋Œ€๋ถ€๋ถ„ base, ์ผ๋ถ€ accent, ์ผ๋ถ€ dark
                float3 col;
                if (i.colorSeed < _DarkRatio)
                    col = _ColorC.rgb;
                else if (i.colorSeed < _DarkRatio + _AccentRatio)
                    col = _ColorB.rgb;
                else
                    col = _ColorA.rgb;

                // UV.y ๊ธฐ๋ฐ˜ ๋๋ถ€๋ถ„ ์•ฝํ•œ ํŽ˜์ด๋“œ (์ข…์ด ๋ ๋А๋‚Œ)
                float fade = 1.0 - pow(abs(i.uv.y - 0.5) * 2.0, 3.0) * 0.3;

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

                col *= lighting * mainLight.color * fade;

                return float4(col, 1.0);
            }

            ENDHLSL
        }
    }
}

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

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