개요

Unity SpriteRenderer에 커스텀 쉐이더를 사용할 수 있지만 MaterialPropertyBlock을 사용하는 경우 Dynamic Batcing이 묶이지 않기 때문에 사용이 제한됩니다.

Particle System의 CustomVertexSteam, UGUI의 BaseMeshEffect 클래스와 같이 Vertex Attributes를 추가하는 방식으로 커스텀 쉐이더를 사용해도 배칭 이슈가 발생하지 않는데 SpriteRenderer는 기본 Vertex Color 외에 다른 방법이 없습니다.

 

이번 포스팅은 SpriteRenderer에 Vertex Attributes를 추가하는 방법에 대한 R&D 내용을 작성합니다.

 

 

Sprite.SetVertexAttribute 활용

void ModifyVertexAttribute(SpriteRenderer spriteRenderer)
{
    Sprite sprite = spriteRenderer.sprite;
    if(sprite == null) return;
    
    int vertexCount = sprite.GetVertexCount();
    NativeArray<Vector2> texcoords2 = new NativeArray<Vector2>(vertexCount, Allocator.Temp);

    for (int i = 0; i < vertexCount; i++)
    {
        texcoords2[i] = new Vector2(_blurPower, _BlendAmount);
    }

    sprite.SetVertexAttribute(VertexAttribute.TexCoord1, texcoords2);
    texcoords2.Dispose();

    spriteRenderer.sprite = sprite;
}

 

위와 같이 Sprite.SetVertexAttribute 함수를 통해 Sprite의 버텍스 정보를 수정할 수 있습니다.

예제에서는 Texcoord1 항목을 추가했으며 쉐이더에서는 TEXCOORD1 값을 받아 Fragment로 전달하여 값을 활용합니다.

 

 

결과, 의도한대로 Vertex Attribute로 값을 전달하여 작동까지 되며, 배칭이 유지되는 것을 확인할 수 있습니다.

 

 

한계점 - Sprite 에셋을 새로 생성 필요

Unity Sprite의 내부 구조를 확인할 수 없지만, Sprite 에셋 자체가 Mesh처럼 버텍스 정보를 포함하고 있는 것 같습니다.

그래서 이 방식이 동작하기 위해서는 VertexAttribute를 수정하는 Sprite마다 에셋을 새로 생성해야 하며 메모리 부담이 높아지는 위험성이 있습니다.

 

 

결론

이 포스팅에서 소개한 VertexAttribute 방식은 메모리 이슈로 사용하면 안됩니다.

직접 SpriteRenderer 시스템을 구현하는 방법이 가장 적절할 것으로 판단합니다. (맨 아래 레퍼런스 참고)

 

*Unity 2023.1부터 SpriteRenderer에 SRPBatcher가 지원됩니다. (23.12.04기준 아직 Beta버전)

 

 

예제 전체 코드

SpriteRenderer에 붙이는 스크립트

using UnityEngine;
using UnityEngine.U2D;
using UnityEngine.Rendering;
using Unity.Collections;
#if UNITY_EDITOR
using UnityEditor;
#endif


namespace CatDarkGame
{
    //[ExecuteInEditMode]
    [RequireComponent(typeof(SpriteRenderer))]
    [DisallowMultipleComponent]
    public class SpriteBlurRenderer : MonoBehaviour
    {
        [SerializeField][HideInInspector] private Material _materialAsset = null;
        [SerializeField][HideInInspector] private SpriteRenderer _spriteRenderer;
        private Sprite _sprite;
        
        [Range(0.01f, 16)][SerializeField] private float _blurPower = 2.0f;
        [Range(0, 1)][SerializeField] private float _BlendAmount = 0.5f;
        [SerializeField] private bool _customVertexStream = false;

        private void OnEnable()
        {
#if UNITY_EDITOR
            if (!_materialAsset)
            {
                string assetPath = GetScriptAssetPath + "/Material/CatDarkGame_Sprites_Sprite_Blur.mat";
                Debug.Log(assetPath);
                _materialAsset = AssetDatabase.LoadAssetAtPath<Material>(assetPath);
            }
#endif
            if (!_spriteRenderer) _spriteRenderer = GetComponent<SpriteRenderer>();
            _spriteRenderer.material = _materialAsset;


            // Sprite 에셋을 새로 생성하는 방식, (메모리 증가 위험)
            // Unity Sprite 시스템은 Sprite 에셋 = Mesh 역할.
            // 결론 : 이 방식도 올바른 솔루션이 아님
            if (_spriteRenderer && !_sprite)
            {
                _sprite = Instantiate(_spriteRenderer.sprite);
                _sprite.hideFlags = HideFlags.HideAndDontSave;
                _sprite.name = _sprite.name.Replace("(Clone)", "");
                _spriteRenderer.sprite = _sprite;
            }

        }

