Hello!
Does PlayCanvas have clipping planes like in Three.js?
Example: three.js webgl - clipping planes
Doc: three.js docs
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.
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.
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.
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.