I was working on 2D sprite rendering DOTS + URP compatible solution. Using regular URP (not 2D) to render sprites. There is some ugly things in my system which I want to try to get rid of.
In my system I can render sprites with different materials/shaders, but to do that I need to collect all, then sort and finally render in resulted order. Because, as I mentioned, there can be more then one shader, then we can have different passes. I’ve decided to get LTW of sprites and shift position’s Z a bit to display sprites in order. For now it is .0001f per position in order, because camera can’t recognize difference below that. I’m not a huge fan of some hidden constant values, but such solution also brings me a need to adjust camera’s clipping planes to be able to fit all rendered sprites + editor and runtime camera have a different Z shift minimum + in scene view with 3D mode enabled rendering result looks weird, like some kind of accordion, which is getting recognizable with a lot of sprites on a scene.
In general i just change LTW’s Z position I passing to shader’s matrices buffer to be able to render passes all at once and remain sprites in proper order.
So is there any good clean ways to do what I want to do?
Shader code:
//TODO: try to simplify #if defined strings
Shader "Universal Render Pipeline/2D/General Sprite Shader"
{
Properties
{
_MainTex("_MainTex", 2D) = "white" {}
}
HLSLINCLUDE
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
CBUFFER_START(UnityPerMaterial)
//set here material properties
CBUFFER_END
ENDHLSL
SubShader
{
Tags {"Queue" = "Transparent" "RenderType" = "TransparentCutout" "RenderPipeline" = "UniversalPipeline" }
Blend SrcAlpha OneMinusSrcAlpha
Cull Off
ZWrite On
Pass
{
Tags { "LightMode" = "UniversalForward" "Queue" = "Transparent" "RenderType" = "TransparentCutout"}
HLSLPROGRAM
#pragma vertex UnlitVertex
#pragma fragment UnlitFragment
#pragma target 4.5
#pragma exclude_renderers gles gles3 glcore
#pragma multi_compile_instancing
#pragma instancing_options procedural:setup
struct Attributes
{
float3 positionOS : POSITION;
float2 uv : TEXCOORD0;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct Varyings
{
float4 positionCS : SV_POSITION;
float2 uv : TEXCOORD0;
float4 mainTexAtlasST : ATLASST;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
TEXTURE2D(_MainTex);
SAMPLER(sampler_MainTex);
#if defined(UNITY_INSTANCING_ENABLED) || defined(UNITY_PROCEDURAL_INSTANCING_ENABLED) || defined(UNITY_STEREO_INSTANCING_ENABLED)
StructuredBuffer<float4x4> _transformMatrixBuffer;
StructuredBuffer<float4> _mainTexSTOnAtlasBuffer; //ST means Scale + Translation
StructuredBuffer<float4> _mainTexSTBuffer;
StructuredBuffer<int> _flipBuffer;
#endif
void setup()
{
#if defined(UNITY_INSTANCING_ENABLED) || defined(UNITY_PROCEDURAL_INSTANCING_ENABLED) || defined(UNITY_STEREO_INSTANCING_ENABLED)
unity_ObjectToWorld = _transformMatrixBuffer[unity_InstanceID];
#endif
}
float2 TilingAndOffset(float2 UV, float2 Tiling, float2 Offset)
{
//Tiling is like Width/Height ratio, like how much texture should be stratched
//offset is just regular offset from 0,0
//so when UV.x is 0/1 it is left/right UV coords of renderer rect
return UV * Tiling + Offset;
}
Varyings UnlitVertex(Attributes attributes, uint instanceID : SV_InstanceID)
{
Varyings varyings = (Varyings)0;
//extract all CBuffer data here
#if defined(UNITY_INSTANCING_ENABLED) || defined(UNITY_PROCEDURAL_INSTANCING_ENABLED) || defined(UNITY_STEREO_INSTANCING_ENABLED)
varyings.mainTexAtlasST = _mainTexSTOnAtlasBuffer[instanceID];
float4 mainTexST = _mainTexSTBuffer[instanceID];
int flipValue = _flipBuffer[instanceID];
#else
//fallback if somehow instancing failed
varyings.mainTexAtlasST = float4(1, 1, 0, 0);
float4 mainTexST = float4(1, 1, 0, 0);
int flipValue = 0;
#endif
UNITY_SETUP_INSTANCE_ID(attributes);
UNITY_TRANSFER_INSTANCE_ID(attributes, varyings);
varyings.positionCS = TransformObjectToHClip(attributes.positionOS);
float2 uv = attributes.uv;
//flip uv if necessary
uv.x = flipValue >= 0 ? uv.x : (1.0 - uv.x);
//tiling and offset uv
uv = TilingAndOffset(uv, mainTexST.xy, mainTexST.zw);
//pass uv to fragment shader
varyings.uv = uv;
return varyings;
}
float4 UnlitFragment(Varyings varyings) : SV_Target
{
//finally frac uv and locate on atlas using tiling and offset
varyings.uv = TilingAndOffset(frac(varyings.uv), varyings.mainTexAtlasST.xy, varyings.mainTexAtlasST.zw);
float4 texColor = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, varyings.uv);
clip(texColor.w - 0.5);
return texColor;
}
ENDHLSL
}
}
Fallback "Sprites/Default"
}
Adjusting LTWs job code. Just to show what I actually do, look at PER_INDEX_OFFSET * index
[BurstCompile]
internal struct FillMatricesArrayJob : IJobParallelFor
{
[ReadOnly] public NativeArray<SpriteData> spriteDataArray;
[ReadOnly] public NativeList<RenderArchetypeForSorting> archetypeLayoutData;
[WriteOnly][NativeDisableParallelForRestriction] public NativeArray<float4x4> matricesArray;
private const float PER_INDEX_OFFSET = .0001f; //below this value camera doesn't recognize difference
public void Execute(int index)
{
var spriteData = spriteDataArray[index];
var renderPosition = spriteData.position - spriteData.scale * spriteData.pivot;
matricesArray[archetypeLayoutData[spriteData.archetypeIncludedIndex].stride + spriteData.entityInQueryIndex] = float4x4.TRS
(
new float3(renderPosition.x, renderPosition.y, PER_INDEX_OFFSET * index),
quaternion.identity,
new float3(spriteData.scale.x, spriteData.scale.y, 1f)
);
}
}
Example of how it looks like in scene view