Clipping and stencil position

I am working on a project with models loaded into a scene from several *.obj files.
I test cutting models in 6 planes (front, back, left, right, top, bottom).
I’ve been struggling for days to get the incisions filled with plane. I’ve looked at various solutions, but I’m not quite able to implement them in my project.
Eventually I went back to the standard example: three.js examples
I don’t quite understand the truncation principle in this example, but I’m trying to apply it to my project.
I’m testing this with two objects (in a group).
Unfortunately, intersection fills are not created in the model position.
When I added copying the mesh position:

  stencilGroup.position.copy( mesh.position );

works ok but only with one model (first or second moved object).
Unfortunately, with two (or more) models, this does not work.
Clipping/stencil is a tricky topic but where is the error?



<!DOCTYPE html>
<html lang="en">
	<head>
		<title>stencil-mytest</title>
		<meta charset="utf-8">
		<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
		<link type="text/css" rel="stylesheet" href="main.css">
	</head>
	<body>
		<div id="info"><a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> - solid geometry with clip cutplanes and stencil materials<br>according to the original: <a href="https://threejs.org/examples/?q=stencil#webgl_clipping_stencil" target="_blank" rel="noopener">examples/webgl_clipping_stencil.html</a></div>
		<!-- Import maps polyfill -->
		<!-- Remove this when import maps will be widely supported -->
		<script async src="https://unpkg.com/es-module-shims@1.6.3/dist/es-module-shims.js"></script>
		<script type="importmap">
			{
				"imports": {
					"three": "../build/three.module.js",
					"three/addons/": "./jsm/"
				}
			}
		</script>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
// import Stats from 'three/addons/libs/stats.module.js';

let camera, scene, renderer;
// let stats;
let controls;
let q,x,y,z,n,m,e;
let obj,geom,mater,mesh;

// CUTPLANEs - predef
let planeObjects, cropmodels;
let cutparams = {
	cuthelpers: false,		// display/hide cuthelpers
	cutclipcol:	0xE91E63,
	// 6 cutplanes/constant
	cutx: 0.4, cuty: 0.4, cutz: 2,
	cutx2:0.4, cuty2:0.4, cutz2:2,
	};
