How to implement 8bit sprite stroke shader

I am making an 8 bit game.
I am looking for a shader that creates a border on a Sprite.

Here is the output I want exactly.

But it is written in Unity shader.
Is there a way to use this shader in cocos2dx?

I’ve applied the similar shader in this article, but the texture is smaller and not applied to some corners.
If I can’t use the Unity shader, how can I fix the bug in the shader below?

Nice results (rockman / mega man) from this shader :+1:

basically you just need to look at the fragment shader part of the unity shader

// If outline is enabled and there is a pixel, try to draw an outline.
	if (_Outline > 0 && c.a != 0) {
		// Get the neighbouring four pixels.
		fixed4 pixelUp = tex2D(_MainTex, IN.texcoord + fixed2(0, _MainTex_TexelSize.y));
		fixed4 pixelDown = tex2D(_MainTex, IN.texcoord - fixed2(0, _MainTex_TexelSize.y));
		fixed4 pixelRight = tex2D(_MainTex, IN.texcoord + fixed2(_MainTex_TexelSize.x, 0));
		fixed4 pixelLeft = tex2D(_MainTex, IN.texcoord - fixed2(_MainTex_TexelSize.x, 0));

		// If one of the neighbouring pixels is invisible, we render an outline.
		if (pixelUp.a * pixelDown.a * pixelRight.a * pixelLeft.a == 0) {
			c.rgba = fixed4(1, 1, 1, 1) * _OutlineColor;
		}
	}

	c.rgb *= c.a;

And then you just tweak the outline shader from cpp test to become like the unity’s one
(using up, down, right, left textureCoordinate offsets instead of ‘radius’).
Do note that texelSize does not come built-in for glsl, you will have to pass it in as a uniform.

1 Like

Thank you for answer!!
I will try it and post result.

Thank you!:sunglasses:

I modified the shader to get the result I wanted.
However, this method has three problems.

  1. When the Sprite is made translucent, the borders drawn on the top, bottom, left, and right have transparency, which makes it strange.
  2. If the border size is larger than the texture size, it is cut.
  3. u_radius is not in pixels.

shader.cpp (767 Bytes)

auto sprite = Sprite::create("Character/StrangePerson.png");
sprite->setPosition(sWin / 2);
sprite->getTexture()->setAliasTexParameters();
sprite->setScale(0.5);
this->addChild(sprite);

static const char* shaderName = "stroke";

GLProgram* shader = GLProgram::createWithByteArrays(ccPositionTextureColor_noMVP_vert, shaderSource);

shader->link();
shader->updateUniforms();

shader->bindAttribLocation(GLProgram::ATTRIBUTE_NAME_COLOR, GLProgram::VERTEX_ATTRIB_COLOR);
shader->bindAttribLocation(GLProgram::ATTRIBUTE_NAME_POSITION, GLProgram::VERTEX_ATTRIB_POSITION);
shader->bindAttribLocation(GLProgram::ATTRIBUTE_NAME_TEX_COORD, GLProgram::VERTEX_ATTRIB_TEX_COORD);

GLProgramCache::getInstance()->addGLProgram(shader, shaderName);

auto state = GLProgramState::create(GLProgramCache::getInstance()->getGLProgram(shaderName));
sprite->setGLProgramState(state);

Vec3 color(1, 1, 1);
GLfloat radius = 0.005f;

state->setUniformVec3("u_outlineColor", color);
state->setUniformFloat("u_radius", radius);

Result:

Nice, it is looking good~

1). You can do a conditional check similar to this. Check surrounding 4 pixels’ alpha and maybe use the lowest non-zero value?

// If one of the neighbouring pixels is invisible, we render an outline.
if (pixelUp.a * pixelDown.a * pixelRight.a * pixelLeft.a == 0) {
	c.rgba = fixed4(1, 1, 1, 1) * _OutlineColor;
}

2). Sorry i am not sure what this means, perhaps a screenshot might explain better?

