- Home /
Shaders - offset texture coordinates by a single pixel
I'm trying to make an outline shader for sprites, so I need to check adjacent pixels in the texture. However, the tex2d() shader function expects 0..1 for x/y coordinates, so if I want to offset by a single pixel, I would have to know the texture's size. Since I use unity's sprite packer, these are usually fairly large sprites.
Is there a way to check a neighboring pixel in the texture? Is there a way to get the texture size in pixels, so I can calculate this myself?
Answer by _Gkxd · Aug 27, 2015 at 12:55 AM
I adapted the default sprite shader to do roughly what you want:
Shader "Custom/OutlinedSprite"
{
Properties
{
[PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {}
_Color ("Tint", Color) = (1,1,1,1)
_OutlineColor ("Outline", Color) = (1,1,1,1)
[MaterialToggle] PixelSnap ("Pixel snap", Float) = 0
}
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 multi_compile _ PIXELSNAP_ON
#include "UnityCG.cginc"
struct appdata_t
{
float4 vertex : POSITION;
float4 color : COLOR;
float2 texcoord : TEXCOORD0;
};
struct v2f
{
float4 vertex : SV_POSITION;
fixed4 color : COLOR;
half2 texcoord : TEXCOORD0;
};
fixed4 _Color;
fixed4 _OutlineColor;
float _TexWidth;
float _TexHeight;
v2f vert(appdata_t IN)
{
v2f OUT;
OUT.vertex = mul(UNITY_MATRIX_MVP, IN.vertex);
OUT.texcoord = IN.texcoord;
OUT.color = IN.color * _Color;
#ifdef PIXELSNAP_ON
OUT.vertex = UnityPixelSnap (OUT.vertex);
#endif
return OUT;
}
sampler2D _MainTex;
float4 _MainTex_TexelSize;
fixed4 frag(v2f IN) : SV_Target
{
fixed4 c = tex2D(_MainTex, IN.texcoord) * IN.color;
if (c.a == 0) {
return fixed4(0, 0, 0, 0); // Skip outline for transparent pixels
}
// Get the colors of the surrounding pixels
fixed4 up = tex2D(_MainTex, IN.texcoord + fixed2(0, _MainTex_TexelSize.y));
fixed4 down = tex2D(_MainTex, IN.texcoord - fixed2(0, _MainTex_TexelSize.y));
fixed4 left = tex2D(_MainTex, IN.texcoord - fixed2(_MainTex_TexelSize.x, 0));
fixed4 right = tex2D(_MainTex, IN.texcoord + fixed2(_MainTex_TexelSize.x, 0));
// This method uses an if statement
if (up.a * down.a * left.a * right.a == 0) {
c.rgb = _OutlineColor.rgb;
}
/* This method doesn't use an if statement, but it won't work for sprites with semi-transparency.
I prefer the previous method because I don't notice any performace difference between the two.
float isNotOutline = up.a * down.a * left.a * right.a;
c.rgb = isNotOutline * c.rgb + (1-isNotOutline) * _OutlineColor;
*/
c.rgb *= c.a;
return c;
}
ENDCG
}
}
}
The outline here will appear inside the sprite. It's pretty easy to change so that the outline is on the outside of the sprite, but there are some edge cases that you have to handle when you're at the edge of a sprite.
Here is the original sprite, rendered using a normal sprite renderer:
Here is the sprite with the sprite renderer's material set to one that uses the outline shader:
Edit:
To answer your question, float4 _MainTex_TexelSize;
gets a few numbers that are relevant to the texture called _MainTex
.
It gives you a 4-vector where the first two elements are the size of one pixel in uv coordinates, and the last 2 elements is the resolution of the texture.
Perfect!
I guess _$$anonymous$$aintex_TexelSize is something that Unity sets per texture? Is this documented somewhere? I found this link http://docs.unity3d.com/$$anonymous$$anual/SL-UnityShaderVariables.html but there's nothing there about _TexelSize...
There's this Twitter post from someone at Unity, which I saw in some other answer before: https://twitter.com/aras_p/status/258567507487637504
Other than that, I couldn't find any "official" documentation either.
Answer by IgorAherne · Aug 26, 2015 at 09:25 PM
-how to offset by 1 pixel http://vvvv.org/documentation/tutorial-effects-neighbouring-pixels
how to set up the offset http://answers.unity3d.com/questions/256638/acessing-a-global-variable-in-shader.html
I am not familiar with sprites, but I understand that you would like to have different uniform variables per different meshes. Ideally you would want to use 1 material to avoid any additional draw calls.
Whatever the case is, you are applying a texture on triangles of a mesh (quad etc).
A mesh is made of vertices which carry position, uv coordinates and a color.
You can color code the meshes' vertices with color. In shader, have several uniforms (20, 40, doesn't matter, uniforms are cheap). Test the for the color of vertex and use the appropriate offset uniform. Say if you see that the vertex is yellow - apply the offset with offset = 0.2, else if pinkish = 0.3 etc
.
That way you can tweak the values once, setting up all the uniforms via the c# script or directly in the editor via properties of material, ensuring that they look good on all the quads and save on the drawcalls, having just 1 material.
You will have to look into optimizing dynamic branching. Shaders don't like loops and if/else statements. See how you can implement step / mix() functions
Thanks, I actually read that one, but I find it problematic to have to create a new material for every sprite to set it's size... Isn't there a way to do this from within the shader given the texture?
I found this reference: http://http.developer.nvidia.com/Cg/tex2Dsize.html but it doesn't seem to work within unity.
Thanks for your explanation. I'm familiar with the basics of meshes and shaders, the main issue was looking at a neighboring pixel of the texture given the uv coordinates of the current fragment only. See _Gkxd's answer.
Your answer
Follow this Question
Related Questions
Are shaders more efficient than manual pixel replacement? 0 Answers
How to use a shader to write to a mipmap (partial mipmap update) 2 Answers
Mobile Shader with Albedo Color and Normal Map 0 Answers
Shader on 3D model not seen over 2D sprites 0 Answers
There is an issue with texture wrapping around sphere's in unity 0 Answers