I am very new to shaders and I achieved the following through some trial and error and need some help figuring out where I’m wrong in my thinking or atleast understand why it works the way it does at the moment. Any hint or answer to any of the questions will be much appreciated
I’m trying to create a dot matrix shader meaning it will take in a custom “Space” parameter and spread all pixels of a sprite so they have this space between them.
This is what I have so far which sort of works
fixed4 frag (v2f i) : COLOR
{
i.uv += i.uv;
float spacingX = _MainTex_TexelSize.x * _Spacing;
float spacingY = _MainTex_TexelSize.y * _Spacing;
float totalSpaceX = (_MainTex_TexelSize.z ) * spacingX;
float totalSpaceY = (_MainTex_TexelSize.w ) * spacingY;
// Calculate uv units to 1 texture unit with spacing for final render
float xPuvFinal = 1 / (_MainTex_TexelSize.z /*+ totalSpaceX*/); // Works wierd when I add totalSpace
float yPuvFinal = 1 / (_MainTex_TexelSize.w /* +totalSpaceY */);
// Calculate uv units to 1 texture unit in original texture
float x = 1 / _MainTex_TexelSize.z;
float y = 1 / _MainTex_TexelSize.w;
// Calculate which pixel in the final render is it
int xFinalPixel = floor(i.uv.x / xPuvFinal);
int yFinalPixel = floor(i.uv.y / yPuvFinal);
half4 c;
if(i.uv.x >= (xFinalPixel * xPuvFinal + spacingX) && i.uv.y >= (yFinalPixel * yPuvFinal + spacingY))
{
// Calculates which texture pixel to use
fixed xOffset = xFinalPixel / 2 * x;
fixed yOffset = yFinalPixel / 2 * y;
c = tex2D(_MainTex, i.uv - float2(xOffset, yOffset));
c.rgb *= c.a;
}
else // Blank
{
c = float4(0,0,0,0);
}
return c;
}
It does the following steps:
Calculates space for x and y by multiplying with texel dimensions
Calculates how much space exists in total in the final image
Calculates how much UV is being given to every final “pixel” of the sprite on screen
Calculate how much UV is being give to every original pixel of the sprite
Find out which “current pixel” are you drawing right now using what we found (step 3)
Check if the uv is smaller than the “current pixel” multiplied by how much uv per “pixel” (step 3) + space (step 1), then draw the right pixel from texture(found in step 4) otherwise, leave blank.
Still I have a few things I can’t wrap my mind around:
Logic dictates that I should take into account the total space in order to complete step 3 but I found out it works better without it. Meaning step 3 and 4 are the same and are redundant.
The effect I achieved makes it so playing with the “_Space” parameter makes the pixels themselves shrink and grow as opposed to making the space between them smaller or bigger:
Also for some reason every pixel of the sprite is being represented by 4 “pixels”. Only thing I can think of the may cause is me multiplying the uv by 2 at the start - which is a leftover from an earlier version of this where the space was always 1 pixel. Even now without doubling the UV the sprite is somewhere outside of the target
I think you’re over complicating things for yourself. You could set the sprite texture to be point sampled and then just use tex2D(_MainTex, i.uv) as is without anything fancy. Then you can calculate the distance to the pixel edges with a little math and use clip() to skip those pixels.
Something like this:
Shader "Unlit/pixelDots"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_Spacing ("Spacing", Range(0,1)) = 0.25
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float4 vertex : SV_POSITION;
float2 uv : TEXCOORD0;
};
sampler2D _MainTex;
float4 _MainTex_ST;
float4 _MainTex_TexelSize;
float _Spacing;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
// uvs * texture resolution
float2 texelUV = i.uv * _MainTex_TexelSize.zw;
// set texture to point filtering, sample normally
fixed4 col = tex2D(_MainTex, i.uv);
// optional force texture to be sampled as if it's using point filtering
// float2 pointUVs = (floor(texelUV) + 0.5) * _MainTex_TexelSize.xy;
// fixed4 col = tex2D(_MainTex, pointUVs);
// rescale each texel to a -1 to 1 range, then get the absolute of that.
// this gets a distance from the center of the "pixel", where the edges are 1
// and the center is 0.
float2 pixelUV = frac(texelUV) * 2.0 - 1.0;
// get the max distance for a square dot
float pixelCenterDist = max(abs(pixelUV.x), abs(pixelUV.y));
// alternative if you want round dots, scaled so a spacing of 0 has no holes
// float pixelCenterDist = length(pixelUV) * 0.70716;
// clip the pixel distance value
clip(1 - _Spacing - pixelCenterDist);
return col;
}
ENDCG
}
}
}
Alternatively, here’s a version that uses a smooth alpha blended & anti-aliased edge based on screen space derivatives:
Shader "Unlit/pixelDotAA"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_Spacing ("Spacing", Range(0,1)) = 0.25
}
SubShader
{
Tags { "Queue"="Transparent" "RenderType"="Transparent" }
LOD 100
Blend SrcAlpha OneMinusSrcAlpha
ZWrite Off
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float4 vertex : SV_POSITION;
float2 uv : TEXCOORD0;
};
sampler2D _MainTex;
float4 _MainTex_ST;
float4 _MainTex_TexelSize;
float _Spacing;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
// uvs * texture resolution
float2 texelUV = i.uv * _MainTex_TexelSize.zw;
// set texture to point filtering, sample normally
fixed4 col = tex2D(_MainTex, i.uv);
// optional force texture to be sampled as if it's using point filtering
// float2 pointUVs = (floor(texelUV) + 0.5) * _MainTex_TexelSize.xy;
// fixed4 col = tex2D(_MainTex, pointUVs);
// rescale each texel to a -1 to 1 range, then get the absolute of that.
// this gets a distance from the center of the "pixel", where the edges are 1
// and the center is 0.
float2 pixelUV = frac(texelUV) * 2.0 - 1.0;
// get the max distance for a square dot
float pixelCenterDist = max(abs(pixelUV.x), abs(pixelUV.y));
// alternative if you want round dots, scaled so a spacing of 0 has no holes
// float pixelCenterDist = length(pixelUV) * 0.70716;
// get screen space derivatives of the pixel center distance
float derivDist = fwidth(pixelCenterDist);
// use derivatives to sharpen edge
col.a *= smoothstep(_Spacing - derivDist * 0.5, _Spacing + derivDist * 0.5, 1 - pixelCenterDist);
return col;
}
ENDCG
}
}
}
Thank you that’s a perfect answer! I didn’t know clip was a thing and didn’t think of just checking distance from borders. This way the space is also applied equally to all sides of the pixel.
Originally I wanted to keep the pixels at their correct size and the space not take from the pixel itself but thinking it it through this is better since you don’t need to account for the sprite drifting off on the uv.
Again, thank you. I learned a lot from your detailed answer !