3). The unity shader is using TexelSize instead of radius, which is 1 / TextureSizeInPixels. You can refer to this shader example which is doing something like that

Thank you :slight_smile:
Improved shader.

  1. Sorry i can’t know what it means
    This is the result of setting the opacity to 50. The transparency of the overlapped parts becomes brighter.
  2. 2
    (The red part is transparent.)
    If there is a margin like the picture above, it will not be cut.
    1
    However, if the image fits like this, the shader is not applied to the part. Not surprisingly, how do I fix it?
  3. solved!!

There are still a number of problems, but this is a good shader.

Below is full source.
OutlineTest.zip (19.7 KB)

ok, i’ve just taken a look at your shader code.
you are still using the same method from cpp test, only changing the texture offset values.

  1. The issue in this shader is that it will accumulate the alpha values of those pixels around it. Those outline having higher opacity are those pixels with more neighbouring pixels that are non-transparent.

  2. And how this shader works is that it will draw the outline on transparent pixels surrounding the colored pixels. So, when there is no transparent pixel above (just fit size), then there is no outline there.

If you change the shader to be like the unity one (in my first post), it will work differently. It will replace the colored pixel at the edge with outline color, instead of outline on the transparent pixel.

fixed4 c = SampleSpriteTexture (IN.texcoord) * IN.color;

the first part of the unity shader is almost the same as this part of your shader

normal = texture2D(CC_Texture0, vec2(v_texCoord.x, v_texCoord.y));

except that it multiplies with IN.color, which is equivalent to v_fragmentColor
and as such it has already factored the opacity.

As you see, the very last part of the shader before returning the final color, it multiplies by the opacity that was factored in the first part, so there won’t be any issues with the opacity.

c.rgb *= c.a;

EDIT: Seeing that you have a u_thickness variable, it seems drawing the outline on transparent pixels is what will make the thickness work.
And regards to your #2 issue, it must have transparent pixels at the edge for the outline.

You should change the accumulate method to be using 4 different variables instead (for each of the surrounding pixels).

	accum.rgb = u_outlineColor * accum.a;
	accum.a = 1;

And instead of this, rgb should not be multiplied by alpha, while accum.a should be the max of the 4 pixels’ alpha.

Wow, your reply speed is really fast!! :star_struck:

there must be some extra space in the texture… okay

varying vec2 v_texCoord;
varying vec4 v_fragmentColor;

uniform vec3 u_outlineColor;
uniform int u_thickness;
uniform vec2 u_resolution;

void main()
{
	vec2 unit = 1.0 / u_resolution.xy;

	vec2 ut = unit * u_thickness;
	vec4 right = texture2D(CC_Texture0, vec2(v_texCoord.x + ut.x, v_texCoord.y));
	vec4 left = texture2D(CC_Texture0, vec2(v_texCoord.x - ut.x, v_texCoord.y));
	vec4 up = texture2D(CC_Texture0, vec2(v_texCoord.x, v_texCoord.y + ut.y));
	vec4 down = texture2D(CC_Texture0, vec2(v_texCoord.x, v_texCoord.y - ut.y));
	
	vec4 accum = right + left + up + down;
	if(accum.a > 0.0) accum.a = 1;
	
	accum.rgb = u_outlineColor * accum.a;

	vec4 normal = texture2D(CC_Texture0, vec2(v_texCoord.x, v_texCoord.y));

	normal = (accum * (1.0 - normal.a)) + (normal * normal.a);

	gl_FragColor = v_fragmentColor * normal;
}

Result:


I set opacity each 255, 155.

Shader now works fine!
Thanks for the solution to the problem. :wink:

In solving this problem, I realized that the glsl code is called for each pixel. Did I get it wrong?

Good to see it’s working right now :+1:
Yes, fragment shader is called for every single pixel.

1 Like

This topic was automatically closed 24 hours after the last reply. New replies are no longer allowed.