I'm making a puzzle game in Unity with sprite-based graphics. When the game is paused, I want to hide most game elements, because I don't want the player to be able to pause and leisurely think about their next move.
I could just make the sprites invisible, but I decided to do something more stylish; render sprites with pseudo-glitches, simulated in this shader:
Shader "Sprites/Glitch Sprite"
{
Properties
{
[NoScaleOffset] _MainTex ("Sprite Atlas", 2D) = "white" {}
[MaterialToggle] PixelSnap ("Pixel snap", Float) = 0
[HideInInspector] _Flip ("Flip", Vector) = (1,1,1,1)
[PerRendererData] _TextureRect ("Base Texture Rect", Vector) = (0, 0, 0, 0)
// The position and size of this sprite on the original texture atlas
}
SubShader
{
Tags
{
"Queue"="Transparent"
"IgnoreProjector"="True"
"RenderType"="Transparent"
"PreviewType"="Plane"
"CanUseSpriteAtlas"="True"
}
Cull Off
Lighting Off
ZWrite Off
Blend One OneMinusSrcAlpha
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma target 3.0
#pragma multi_compile_instancing
#pragma multi_compile _ PIXELSNAP_ON
#pragma multi_compile _ ETC1_EXTERNAL_ALPHA
#pragma instancing_options assumeuniformscaling
// Sprites will not scale in my game
#pragma instancing_options nolightmap
#pragma instancing_options nolightprobe
// My game does not use lighting (it's all pixel art)
#include "UnitySprites.cginc"
UNITY_INSTANCING_BUFFER_START(Props)
UNITY_DEFINE_INSTANCED_PROP(float4, _TextureRect)
// Sprites are GPU-instanced where supported
UNITY_INSTANCING_BUFFER_END(Props)
// These four properties are set once, when the game is first loaded
uniform float4 _MainTex_TexelSize;
uniform float2 _InitialSeed;
uniform float4 _SpriteRectChunks[128];
// Coordinates of each 8x8 sprite chunk I want to use in the shader
// x,y are the positions (in pixels) of the chunk on the original atlas, z,w are unused and set to 0
uniform int _SpriteRectChunkCount = 0;
// Not all of these vectors are used, so I also pass in how many actually are
struct vert2frag_img
{
float4 pos : SV_POSITION;
half2 uv : TEXCOORD0;
nointerpolation float4 chunk : POSITION1;
// The chunk selection is done in the vertex shader
UNITY_VERTEX_INPUT_INSTANCE_ID
UNITY_VERTEX_OUTPUT_STEREO
};
// Taken from https://stackoverflow.com/a/3451607/1089957
float2 remap(float2 value, float2 low1, float2 high1, float2 low2, float2 high2)
{
return low2 + (value - low1) * (high2 - low2) / (high1 - low1);
}
// Given a vec2 as a seed, return a number from [0, 1)
// Taken from https://stackoverflow.com/a/10625698/1089957
float random(float2 seed)
{
const float2 r = float2(
23.1406926327792690,
2.6651441426902251
);
return frac(cos(fmod(123456789., 1e-7 + 256. * dot(seed, r))));
}
int random(float2 seed, int limit) {
return (int)round(random(seed) * limit);
}
vert2frag_img vert(appdata_img IN)
{
vert2frag_img OUT;
UNITY_SETUP_INSTANCE_ID(IN);
UNITY_TRANSFER_INSTANCE_ID(IN, OUT);
UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(OUT);
OUT.pos = UnityFlipSprite(IN.vertex, _Flip);
OUT.pos = UnityObjectToClipPos(OUT.pos);
OUT.uv = IN.texcoord;
float2 position = mul(unity_ObjectToWorld, float4(0, 0, 0, 1)).xy;
#ifdef UNITY_INSTANCING_ENABLED
int index = random(round(position * _InitialSeed * 8 + UNITY_GET_INSTANCE_ID(IN)), _SpriteRectChunkCount);
#else
int index = random(round(position * _InitialSeed * 8), _SpriteRectChunkCount);
#endif
OUT.chunk = _SpriteRectChunks[index];
#ifdef PIXELSNAP_ON
OUT.pos = UnityPixelSnap(OUT.pos);
#endif
return OUT;
}
fixed4 frag(vert2frag_img IN) : SV_Target
{
UNITY_SETUP_INSTANCE_ID(IN);
float2 pixels = remap(IN.uv, 0, 1, 0, _MainTex_TexelSize.zw);
// texture coords (range [0, 1]) -> texture pixels (range [0, TextureSize])
pixels = pixels - UNITY_ACCESS_INSTANCED_PROP(Props, _TextureRect).xy;
// Sprite pixel coordinate, relative to (0, 0)
pixels = fmod(pixels, 8);
// Display glitched sprites in 8x8 chunks
pixels = pixels + IN.chunk.xy;
// Back to the sprite
pixels = remap(pixels, 0, _MainTex_TexelSize.zw, 0, 1);
// Remap to normalized coordinates
fixed4 c = tex2D(_MainTex, clamp(pixels, 0, 1));
c.rgb *= c.a;
return c;
}
ENDCG
}
}
}
There's a lot of window dressing, but the actual shader code in the CGPROGRAM
block is standard HLSL. This is a screenshot of my game without this shader:
And this is the same scene, with the shader:
Here are some relevant facts:
- All sprites are stored on a single texture atlas.
- All sprites have a length and width that are a multiple of 8. But their position on the texture atlas might not be.
- Some sprites in the second screenshot are invisible, even though they should be glitchy like the ones that are. While this is technically a bug, it's consistent with the style I'm going for, so I will probably leave it as is. If you happen to have a fix to suggest, I'll try it out and see how it looks, but that's not the basis of my question.
- I intend to release this game on a variety of platforms, so make no assumptions about what graphics API (DirectX, OpenGL, etc.) is actually being used. However, this shader will always be written in HLSL (Unity compiles it to other languages as needed).
These are my questions:
- Are there ways I can optimize this shader?
I'm looking for changes I can make in either the actual HLSL code or the surrounding ShaderLab declarations.