let cutplanes = [
Object.assign(new THREE.Plane( new THREE.Vector3( -1,  0,  0 ), cutparams.cutx ),{name: "cutx"} ),
Object.assign(new THREE.Plane( new THREE.Vector3(  0, -1,  0 ), cutparams.cuty ),{name: "cuty"} ),
Object.assign(new THREE.Plane( new THREE.Vector3(  0,  0, -1 ), cutparams.cutz ),{name: "cutz"} ),
Object.assign(new THREE.Plane( new THREE.Vector3(  1,  0,  0 ), cutparams.cutx2 ),{name: "cutx2"} ),
Object.assign(new THREE.Plane( new THREE.Vector3(  0,  1,  0 ), cutparams.cuty2 ),{name: "cuty2"} ),
Object.assign(new THREE.Plane( new THREE.Vector3(  0,  0,  1 ), cutparams.cutz2 ),{name: "cutz2"} ),
]
// console.log(cutplanes)

	function init() {

		scene = new THREE.Scene();
		camera = new THREE.PerspectiveCamera( 36, window.innerWidth / window.innerHeight, 1, 100 );
		camera.position.set( 5.5, 3.6, 3.2 );

		// #############
		scene.add( new THREE.AmbientLight( 0xffffff, 1.5 ) );
		let dirLight = new THREE.DirectionalLight( 0xffffff, 3 );
			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;
		scene.add( dirLight );
		// #############
		let planeHelpers = cutplanes.map( p => new THREE.PlaneHelper( p, 2, 0xffffff ) );
			planeHelpers.forEach( ph => {
				ph.visible = cutparams.cuthelpers,	// poka/ukryj cuthelpers;
				scene.add( ph );
			} );
		// #############
		let axesHelper = new THREE.AxesHelper( 2 );
		scene.add( axesHelper );
                    // #############
		// MODELs
		// GROUP of all loaded models (e.g. *.obj)
		let allmodels = new THREE.Group();
		scene.add( allmodels );
		allmodels.visible=false;	// hide group

		// Object 1
		geom = new THREE.TorusKnotGeometry( 0.4, 0.15, 220, 60 );
		mater = new THREE.MeshStandardMaterial( {
			color: 0xFFC107,
			// clippingPlanes: cutplanes,
			// clipShadows: true,
			shadowSide: THREE.DoubleSide,
		} );
		obj = new THREE.Mesh( geom, mater );
			obj.castShadow = true;
			// obj.side = THREE.DoubleSide;
			// obj.material.side = THREE.DoubleSide;
			// scene.add( obj );
		allmodels.add(obj)

		// Object 2
		geom = new THREE.TorusKnotGeometry( 0.4, 0.15, 220, 60 );
		mater = new THREE.MeshStandardMaterial( {
			color: 0x00ff00,
			// clippingPlanes: cutplanes,
			// clipShadows: true,
			shadowSide: THREE.DoubleSide,
		} );
		obj = new THREE.Mesh( geom, mater );
			obj.castShadow = true;
			// obj.side = THREE.DoubleSide;
			// obj.material.side = THREE.DoubleSide;
			obj.position.z= -1.5
			// scene.add( obj );
		allmodels.add(obj)

		// GROUP for cropped models
		let cropmodels = new THREE.Group();
		scene.add( cropmodels );

		// for ( let i = 0; i <= 0; i ++ ) { // test only first object from group allmodels
		// for ( let i = 1; i <= 1; i ++ ) { // test only second object from group allmodels
		for ( let i = 0; i < allmodels.children.length; i ++ ) { // test ALL objects

			mesh = allmodels.children[i]	// mesh
			geom = mesh.geometry			// geometry

			// Set up clip plane rendering
			planeObjects = [];
			// let planeGeom = new THREE.PlaneGeometry( 4, 4 );
			let planeGeom = new THREE.PlaneGeometry( 50, 50 );

			for ( let i = 0; i < cutplanes.length; i ++ ) {

				let poGroup = new THREE.Group();
				let iplane = cutplanes[ i ];
				let stencilGroup = createPlaneStencilGroup( geom, iplane, i + 1 );

				// iplane is clipped by the other clipping cutplanes
				let planeMat =
					new THREE.MeshStandardMaterial( {
						color: cutparams.cutclipcol, // mycolor
						metalness: 0.1,
						roughness: 0.75,
						clippingPlanes: cutplanes.filter( p => p !== iplane ),

						stencilWrite: true,
						stencilRef: 0,
						stencilFunc: THREE.NotEqualStencilFunc,
						stencilFail: THREE.ReplaceStencilOp,
						stencilZFail: THREE.ReplaceStencilOp,
						stencilZPass: THREE.ReplaceStencilOp,

					} );

				let po = new THREE.Mesh( planeGeom, planeMat );
					po.onAfterRender = function ( renderer ) { renderer.clearStencil(); };
					po.renderOrder = i + 1.1;

				// test OK only for first or second object from group allmodels
				stencilGroup.position.copy( mesh.position );

				cropmodels.add( stencilGroup );
				poGroup.add( po );
				planeObjects.push( po );

				scene.add( poGroup );
			} // for cutplanes.length

			// add the color
			let clippedColorFront = new THREE.Mesh( geom, mesh.material );	// apply source mesh material
			clippedColorFront.castShadow = true;
			clippedColorFront.renderOrder = 6;
			clippedColorFront.material.clippingPlanes = cutplanes;
			clippedColorFront.material.clipShadows = true;
			clippedColorFront.material.shadowSide = THREE.DoubleSide;
			clippedColorFront.position.copy( mesh.position ); // apply source mesh position

			cropmodels.add( clippedColorFront );
		} // for allmodels.children


		// 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;	// !!!

		// Controls
		controls = new OrbitControls( camera, renderer.domElement );
		controls.minDistance = 2;
		controls.maxDistance = 20;
		controls.target.set(0.3,-0.1,-0.6)

		// ???
		controls.addEventListener( 'change', render ); // use only if there is no animation loop

		controls.update();
		// #####################################################
		// GUI
		let gui = new GUI();
		gui.add ( cutparams, 'cuthelpers' ).listen()
		.onChange( function (val) {
			cutparams.cuthelpers = val;
			planeHelpers.forEach( ph => {
			ph.visible = val
			} );
		})
		gui.add( cutparams, 'cutx' , 0, 2, 0.001).onChange( function (val) {
			cutparams['cutx'] = val;
			x = cutplanes.find (x => x.name === 'cutx'); x.constant = val;
		})
		gui.add( cutparams, 'cutx2' , 0, 2, 0.001).onChange( function (val) {
			cutparams['cutx2'] = val;
			x = cutplanes.find (x => x.name === 'cutx2'); x.constant = val;
		})
		gui.add( cutparams, 'cuty' , 0, 2, 0.001).onChange( function (val) {
			cutparams['cuty'] = val;
			x = cutplanes.find (x => x.name === 'cuty'); x.constant = val;
		})
		gui.add( cutparams, 'cuty2' , 0, 2, 0.001).onChange( function (val) {
			cutparams['cuty2'] = val;
			x = cutplanes.find (x => x.name === 'cuty2'); x.constant = val;
		})
		gui.add( cutparams, 'cutz' , 0, 2, 0.001).onChange( function (val) {
			cutparams['cutz'] = val;
			x = cutplanes.find (x => x.name === 'cutz'); x.constant = val;
		})
		gui.add( cutparams, 'cutz2' , 0, 2, 0.001).onChange( function (val) {
			cutparams['cutz2'] = val;
			x = cutplanes.find (x => x.name === 'cutz2'); x.constant = val;
		})

} // init

