I’m new to three.js and stackoverflow. I’m trying to clip and cap three.js objects that have been rendered so I can move the helperPlane back and forth through the object to see inside it. There's an object inside it. What I’m looking to do is similar to this description of advanced clipping techniques in OpenGL here: More OpenGL Game Programming - Bonus - Advanced Clip Planes. So, if this can be done in OpenGL, there must be some way to do it in WebGL too?
I adapted the clipping_stencil example from threejs ( webgl - clipping stencil ), and everything looks right as long as I don’t move the helperPlanes. When the helperPlanes are moved, some of the cap faces of the larger mesh disappear, there’s some rendering artifacts-I think this is z-fighting-and the caps might not be rendered in the desired position.
Setting the renderingOrder property for the meshes was the big trick to getting the inner mesh to be rendered in the scene, but I don't know what to do about the z-fighting? when I move the clipping planes on the sliders.
I also posted this on discourse.threejs. Everything is on a JSFiddle. Any help would be greatly appreciated.
import * as THREE from 'three';
import Stats from 'https://threejs.org/examples/jsm/libs/stats.module.js';
import {GUI} from 'https://threejs.org/examples/jsm/libs/lil-gui.module.min.js';
import { OrbitControls } from 'https://threejs.org/examples/jsm/controls/OrbitControls.js';
let camera, scene, renderer, object, object2, stats;
let planes, planeObjects, planeObjects2, planeHelpers;
let clock;
const params = {
animate: false,
planeX: {
constant: 1,
negated: false,
displayHelper: false
},
planeY: {
constant: 1,
negated: false,
displayHelper: false
},
planeZ: {
constant: 0,
negated: false,
displayHelper: false
}
};
init();
animate();
function createPlaneStencilGroup( geometry, plane, renderOrder ) {
const group = new THREE.Group();
const baseMat = new THREE.MeshBasicMaterial();
baseMat.depthWrite = false;
baseMat.depthTest = false;
baseMat.colorWrite = false;
baseMat.stencilWrite = true;
baseMat.stencilFunc = THREE.AlwaysStencilFunc;
/* Subtract the mask created from the front-facing image
from the mask created from the back-facing image, we get
a new mask that represents the area where the clip edge
would be. Set the stencil buffer operation to increment
when rederering back-facing polygons and decrement on
front-facing polygons. This results in the desired mask
stored in the stencil buffer : http://glbook.gamedev.net/GLBOOK/glbook.gamedev.net/moglgp/advclip.html */
// back faces
const mat0 = baseMat.clone();
mat0.side = THREE.BackSide;
mat0.clippingPlanes = [ plane ];
mat0.stencilFail = THREE.IncrementWrapStencilOp;
mat0.stencilZFail = THREE.IncrementWrapStencilOp;
mat0.stencilZPass = THREE.IncrementWrapStencilOp;
//mat0.depthFunc = THREE.LessDepth; // See reference above
const mesh0 = new THREE.Mesh( geometry, mat0 );
mesh0.renderOrder = renderOrder;
group.add( mesh0 );
// front faces
const mat1 = baseMat.clone();
mat1.side = THREE.FrontSide;
mat1.clippingPlanes = [ plane ];
mat1.stencilFail = THREE.DecrementWrapStencilOp;
mat1.stencilZFail = THREE.DecrementWrapStencilOp;
mat1.stencilZPass = THREE.DecrementWrapStencilOp;
//mat1.depthFunc = THREE.LessDepth;
const mesh1 = new THREE.Mesh( geometry, mat1 );
mesh1.renderOrder = renderOrder;
group.add( mesh1 );
return group;
}
function init(){
//clock
clock = new THREE.Clock();
// scene
scene = new THREE.Scene();
// camera
camera = new THREE.PerspectiveCamera(36, window.innerWidth/window.innerHeight, 1,100);
camera.position.set(2,2,2);
// Lights
scene.add(new THREE.AmbientLight(0xffffff, 0.5));
const dirLight = new THREE.DirectionalLight(0xffffff,1);
dirLight.position.set(5,10,7.5);
dirLight.castShadow = true;
dirLight.shadow.camera.right = 2;
dirLight.shadow.camera.left = -2;
dirLight.shadow.camera.top = 2;
dirLight.shadow.camera.bottom = -2;
dirLight.shadow.mapSize.width = 1024;
dirLight.shadow.mapSize.height = 1024;
scene.add(dirLight);
//Clipping planes
planes = [
new THREE.Plane( new THREE.Vector3( - 1, 0, 0 ), 1 ),
new THREE.Plane( new THREE.Vector3( 0, - 1, 0 ), 1 ),
new THREE.Plane( new THREE.Vector3( 0, 0, - 1 ), 0 )
];
planeHelpers = planes.map( p => new THREE.PlaneHelper( p, 2, 0xffffff ) );
planeHelpers.forEach( ph => {
ph.visible = false;
scene.add( ph );
} );
//Inner Cube
const geometry = new THREE.BoxGeometry( 0.5,0.5,0.5 );
//Outer Cube
const geometry2 = new THREE.BoxGeometry( 1,1,1 );
object = new THREE.Group();
scene.add(object);
//Set up clip plane rendering
/*
See https://discourse.threejs.org/t/capping-two-clipped-geometries-using-two-planes-which-are-negated-to-each-other/32643
Object 1
Render order 1: Draw front face / back face clipped and front face
/ back face not clipped (4 meshes)
Render order 2: Draw planar clip cap
Object 2
Render order 3: Draw front face / back face clipped and front face
/ back face not clipped (4 meshes)
Render order 4: Draw planar clip cap
*/
planeObjects = [];
planeObjects2 = [];
const planeGeom = new THREE.PlaneGeometry( 4, 4 );
for ( let i = 0; i < 3; i ++ ) {
const poGroup = new THREE.Group();
const poGroup2 = new THREE.Group()
const plane = planes[ i ];
// Object 1
const stencilGroup = createPlaneStencilGroup( geometry,
plane, i + 4 ); // Render after first group
// Object 2
const stencilGroup2 = createPlaneStencilGroup( geometry2,
plane, i + 1 ); // Render this first
// PLANAR CLIP CAP
// plane is clipped by the other clipping planes
const planeMat =
new THREE.MeshStandardMaterial( {
color: 0xfff000, // inner torus colour
metalness: 0.1,
roughness: 0.75,
clippingPlanes: planes.filter( p => p !== plane ),
//depthFunc: THREE.LessDepth,
stencilWrite: true,
stencilRef: 0,
stencilFunc: THREE.NotEqualStencilFunc,
stencilFail: THREE.ReplaceStencilOp,
stencilZFail: THREE.ReplaceStencilOp,
stencilZPass: THREE.ReplaceStencilOp,
} );
const planeMat2 =
new THREE.MeshStandardMaterial( {
color: 0xff0000, // inner torus colour
metalness: 0.1,
roughness: 0.75,
clippingPlanes: planes.filter( p => p !== plane ),
//depthFunc: THREE.LessDepth,
stencilWrite: true,
stencilRef: 0,
stencilFunc: THREE.NotEqualStencilFunc,
stencilFail: THREE.ReplaceStencilOp,
stencilZFail: THREE.ReplaceStencilOp,
stencilZPass: THREE.ReplaceStencilOp,
} );
const po = new THREE.Mesh( planeGeom, planeMat );
const po2 = new THREE.Mesh( planeGeom, planeMat2 );
po.onAfterRender = function ( renderer ) {
renderer.clearStencil();
};
po2.onAfterRender = function ( renderer ) {
renderer.clearStencil();
};
// Draw Planar Clip Cap
po.renderOrder = i + 4.1; // Render last (slightly)
po2.renderOrder = i + 1.1; // Render slightly after first group
object.add( stencilGroup );
object.add( stencilGroup2 );
poGroup.add( po );
poGroup2.add( po2 );
planeObjects.push( po );
planeObjects2.push( po2 );
scene.add( poGroup );
scene.add( poGroup2 );
}
// Object 1
const material = new THREE.MeshStandardMaterial( {
color: 0xfff000, // outer torus colour
metalness: 0.1,
roughness: 0.75,
clippingPlanes: planes,
clipShadows: true,
shadowSide: THREE.DoubleSide,
} );
// add the color
const clippedColorFront = new THREE.Mesh( geometry, material );
clippedColorFront.castShadow = true;
clippedColorFront.renderOrder = 6;
object.add( clippedColorFront );
// Object 2
const material2 = new THREE.MeshStandardMaterial( {
color: 0xff0000, // outer colour
metalness: 0.1,
roughness: 0.75,
side: THREE.DoubleSide,
clippingPlanes: planes,
clipShadows: true,
shadowSide: THREE.DoubleSide,
} );
// add the color
const clippedColorFront2 = new THREE.Mesh( geometry2, material2 );
clippedColorFront2.castShadow = true;
clippedColorFront2.renderOrder = 3;
object.add( clippedColorFront2 );
//Ground
const ground = new THREE.Mesh(
new THREE.PlaneGeometry(9,9,1,1),
new THREE.MeshPhongMaterial({color:0x999999, opacity:0.25, side:THREE.DoubleSide})
);
ground.rotation.x = - Math.PI/2; // rotates x/y to x/z
ground.position.y = -1;
ground.receiveShadow = true;
scene.add(ground);
//Stats
stats = new Stats();
document.body.appendChild(stats.dom);
//Renderer
renderer = new THREE.WebGLRenderer({antialias:true});
renderer.shadowMap.enabled = true;
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearColor( 0x263238 );
window.addEventListener('resize',onWindowResize);
document.body.appendChild(renderer.domElement);
renderer.localClippingEnabled = true;
const controls = new OrbitControls(camera, renderer.domElement);
controls.minDistance = 2;
controls.maxDistance = 20;
controls.update();
//GUI
const gui = new GUI();
gui.add(params, 'animate');
const planeX = gui.addFolder( 'planeX' );
planeX.add( params.planeX, 'displayHelper' ).onChange( v => planeHelpers[ 0 ].visible = v );
planeX.add( params.planeX, 'constant' ).min( - 1 ).max( 1 ).onChange( d => planes[ 0 ].constant = d );
planeX.add( params.planeX, 'negated' ).onChange( () => {
planes[ 0 ].negate();
params.planeX.constant = planes[ 0 ].constant;
} );
planeX.open();
const planeY = gui.addFolder( 'planeY' );
planeY.add( params.planeY, 'displayHelper' ).onChange( v => planeHelpers[ 1 ].visible = v );
planeY.add( params.planeY, 'constant' ).min( - 1 ).max( 1 ).onChange( d => planes[ 1 ].constant = d );
planeY.add( params.planeY, 'negated' ).onChange( () => {
planes[ 1 ].negate();
params.planeY.constant = planes[ 1 ].constant;
} );
planeY.open();
const planeZ = gui.addFolder( 'planeZ' );
planeZ.add( params.planeZ, 'displayHelper' ).onChange( v => planeHelpers[ 2 ].visible = v );
planeZ.add( params.planeZ, 'constant' ).min( - 1 ).max( 1 ).onChange( d => planes[ 2 ].constant = d );
planeZ.add( params.planeZ, 'negated' ).onChange( () => {
planes[ 2 ].negate();
params.planeZ.constant = planes[ 2 ].constant;
} );
planeZ.open();
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize( window.innerWidth, window.innerHeight );
}
function animate() {
const delta = clock.getDelta();
requestAnimationFrame( animate );
if ( params.animate ) {
object.rotation.x += delta * 0.5;
object.rotation.y += delta * 0.2;
}
for ( let i = 0; i < planeObjects.length; i ++ ) {
const plane = planes[ i ];
// Planar clip cap for object 1
const po = planeObjects[ i ];
plane.coplanarPoint( po.position );
// planar clip cap for object 2
const po2 = planeObjects[ i ];
plane.coplanarPoint( po2.position );
// planar clip cap for object 1
po.lookAt(
po.position.x - plane.normal.x,
po.position.y - plane.normal.y,
po.position.z - plane.normal.z,
);
// planar clip cap for object 2
po2.lookAt(
po2.position.x - plane.normal.x,
po2.position.y - plane.normal.y,
po2.position.z - plane.normal.z,
);
}
stats.begin();
renderer.render( scene, camera );
stats.end();
}