        private bool CheckCanUpdate()
        {
            if (!_spriteRenderer) return false;
            return true;
        }

        private void Update()
        {
            if (!CheckCanUpdate()) return;
            
            if (!_customVertexStream)
            {
                _materialAsset.DisableKeyword("_CUSTOMVERTEXSTREAM");
                MaterialPropertyBlock mpb = new MaterialPropertyBlock();
                _spriteRenderer.GetPropertyBlock(mpb);
                mpb.SetFloat("_BlurPower", _blurPower);
                mpb.SetFloat("_BlendAmount", _BlendAmount);
                _spriteRenderer.SetPropertyBlock(mpb);
            }
            else
            {
                _materialAsset.EnableKeyword("_CUSTOMVERTEXSTREAM");
                ModifyVertexAttribute(_spriteRenderer);
            }
        }

        void ModifyVertexAttribute(SpriteRenderer spriteRenderer)
        {
            Sprite sprite = spriteRenderer.sprite;
            if(sprite == null) return;
            
            int vertexCount = sprite.GetVertexCount();
            NativeArray<Vector2> texcoords2 = new NativeArray<Vector2>(vertexCount, Allocator.Temp);

            for (int i = 0; i < vertexCount; i++)
            {
                texcoords2[i] = new Vector2(_blurPower, _BlendAmount);
            }

            sprite.SetVertexAttribute(VertexAttribute.TexCoord1, texcoords2);
            texcoords2.Dispose();

            spriteRenderer.sprite = sprite;
        }


#if UNITY_EDITOR
        private string GetScriptAssetPath
        {
            get
            {
                string path = AssetDatabase.FindAssets($"t:Script {nameof(SpriteBlurRenderer)}")[0];
                path = AssetDatabase.GUIDToAssetPath(path);
                return path.Substring(0, path.LastIndexOf("/"));
            }
        }
#endif
    }
}

 

 

쉐이더