function createPlaneStencilGroup( geom, iplane, 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;

	// back faces
	const mat0 = baseMat.clone();
	mat0.side = THREE.BackSide;
	mat0.clippingPlanes = [ iplane ];
	mat0.stencilFail = THREE.IncrementWrapStencilOp;
	mat0.stencilZFail = THREE.IncrementWrapStencilOp;
	mat0.stencilZPass = THREE.IncrementWrapStencilOp;

	const mesh0 = new THREE.Mesh( geom, mat0 );
	mesh0.renderOrder = renderOrder;
	group.add( mesh0 );

	// front faces
	const mat1 = baseMat.clone();
	mat1.side = THREE.FrontSide;
	mat1.clippingPlanes = [ iplane ];
	mat1.stencilFail = THREE.DecrementWrapStencilOp;
	mat1.stencilZFail = THREE.DecrementWrapStencilOp;
	mat1.stencilZPass = THREE.DecrementWrapStencilOp;

	const mesh1 = new THREE.Mesh( geom, mat1 );
	mesh1.renderOrder = renderOrder;

	group.add( mesh1 );

	return group;
} // createPlaneStencilGroup

function onWindowResize() {
	camera.aspect = window.innerWidth / window.innerHeight;
	camera.updateProjectionMatrix();
	renderer.setSize( window.innerWidth, window.innerHeight );
} // onWindowResize

function animate() {
	requestAnimationFrame( animate );	// ???

	if (planeObjects && planeObjects.length > 0){
		for ( let i = 0; i < planeObjects.length; i ++ ) {
			const iplane = cutplanes[ i ];
			const po = planeObjects[ i ];
			iplane.coplanarPoint( po.position );
			po.lookAt(
				po.position.x - iplane.normal.x,
				po.position.y - iplane.normal.y,
				po.position.z - iplane.normal.z,
			);
		}
	}
	render();
} // animate

function render() {
	renderer.render(scene, camera);
}

// ================= START =====================

init();
animate();

</script>
</body>
</html>

Stencils are dark magic. Very few people can work with them, and among these people only 1% know what they are doing.

The code above has some general bugs, like nested cycles with the same control variable:

for ( let i = 0; i < allmodels.children.length; i ++ ) {
	for ( let i = 0; i < cutplanes.length; i ++ ) {
		:
	}
}

I think that even if you fix this, the clipping will not work. Here is my hypothesis:

  • stencil-based clipping requires several objects rendered in a very specific order. For example, if you want to do render A, the process actually does A1A2A3. Some of the order is controlled by renderOrder, some by onAfterRender.
  • if you have a second object, the order becomes more complex, I’m not sure which one is the correct one, but variants are:
    • (A1A2A3) (B1B2B3) – first A, then B
    • (A1B1) (A2B2) (A3B3) – A and B completely interwoven
    • [ (A1A2) (B1B2) ] (A3B3) – A and B partially interwoven
    • and so on

ok @PavelBoytchev thank you very much for looking at the problem and analyzing :slight_smile:

“Stencils are dark magic. Very few people can work with them, and among these people only 1% know what they are doing.”

and this is the most important thing in your answer! :sunglasses:
the process itself is very difficult to understand for inexperienced users;
I tried in various ways a bit on the principle of rearranging Lego blocks, but in this case I did not achieve the intended goal; ok in one cycle i fixed i=>ii but it did indeed fix the underlying problem;
It seemed to me that the process is clearly divided into cycles

 for ( let i = 0; i < allmodels.children.length; i ++ ) {
          ...get another children object A...
      for ( let ii = 0; ii < cutplanes.length; ii ++ ) {
        ...execute a process on this children object A...
       }
     ... final with A... and go to proces with object B
 }

It’s a pity that there is no example anywhere showing how to do this :frowning: