Outline effect for V2 engine

Hi - the ApplyOutline.js effect is no longer working in V2, and I’ve tried updating it with claude but to no success.

Has anyone created an outline effect - that can show on specific entities - for V2 ?

ok cheers !

-Jerry

You can use `Outline Renderer`

Here is an example:

https://playcanvas.vercel.app/#/graphics/outlines-colored

1 Like

Even the original effect still works, see example PlayCanvas Examples

it uses this script

1 Like

thanks ! -those both look good - My old one does work now with V2, although not when used in conjunction with the cameraFrame class - which handles ssao, grading etc. When I use it with that enabled the screen goes black.

I’m not sure what the solution there is - or whether these two solutions above will have the same issue. Am trying to connect up the MCP to see if it can diagnose the problem.

yes, the old effect does not work with camera frame.
but the colored outlines does.

so with posteffect-outline.js - I can’t just add this to my existing camera (the one with the cameraFrame fx class) - do I need to write a script that references it and add that to the camera ?

as mentioned, posteffect-outline.js cannot be used when you use CameraFrame.
You need to use OutlineRenderer | Engine API Reference - v2.22.4 directly, from script that drive what gets selected typically.

thanks - if I want to add a script to a scene which uses this functionality I get a bunch of shader errors. Any idea what the way around this might be ?
*
Include “lightBufferDefinesPS” not resolved while preprocessing fragment shader for Shader Id 8 (GLSL) StandardShader-forward-proc*

[object Object]

Shader preprocessing reached the end of the file without encountering the necessary #endif to close a preceding #if, #ifdef, or #ifndef block. fragment shader for Shader Id 8 (GLSL) StandardShader-forward-proc

Failed to preprocess shader:

[object Object]

Shader: Failed to create shader Shader Id 8 (GLSL) StandardShader-forward-proc. Vertex or fragment shader source is empty.

[object Object]

*Failed to compile vertex shader:

