개요
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
'Unity > TA' 카테고리의 다른 글
Unity Linear ColorSpace에서 UI 알파가 다르게 보이는 이슈 (3) | 2024.01.02 |
---|---|
URP 렌더피처를 활용한 Sprite 개별 블러 효과 구현 (3) | 2023.12.05 |
SRP Batcher GLES & Vulkan 비교 (0) | 2023.11.14 |
CustomInspector CategoryHeaderGUI - 어두운 폴드바GUI (0) | 2023.10.12 |
Unity 2023-URP 16 렌더피처 API 변경 정리 (0) | 2023.09.22 |
WRITTEN BY
- CatDarkGame
Technical Artist dhwlgn12@gmail.com