[SOLVED] Outline Shader GLSL

Hello everyone,
I’m working on a outline shader which will create a outline around the model according to the camera view of that object. But, when I’m applying the shader to plane. It become invisible when I run the scene and their are no error in the console too. So, i’m posting my code below, please see if you find any mistake.

Vertex Shader:

in vec2 a_position;
in vec2 a_tex_coord;
in vec4 a_colour;

uniform mat4 matrix;

out  vec4 v_colour;
out  vec2 tex_coords;

void main() {
   v_colour = a_colour;
   tex_coords = a_tex_coord;
   gl_Position = matrix * vec4(a_position, 0, 1);
}

Fragment Shader

in vec4 v_colour;
in vec2 tex_coords;
out vec4 pixel;

uniform sampler2D t0;
uniform float outline_thickness;
//uniform vec3 outline_colour;
uniform float outline_threshold;

void main() {
    pixel = texture(t0, tex_coords);
    
    vec3 outline_colour = vec3(0,0,1);
    
    if (pixel.a <= outline_threshold) {
        ivec2 size = textureSize(t0, 0);

        float uv_x = tex_coords.x * float(size.x);
        float uv_y = tex_coords.y * float(size.y);

        float sum = 0.0;
        for (int n = 0; n < 9; ++n) {
            uv_y = (tex_coords.y * float(size.y)) + (outline_thickness * float(float(n) - 4.5));
            float h_sum = 0.0;
            h_sum += texelFetch(t0, ivec2(uv_x - (4.0 * outline_thickness), uv_y), 0).a;
            h_sum += texelFetch(t0, ivec2(uv_x - (3.0 * outline_thickness), uv_y), 0).a;
            h_sum += texelFetch(t0, ivec2(uv_x - (2.0 * outline_thickness), uv_y), 0).a;
            h_sum += texelFetch(t0, ivec2(uv_x - outline_thickness, uv_y), 0).a;
            h_sum += texelFetch(t0, ivec2(uv_x, uv_y), 0).a;
            h_sum += texelFetch(t0, ivec2(uv_x + outline_thickness, uv_y), 0).a;
            h_sum += texelFetch(t0, ivec2(uv_x + (2.0 * outline_thickness), uv_y), 0).a;
            h_sum += texelFetch(t0, ivec2(uv_x + (3.0 * outline_thickness), uv_y), 0).a;
            h_sum += texelFetch(t0, ivec2(uv_x + (4.0 * outline_thickness), uv_y), 0).a;
            sum += h_sum / 9.0;
        }

        if (sum / 9.0 >= 0.0001) {
            pixel = vec4(outline_colour, 1);
        }
    }
}

PC Script :

var CustomShader = pc.createScript('customShader');

CustomShader.attributes.add('vs', {
    type: 'asset',
    assetType: 'shader',
    title: 'Vertex Shader'
});

CustomShader.attributes.add('fs', {
    type: 'asset',
    assetType: 'shader',
    title: 'Fragment Shader'
});

CustomShader.attributes.add('diffuseMap', {
    type: 'asset',
    assetType: 'texture',
    title: 'Diffuse Map'
});

CustomShader.attributes.add('outlineThickness', {
    type: 'number',
    default: 0.2,
    title: 'outlineThickness'
});

CustomShader.attributes.add('outlineColour', {
    type: 'vec3',
    title: 'outline_colour'
});

CustomShader.attributes.add('outlineThreshold', {
    type: 'number',
    default: 0.5,
    title: 'outline_threshold'
});
// initialize code called once per entity
CustomShader.prototype.initialize = function() {
    this.time = 0;

    var app = this.app;
    var model = this.entity.model.model;
    var gd = app.graphicsDevice;

    var diffuseTexture = this.diffuseMap.resource;
    var version = "#version 300 es\n";
    var vertexShader = version + this.vs.resource;

    var fragmentShader = "precision " + gd.precision + " float;\n";
    fragmentShader = version + fragmentShader + this.fs.resource;

    // A shader definition used to create a new shader.
    var shaderDefinition = {
        attributes: {
            a_position: pc.SEMANTIC_POSITION,
            a_tex_coord: pc.SEMANTIC_TEXCOORD0,
            a_colour: pc.SEMANTIC_COLOR
        },
        vshader: vertexShader,
        fshader: fragmentShader
    };

    // Create the shader from the definition
    this.shader = new pc.Shader(gd, shaderDefinition);

    // Create a new material and set the shader
    this.material = new pc.Material();
    this.material.setShader(this.shader);

    this.material.setParameter('t0', diffuseTexture);
    
    this.material.setParameter('outline_thickness', this.outlineThickness);
    
    this.material.setParameter('outline_colour', this.outlineColour);
    
    this.material.setParameter('outline_threshold', this.outlineThreshold);

    model.meshInstances[0].material = this.material;

    console.log(model.meshInstances[0].material);
    
};

So, Please check it once. if anybody have solution to these scripts or if anyone can suggest me an alternative.

Thanks

Hi @Shubham_D,

