Clipping planes in PlayCanvas

Hello!
Does PlayCanvas have clipping planes like in Three.js?

Example: three.js webgl - clipping planes
Doc: three.js docs

Hi @Gradess,

No, there isn’t an API to do that automatically for a given material.

It’s doable with custom shaders, if you are comfortable with shaders. Mainly it requires calculating the distance of the world pos of each pixel from a given plane and discarding it, if the distance is negative.

2 Likes

Here is a nice example by @max https://playcanvas.com/project/701512/overview/clip-plane

If you need it to target specific entities / materials, lmk, I have some code for that. Right now as it stands it applies to everything.

3 Likes

Hi there, I might want to target specific entities. How to do that?

Hey,
Here is some code that might help you out. It might have a few bugs as I haven’t used it since Nov.

/*
    Usage:

    app.fire('clipPlane:set', position, normal);
    app.fire('clipPlane:enable');
    app.fire('clipPlane:disable');
*/
var ClipPlaneGroup = pc.createScript('clipPlaneGroup');
ClipPlaneGroup.attributes.add('entities', {type: 'entity', array: true, description: 'Add entities to be clipped by this clip plane.'});
ClipPlaneGroup.attributes.add('clipPlaneChunk', {
    title: 'Clip Plane Shader',
    type: 'asset',
    assetType: 'shader',
    description: 'Shader that implements clip plane logic'
});

ClipPlaneGroup.prototype.initialize = function() {
    this.device = this.app.graphicsDevice;
    
    // transforms for plane
    this.position = new pc.Vec3(0, 0, 0);
    this.normal = new pc.Vec3(0, 1, 0);
    // plane equation `Ax + By + Cz = D` values
    this.planeDataF32 = new Float32Array(4);
    
    this.entitiesClipped = [];
    
    // entity setup
    if (this.entities.length > 0)
    {
        this.entities.forEach((entity) => {
            this.addEntity(entity);
        });
    }

    // disable clipping
    this.on('disable', function() {
        this.device.scope.resolve('clip_enabled').setValue(0);
    }, this);
    
    // when enabling
    this.on('enable', function() {
        this.device.scope.resolve('clip_enabled').setValue(1);
    }, this);
    
    // app wide message to set clip plane
    this.app.on('clipPlane:set', this.updateClipPlane, this);
    
    // app wide message to add a entity
    this.app.on('clipPlane:addEntity', this.addEntity, this);
    
    // app wide message to remove a entity
    this.app.on('clipPlane:removeEntity', this.removeEntity, this);
    
    // app wide message to enable clip plane
    this.app.on('clipPlane:enable', function() {
        this._enabled = true;
    }, this);
    
    // app wide message to disable clip plane
    this.app.on('clipPlane:disable', function() {
        this._enabled = false;
    }, this);

    // set initial defaults
    this.updateClipPlane(this.position, this.normal);
    
    // initially disable
    this._enabled = false;
};

ClipPlaneGroup.prototype.addEntity = function(entity) {
    if (entity && this.entitiesClipped.indexOf(entity) === -1)
    {
        this.setupEntity(entity);
        this.entitiesClipped.push(entity);
        this.updateClipPlane(this.position, this.normal);
    }
};
ClipPlaneGroup.prototype.removeEntity = function(entity) {
    const index = this.entitiesClipped.indexOf(entity);
    if (index > -1)
    {
        this.cancelEntity(entity);
        this.entitiesClipped.splice(index, 1);
        this.updateClipPlane(this.position, this.normal);
    }   
};

ClipPlaneGroup.prototype.setupEntity = function(entity) {
    if (!entity) return;
    if (!entity.model || !entity.model.meshInstances)
    {
        console.warn('[ClipPlaneGroup] Entity missing model component or model:', entity.name);
        return;
    }
    entity.model.meshInstances.forEach((mesh) => {
        const material = mesh.material;

        material.chunks.startPS = this.clipPlaneChunk.resource;
        this.clipPlaneChunk.on('load', () => {
            material.chunks.startPS = this.clipPlaneChunk.resource;
        }, this);

        material.setParameter('clip_enabled', 0);
    });
};

ClipPlaneGroup.prototype.cancelEntity = function(entity) {
    if (!entity) return;
    if (!entity.model || !entity.model.meshInstances)
    {
        console.warn('[ClipPlaneGroup] Entity missing model component or model:', entity.name);
        return;
    }
    entity.model.meshInstances.forEach((mesh) => {
        const material = mesh.material;
        material.setParameter('clip_enabled', 0);
    });
};

ClipPlaneGroup.prototype.updateClipPlane = function(position, normal) {
    if (this.entitiesClipped.length === 0) return;
    
    this._enabled = true;

    var o = this.position.copy(position);
    var n = this.normal.copy(normal);
    
    // calculate plane equation
    this.planeDataF32[0] = n.x;
    this.planeDataF32[1] = n.y;
    this.planeDataF32[2] = n.z;
    this.planeDataF32[3] = -(n.x * o.x + n.y * o.y + n.z * o.z);
    
    this.entitiesClipped.forEach((entity) => {
        entity.model.meshInstances.forEach((mesh) => {
            const material = mesh.material;
            
            /*
                use 'mesh' to apply to the specific mesh material.
                user 'material' to apply to material globally 
            */
            
            // set shader uniform
            mesh.setParameter('clip_plane', this.planeDataF32);
            // enable clipping
            mesh.setParameter('clip_enabled', +this._enabled);
            
            // set shader uniform
            //material.setParameter('clip_plane', this.planeDataF32);
            // enable clipping
            //material.setParameter('clip_enabled', +this._enabled);
        });
    });
};

Main things that I had to change was:

pc.shaderChunks.to entity material node this.entity.model.meshInstances[0].material
and the device.global.scope.resolve to the mesh or material.

3 Likes

Well I had multiple meshes with multiple materials. I have a different approach to automate process.

    var object = this.entity;
    var e = object.clone();
    var meshInstances = object.model.model.meshInstances;
    var numMeshInstances = meshInstances.length;
    for (var i = 0; i < numMeshInstances; i++) {
        object.model.meshInstances[i].material.cull = pc.CULLFACE_BACK;
        object.model.meshInstances[i].material = e.model.meshInstances[i].material.clone();
        e.model.meshInstances[i].material.cull = pc.CULLFACE_FRONT;
            
    }
    this.backer(e);
    this.entity.parent.addChild(e);

“backer” function does the front face culling and stencil test with same principle.

Edit: I am working on cross-section cut effect with stencil test btw.

1 Like