ERROR: 0:30: ‘{’ : unexpected token
ERROR: 0:71: ‘include’ : invalid directive name*

var OutlineAttacher = pc.createScript('outlineAttacher');
OutlineAttacher.attributes.add('camera', { title: 'Camera', type: 'entity' });
// initialize code called once per entity
OutlineAttacher.prototype.initialize = function() {


    this.outlineRenderer = new pc.OutlineRenderer(this.app);
    this.outlineColor = new pc.Color(1, 1, 0, 1); // Yellow outline

    // 2. Add an entity to be outlined (e.g., an entity named 'Box')
    var targetEntity = this.app.root.findByName('Box');
    this.outlineRenderer.addEntity(targetEntity, this.outlineColor);

    // 3. Reference your immediate layer
    this.immediateLayer = this.app.scene.layers.getLayerByName('Immediate');

};

// update code called every frame
OutlineAttacher.prototype.update = function(dt) {

        if (this.outlineRenderer && this.camera) {
        this.outlineRenderer.frameUpdate(this.camera, this.immediateLayer, false);
    }

};

Thanks for the repro script. This is a bug in the engine: the outline shader always enables clustered lighting, so it fails to compile when Clustered Lighting is disabled in your project settings. That’s where the lightBufferDefinesPS errors come from. It’s not related to CameraFrame. This will be fixed in an upcoming engine release.

In the meantime, there are two options:

  • enable Clustered Lighting in the project settings, or
  • use the script below, which works around the bug. Remove the lines marked WORKAROUND once the fix is released.

There is one more issue in your setup. By default the OutlineRenderer renders the outlined objects into the Immediate layer. Your scene camera also renders that layer, so the objects get drawn a second time, in a layer without your lights, and they turn dark or black. The script below avoids this by rendering them into a separate OutlineLayer. Do not add that layer to your camera. The outlines are still composited while the camera renders the Immediate layer, so keep Immediate in the camera’s layers.

var OutlineAttacher = pc.createScript('outlineAttacher');
OutlineAttacher.attributes.add('camera', { title: 'Camera', type: 'entity' });

OutlineAttacher.prototype.initialize = function () {

    // A dedicated layer the outlined objects are rendered to by the outline renderer. The scene
    // camera must NOT render this layer (do not add it to the camera's layers).
    var outlineLayer = this.app.scene.layers.getLayerByName('OutlineLayer');
    if (!outlineLayer) {
        outlineLayer = new pc.Layer({ name: 'OutlineLayer' });
        this.app.scene.layers.push(outlineLayer);
    }

    this.outlineRenderer = new pc.OutlineRenderer(this.app, outlineLayer);
    this.outlineColor = new pc.Color(1, 1, 0, 1); // Yellow outline

    var targetEntity = this.app.root.findByName('Box');
    this.outlineRenderer.addEntity(targetEntity, this.outlineColor);
    this.applyOutlineFix(targetEntity); // WORKAROUND: remove once the engine fix is released

    // the outlines are composited while the scene camera renders the Immediate layer
    this.immediateLayer = this.app.scene.layers.getLayerByName('Immediate');

    this.on('destroy', function () {
        this.outlineRenderer.destroy();
    }, this);
};

OutlineAttacher.prototype.update = function (dt) {
    if (this.outlineRenderer && this.camera) {
        this.outlineRenderer.frameUpdate(this.camera, this.immediateLayer, false);
    }
};

// WORKAROUND: remove this whole function once the engine fix is released.
// The outline shader enables clustered lighting by default, which fails to compile when clustered
// lighting is disabled in the project settings. Call this after each addEntity.
OutlineAttacher.prototype.applyOutlineFix = function (entity) {
    var components = entity.findComponents('render').concat(entity.findComponents('model'));
    components.forEach(function (component) {
        component.meshInstances.forEach(function (meshInstance) {
            var material = meshInstance.material;
            var outlineCallback = material.onUpdateShader;
            if (!outlineCallback || outlineCallback._outlineFix) return;

            var wrapped = function (options) {
                var result = outlineCallback(options);
                // only the outline pass returns new options, all other passes are untouched
                if (result !== options) {
                    result.litOptions.clusteredLightingEnabled = false;
                }
                return result;
            };
            wrapped._outlineFix = true;
            material.onUpdateShader = wrapped;
        });
    });
};

If you outline more entities later, call this.applyOutlineFix(entity) after each addEntity call.

2 Likes

thanks - that works until I re-enable the cameraFrame - in which case the outlines don’t show up, but it doesn’t cause a blank screen at least.

btw what is the process to remove them ? - do I need to de-applyOutlineFix all the added entities ?

Thanks, that helped narrow it down. This is a second engine bug, and it will also be fixed in an upcoming release.

With some CameraFrame settings, the scene is rendered to a target with an extra color attachment that stores the scene depth. That happens with SSAO set to Combine, or with TAA or DOF when SSAO isn’t set to Lighting. The outline composite only writes the color, so the browser rejects the draw and the outlines disappear. In Chrome you can see it in the console as GL_INVALID_OPERATION: glDrawElements: Active draw buffers with missing fragment shader outputs.

The updated script below works around this too. The new function is applyCameraFrameFix. As before, remove the lines marked WORKAROUND once the fixes are released.

Removing outlines: call this.outlineRenderer.removeEntity(entity). You don’t need to undo applyOutlineFix: removeEntity resets the material’s shader callback, which removes the wrapper along with it. If you add the entity again later, call applyOutlineFix again after addEntity.

var OutlineAttacher = pc.createScript('outlineAttacher');
OutlineAttacher.attributes.add('camera', { title: 'Camera', type: 'entity' });

OutlineAttacher.prototype.initialize = function () {

    // A dedicated layer the outlined objects are rendered to by the outline renderer. The scene
    // camera must NOT render this layer (do not add it to the camera's layers).
    var outlineLayer = this.app.scene.layers.getLayerByName('OutlineLayer');
    if (!outlineLayer) {
        outlineLayer = new pc.Layer({ name: 'OutlineLayer' });
        this.app.scene.layers.push(outlineLayer);
    }

    this.outlineRenderer = new pc.OutlineRenderer(this.app, outlineLayer);
    this.applyCameraFrameFix(this.outlineRenderer); // WORKAROUND: remove once the engine fix is released
    this.outlineColor = new pc.Color(1, 1, 0, 1); // Yellow outline

    var targetEntity = this.app.root.findByName('Box');
    this.outlineRenderer.addEntity(targetEntity, this.outlineColor);
    this.applyOutlineFix(targetEntity); // WORKAROUND: remove once the engine fix is released

    // the outlines are composited while the scene camera renders the Immediate layer
    this.immediateLayer = this.app.scene.layers.getLayerByName('Immediate');

    this.on('destroy', function () {
        this.outlineRenderer.destroy();
    }, this);
};

OutlineAttacher.prototype.update = function (dt) {
    if (this.outlineRenderer && this.camera) {
        this.outlineRenderer.frameUpdate(this.camera, this.immediateLayer, false);
    }
};

// WORKAROUND: remove this whole function once the engine fix is released.
// With some CameraFrame settings (for example SSAO set to Combine, or TAA / DOF without SSAO set to
// Lighting) the scene is rendered to a target with an additional color attachment holding the
// depth. The outlines only write the color, so their draw is rejected unless the writes to the
// other attachments are disabled.
OutlineAttacher.prototype.applyCameraFrameFix = function (outlineRenderer) {
    var device = this.app.graphicsDevice;
    var blendState = outlineRenderer.blendState;
    var maskedStates = [];
    var blendOutlines = outlineRenderer.blendOutlines;

    outlineRenderer.blendOutlines = function () {
        var count = device.renderTarget ? device.renderTarget.colorBufferCount : 1;
        if (count > 1) {
            if (!maskedStates[count]) {
                var noWrite = blendState.clone();
                noWrite.setColorWrite(false, false, false, false);
                var masked = blendState.clone();
                for (var i = 1; i < count; i++) {
                    masked.setAttachment(i, noWrite);
                }
                maskedStates[count] = masked;
            }
            this.blendState = maskedStates[count];
        } else {
            this.blendState = blendState;
        }
        blendOutlines.call(this);
    };
};

// WORKAROUND: remove this whole function once the engine fix is released.
// The outline shader enables clustered lighting by default, which fails to compile when clustered
// lighting is disabled in the project settings. Call this after each addEntity.
OutlineAttacher.prototype.applyOutlineFix = function (entity) {
    var components = entity.findComponents('render').concat(entity.findComponents('model'));
    components.forEach(function (component) {
        component.meshInstances.forEach(function (meshInstance) {
            var material = meshInstance.material;
            var outlineCallback = material.onUpdateShader;
            if (!outlineCallback || outlineCallback._outlineFix) return;

            var wrapped = function (options) {
                var result = outlineCallback(options);
                // only the outline pass returns new options, all other passes are untouched
                if (result !== options) {
                    result.litOptions.clusteredLightingEnabled = false;
                }
                return result;
            };
            wrapped._outlineFix = true;
            material.onUpdateShader = wrapped;
        });
    });
};
1 Like

thanks ! i will try this out asap :slight_smile: