It has been solved, link to the final shader: Shader

The problem (as portrayed by the image below) is that the pixels seem to be, for lack of a better word, cut off. I wish to either display a pixel, or not.


My fragment function, it uses the red value of the vertex colors (displayed by the image blow) on the edge of the mesh which are assigned during mesh generation to determine the pixel cutout "weight" by multiplying it with the texture.

fixed4 frag (v2f i) : SV_Target
    fixed4 tex = tex2D(_MainTex, i.uv);

    if (i.color.r * tex.r > 0.4f) discard;

    return fixed4(tex.rgb * clamp(i.color.r, 0.3f, 1.0f), 1.0f);

Vertex colors


The reason this happens is that your fragment shader doesn't run just once for each texel in your input texture. It runs once for each fragment in your output buffer (eg. each pixel on the screen).

So when you're drawing big, pixelated art zoomed in, you get multiple shader evaluations for each texel in the source. Each of those evaluations gets its own smoothly interpolated input from the vertex shader, like the vertex colours, to give you smooth gradients. That means the fragments at one side of a texel can get different inputs, and come to a different conclusions about whether to draw or clip, than fragments at the other side of the same texel, leading to the cutoff splitting the texel midway along.

To fix this with textures, we use Nearest / Point filtering mode to snap our texture samples to the center of the nearest texel. For interpolated data, we'll need to emulate that snapping behaviour.

Animated example of texel-by-texel dissolve using emulated snapping

First, we need to measure which way the colour gradient is sloping:

float2 gradient = float2(ddx(i.color.4), ddy(i.color.r));

The ddx and ddy functions compute screenspace partial derivatives, measuring how much the quantity inside increases as we move a pixel to the right or a pixel up on the screen. This gives us a gradient vector we can use to estimate the function's value at nearby pixels (assuming it's approximately linear, which holds for this case)

Next, we compute where the fragment we're drawing sits relative to the center of this source texel:

// Our current position in texel space (eg from 0,0 to 512, 512)
float2 texelPos = i.uv * _MainTex_TexelSize.zw;

// Rounded to the center of the texel
float2 texelCenter = floor(texelPos) + 0.5f;

// Compute the offset from there to here, and convert back to UV space.
float2 delta = (texelCenter - texelPos) * _MainTex_TexelSize.xy;

(You'll need to declare the float4 _MainTex_TexelSize uniform at the top of your shader, to tell Unity to populate it with the properties of the currently assigned texture)

We're almost there, but one of our vectors is in screenspace and the other is in UV space. So we do a little conversion:

float2x2 uvToScreen;
uvToScreen[0] = ddx(i.uv);
uvToScreen[1] = ddy(i.uv);

float2x2 screenToUV = inverse(uvToScreen);

gradient = mul(screenToUV, gradient);

Using an inverse function like this one:

float2x2 inverse(float2x2 mat) {
    float determinant = mat[0][0] * mat[1][1] - mat[0][1] * mat[1][0];
    float2x2 result = {mat[1][1], -mat[0][1], -mat[1][0], mat[0][0]};
    return result * 1.0f/determinant;

(I feel like we should be able to skip this inversion step, and instead promote the UV offset up to screenspace, but my brain's in weekend mode and I can't seem to make the math work right)

(If your geometry is always parallel to the image plane, you can compute this matrix per-vertex or even as a uniform, but to handle all perspective cases I've done it per fragment)

Finally, we can measure what the interpolated colour should be at the position corresponding to the center of the current texel:

float snapped = i.color.r + dot(gradient, delta);

// discard fragments below the given threshold parameter.
clip(snapped * tex.r - _Threshold);

Note that this rounding in the shader can sometimes disagree with the rounding done by nearest/point filtering, depending on the rounding formula used in the hardware, so if you find you have tiny lines of texels that should be clipped peeking through, you can use the same snapped coordinate for your texture sample too, to ensure they stay in agreement.

  • \$\begingroup\$ I dont know how to thank you... I've been searching so long to find a sollution. And even with figuring out that the issue was that it was rendering each fragment not each texture's pixel i couldnt get anywhere because i didn't know the right words to do a correct search (i really need to find the time to dive into GPU Gems...). Thanks a bunch, and ill try to add the result to the question for all to see. \$\endgroup\$ – Dennis Jun 2 '18 at 19:22

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.