If you are ok with an alternative then you are in luck! Not along ago an official example for an outline shader has been included in the engine examples:

http://playcanvas.github.io/#graphics/model-outline.html

And the source code:

It’s a post process effect, so it’s applied in the camera view and you can select which objects render an outline and which don’t.

Thanks @Leonidas for the solution.
But, I’m not able to understand how to use this inside browser based engine.

Can you please help me again!!

To clarify, browser based engine, you mean using the Playcanvas editor?

yes @Leonidas

sorry for making it confusing :sweat_smile:

Good, I’ve transferred that project in editor and you can take a look here:

https://playcanvas.com/project/682988/overview/model-outline

You control which models render an outline by adding/removing the OutlineLayer layer.

The outline script is attached to the camera and has the color value exposed, which gets also updated in realtime:

2 Likes

Thanks @Leonidas

I will check how it work :+1:

Thank you again

1 Like

Hi @Leonidas, I’m trying your script at the moment. It works fine on a PC, but not on an iMac, the computer goes terribly slow.
It is related to the device pixel ratio, if is not enabled it seems to be ok.

Did you have this issue? Is there a way in the shader to not use this settings.

Hi @Aymeric,

That seems like a GPU performance issue.

This script uses a post process effect which can be taxing though I wouldn’t imagine it can slow down an iMac. What kind of GPU is this iMac using? Can you check if by any chance it’s using the internal GPU (if that’s possible on an iMac, I am not sure)?

Also check at what resolution your iMac is rendering at, if it’s too high and your GPU isn’t powerful enough that slow down makes sense.

I think you could potentially render the post process effect at a lower resolution, though you won’t get accurate outlines that way.

Thanks for your quick answer, I’ve a Radeon Pro 560 4 Go. I don’t think it can use the internal GPU on an iMac ?

My GPU is running at almost 100 via the activity monitor. Here are screen size (from What is my viewport size? Viewport Sizer Tool | YesViz.com):

  • device-pixel-ratio 2
  • device-resolution-width 4096px
  • device-resolution-height 2304px

Edit : from console.log(this.app.graphicsDevice);:
unmaskedRenderer: “AMD Radeon Pro 560 OpenGL Engine”
unmaskedVendor: “ATI Technologies Inc.”

1 Like

I’ll see if I can reproduce on a Macbook, on which browser are you running it and what frame rates are you getting?

To see the fps try launching with the profiler option checked on the droplist under the launch button.

1 Like

Hmm interesting, I’m using latest Firefox and I’ve nearly 200 ms for a frame. But on Chrome, it’s 50ms! I also see the GPU information with Chrome, not on FF.

However even on Chrome it makes the Mac terribly slow. My GPU is running at 80% via the activity monitor. It’s better than 100% but not really acceptable.

1 Like

So, I ran some tests on a newer GPU (AMD Radeon Pro 5300) on MacOS, with the effect enabled I will get about ~50FPS on that example. My resolution is:

  • Device-Pixel-Ratio 2
  • Device-Resolution-Width 3584px
  • Device-Resolution-Height 2240px

I imagine in your case, having a higher resolution (you are above 4K) and an older/slower GPU results in bad performance. It’s mostly expected, this effect is quite taxing in bigger resolutions.

One thing I’ve improved is to set the render target resolution to be half of the display resolution. That will reduce the quality of the outline a bit, but it will improve performance and in your case it may be helpful. Check the example again, the resolution change is in the apply-outline.js script, line 55 (remove the /2 if you would like to restore the quality):

    this.texture = new pc.Texture(this.app.graphicsDevice, {
        width: this.app.graphicsDevice.width / 2,
        height: this.app.graphicsDevice.height / 2,
2 Likes

Any idea how we could apply this outline to a hardware instanced mesh instance? I only want to apply it to some of the instances, not all of them.

You should create a second instanced mesh instance and add it to the Outline layer. In that mesh instance add only the instances you want to render an outline.

1 Like

Hey Leonidas, awesome stuff! Just one question… I have a little issue, I feel like there’s something with depth, it makes text elements (3d ui) see trough other objects, for example: put text inside box, and it will be visible, how can I solve this? :thinking:


Capture

You will need to render those 3D text in the World layer, or at least a layer rendered before depth, so opaque objects do occlude them.

1 Like

Did the job:

    var text = new pc.Entity();
    	text.addComponent("element", {
		type: pc.ELEMENTTYPE_TEXT,
        anchor: new pc.Vec4(0.5,0.5,0.5,0.5),
        pivot: new pc.Vec2(0.5,0.5),
        alignment: new pc.Vec2(0.5,0.5),
        lineHeight: 32,
        autoWidth: true,
        autoHeight: true,
        fontAsset: pc.app.assets.find('PixelFont'),
        fontSize: 0.5,
        color: new pc.Color().fromString("#FFFFFF"),
        layers: [0]
	});

Thanks.

1 Like

But what if i want to use not a 'World" layer, neither UI layer, but a custom one, tried adding a new layer called ‘Text’, no matther what’s the order of ‘Text, transparent’ its nowhere to be seen :thinking: