I am trying to create a tee-piece which is a fitting in the plumbing domain. It consist of 2 tubes that are merged together and has 3 openings as shown in this picture.
I have written some code in threejs where I am trying to create a tube mesh1 and another tube mesh2 and then try to union them into mesh3 with the library @enable3d/three-graphics/jsm/csg - thanks to @Marquizzo. After using the function CSG.union and adding the mesh to the scene I can see that I get one tee-piece but it has also created a hole in geometry 1, which was not expected. You can see a picture of the correct holes(green) and the wrongly created hole (red) here:
it should instead look like this and be as one geometry.
Can anyone tell me how CSG works and why I am getting an extra hole on the backside of the first geometry?
import React, { Component } from 'react';
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
import { CSG } from '@enable3d/three-graphics/jsm/csg';
export default class TubeViewer extends Component {
componentDidMount() {
//Add Scene
this.scene = new THREE.Scene();
//Add Renderer
this.renderer = new THREE.WebGLRenderer({ antialias: true });
this.renderer.setClearColor('#808080');
this.renderer.shadowMap.enabled = true;
this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
this.renderer.setPixelRatio(window.devicePixelRatio);
this.renderer.setSize(window.innerWidth, window.innerHeight);
this.mount.appendChild(this.renderer.domElement);
//Add Camera
const fov = 60;
const aspect = window.innerWidth / window.innerHeight;
const near = 1.0;
const far = 1000.0;
this.camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
this.camera.position.set(1, aspect, 1, 1000);
//Tee-piece
const curve1 = new THREE.LineCurve(new THREE.Vector3(2, 0, 0), new THREE.Vector3(2, 0, 0.1));
const curve11 = new THREE.LineCurve(new THREE.Vector3(2.0, 0, 0.05), new THREE.Vector3(2.05, 0, 0.05));
const geometry1 = new THREE.TubeGeometry(curve1, 20, 0.025, 8, false);
const geometry2 = new THREE.TubeGeometry(curve2, 20, 0.025, 8, false);
const material = new THREE.MeshBasicMaterial({ color: '#C0C0C0' });
const mesh1 = new THREE.Mesh(geometry1, material);
const mesh2 = new THREE.Mesh(geometry2, material);
const mesh3 = CSG.union(mesh1, mesh2);
this.scene.add(mesh3);
//Add raycaster to for interactivity
this.raycaster = new THREE.Raycaster();
this.mouse = new THREE.Vector2();
this.renderer.domElement.addEventListener('click', onClick.bind(this), false);
function onClick(event) {
event.preventDefault();
this.mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
this.mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
this.raycaster.setFromCamera(this.mouse, this.camera);
var intersects = this.raycaster.intersectObjects(this.scene.children, true);
if (intersects.length > 0) {
console.log('Intersection:', intersects[0]);
//console.log(intersects[0].object.uuid);
// console.log(`GUID: ${intersects[0]}`);
let object = intersects[0].object;
object.material.color.set(Math.random() * 0xffffff);
}
}
//Settings
//Add Camera Controls
const controls = new OrbitControls(this.camera, this.renderer.domElement);
controls.addEventListener('change', this.render); // use if there is no animation loop
controls.minDistance = 2;
controls.maxDistance = 10;
controls.target.set(0, 0, -0.2);
controls.update();
///Add AMBIENT LIGHT
let light = new THREE.DirectionalLight(0xffffff, 1.0);
light.position.set(20, 100, 10);
light.target.position.set(0, 0, 0);
light.castShadow = true;
light.shadow.bias = -0.001;
light.shadow.mapSize.width = 2048;
light.shadow.mapSize.height = 2048;
light.shadow.camera.near = 0.1;
light.shadow.camera.far = 500.0;
light.shadow.camera.near = 0.5;
light.shadow.camera.far = 500.0;
light.shadow.camera.left = 100;
light.shadow.camera.right = -100;
light.shadow.camera.top = 100;
light.shadow.camera.bottom = -100;
this.scene.add(light);
light = new THREE.AmbientLight(0xffffff, 0.7);
this.scene.add(light);
//Start animation
this.start();
}
//Unmount when animation has stopped
componentWillUnmount() {
this.stop();
this.mount.removeChild(this.renderer.domElement);
}
//Function to start animation
start = () => {
//Rotate Models
if (!this.frameId) {
this.frameId = requestAnimationFrame(this.animate);
}
};
//Function to stop animation
stop = () => {
cancelAnimationFrame(this.frameId);
};
//Animate models here
animate = () => {
//ReDraw scene with camera and scene object
if (this.cubeMesh) this.cubeMesh.rotation.y += 0.01;
this.renderScene();
this.frameId = window.requestAnimationFrame(this.animate);
};
//Render the scene
renderScene = () => {
if (this.renderer) this.renderer.render(this.scene, this.camera);
};
render() {
return (
<div
style={{ width: '800px', height: '800px' }}
ref={(mount) => {
this.mount = mount;
}}
/>
);
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
enter code here
?
For CSG you'll need solid bodies. These tubes are open.
I created an example using cylinders (tubes are involved to cap) so you can test it.
These cylinders are open ended, so they fail in the same way as your tubes. https://codepen.io/flatworldstudio/pen/bGBjmrP
These are closed, and CSG works as expected. https://codepen.io/flatworldstudio/pen/VwmBRoL
(I'm using a different version of CSG, but they all seem to be built on the same code)