Shader "CatDarkGame/Sprites/Sprite_Blur"
{
    /* 
       
    */
    Properties
    {
        //[PerRendererData] 
        _MainTex ("Sprite Texture", 2D) = "white" {}

        _BlurPower("Blur Power", Range(0.01, 16)) = 2
        _BlendAmount("Blend Amount", Range(0,1)) = 0.5

         [Toggle(_CUSTOMVERTEXSTREAM)]_EnableCustomVertexStream("Enable CustomVertexStream", float) = 0

        // Legacy properties. They're here so that materials using this shader can gracefully fallback to the legacy sprite shader.
        [HideInInspector] _Color ("Tint", Color) = (1,1,1,1)
        [HideInInspector] PixelSnap ("Pixel snap", Float) = 0
        [HideInInspector] _RendererColor ("RendererColor", Color) = (1,1,1,1)
        [HideInInspector] _Flip ("Flip", Vector) = (1,1,1,1)
        [HideInInspector] _AlphaTex ("External Alpha", 2D) = "white" {}
        [HideInInspector] _EnableExternalAlpha ("Enable External Alpha", Float) = 0
    }

    SubShader
    {
        Tags {"Queue" = "Transparent" "RenderType" = "Transparent" "RenderPipeline" = "UniversalPipeline" }

        Blend SrcAlpha OneMinusSrcAlpha
        Cull Off
        ZWrite Off


        Pass
        {
            //Tags { "LightMode" = "Universal2D" }
            Tags { "LightMode" = "UniversalForward" "Queue"="Transparent" "RenderType"="Transparent"}

            HLSLPROGRAM
            #pragma target 4.5
            #pragma multi_compile_instancing
            #pragma multi_compile_fragment _ DEBUG_DISPLAY

            #pragma shader_feature _CUSTOMVERTEXSTREAM

            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
            #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/UnityInstancing.hlsl"
            #include "HLSL/CommonUtil.hlsl"
            #if defined(DEBUG_DISPLAY)
                #include "Packages/com.unity.render-pipelines.universal/Shaders/2D/Include/InputData2D.hlsl"
                #include "Packages/com.unity.render-pipelines.universal/Shaders/2D/Include/SurfaceData2D.hlsl"
                #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Debug/Debugging2D.hlsl"
            #endif

            #pragma vertex UnlitVertex
            #pragma fragment UnlitFragment

            struct Attributes
            {
                float3 positionOS   : POSITION;
                float4 color        : COLOR;
                float2 uv           : TEXCOORD0;
                float2 custom           : TEXCOORD1;
                UNITY_VERTEX_INPUT_INSTANCE_ID
            };

            struct Varyings
            {
                float4  positionCS      : SV_POSITION;
                float4  color           : COLOR;
                float2  uv              : TEXCOORD0;
                float2 custom           : TEXCOORD1;
                #if defined(DEBUG_DISPLAY)
                    float3  positionWS      : TEXCOORD2;
                #endif
                UNITY_VERTEX_OUTPUT_STEREO
                UNITY_VERTEX_INPUT_INSTANCE_ID
            };

            TEXTURE2D(_MainTex); SAMPLER(sampler_MainTex);
            float4 _MainTex_ST;
           
            float4 _Color;
            half4 _RendererColor;
           
           
            #ifdef UNITY_INSTANCING_ENABLED
                UNITY_INSTANCING_BUFFER_START(Props)
                    UNITY_DEFINE_INSTANCED_PROP(half, _BlurPower)
                    UNITY_DEFINE_INSTANCED_PROP(half, _BlendAmount)

                UNITY_INSTANCING_BUFFER_END(Props)

                #define _BlurPower      UNITY_ACCESS_INSTANCED_PROP(Props, _BlurPower)
                #define _BlendAmount    UNITY_ACCESS_INSTANCED_PROP(Props, _BlendAmount)
            #endif 

           CBUFFER_START(UnityPerDrawSprite)
            #ifndef UNITY_INSTANCING_ENABLED
                half _BlurPower;
                half _BlendAmount;
            #endif
            CBUFFER_END

            Varyings UnlitVertex(Attributes attributes)
            {
                Varyings o = (Varyings)0;
                UNITY_SETUP_INSTANCE_ID(attributes);
                UNITY_TRANSFER_INSTANCE_ID(attributes, o);
                UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);

                o.positionCS = TransformObjectToHClip(attributes.positionOS);
                #if defined(DEBUG_DISPLAY)
                    o.positionWS = TransformObjectToWorld(attributes.positionOS);
                #endif
                o.uv = TRANSFORM_TEX(attributes.uv, _MainTex);
                o.color = attributes.color * _Color * _RendererColor;

                o.custom = attributes.custom;
                return o;
            }


            float4 UnlitFragment(Varyings i) : SV_Target
            {
                UNITY_SETUP_INSTANCE_ID(i);
               
                float2 uv = i.uv;
                float4 mainTex = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, uv);
                
                float3 blurCol = 0;
                float blurAlpha = 0;
                half blurPower = _BlurPower;
                #ifdef _CUSTOMVERTEXSTREAM
                    blurPower = i.custom.x;
                #endif
                SumBlur_float(TEXTURE2D_ARGS(_MainTex, sampler_MainTex), uv, blurPower, blurCol, blurAlpha);

                float4 result = 1;
                half blendAmount = _BlendAmount;
                #ifdef _CUSTOMVERTEXSTREAM
                    blendAmount = i.custom.y;
                #endif
                half smoothAmount = smoothstep(0.0, 1, blendAmount); // _BlendAmount;
                result = lerp(mainTex, float4(blurCol, blurAlpha), smoothAmount) * i.color;

                
                #if defined(DEBUG_DISPLAY)
                    SurfaceData2D surfaceData;
                    InputData2D inputData;
                    half4 debugColor = 0;

                    InitializeSurfaceData(result.rgb, result.a, surfaceData);
                    InitializeInputData(i.uv, inputData);
                    SETUP_DEBUG_DATA_2D(inputData, i.positionWS);

                    if(CanDebugOverrideOutputColor(surfaceData, inputData, debugColor))
                    {
                        return debugColor;
                    }
                #endif

                return result;
            }
            ENDHLSL
        }
    }


    Fallback "Sprites/Default"
}

 

 

 

레퍼런스

유니티 포럼 : Additional Per-Vertex Data for SpriteRenderer?

ECS Instanced SpriteRenderer github

Entitiy 시스템으로 SpriteRenderer 구현

Unity Sprite GPU Instance github

 

 


WRITTEN BY
CatDarkGame
Technical Artist dhwlgn12@gmail.com

,