How to make two tubes appear joined?

I create a tube of one color. Then I create another tube of the same dimensions but a different color. The final coords of the first tube are the first coords of the second tube. Yet after rendering, I can “see daylight” between the tubes. There should be no space. How do I make them appear joined?

What are the tubes actually representing?
Are there multi-tube joints allowed (T-shaped, Y-shaped etc.) ?
Maybe also this (this example seems to be doing what you need - but depending on previous answers it may be unnecessarily complicated as well)?

1 Like

image
Do you see the white color between the edge of the yellow section and the blue sections?

You can also use my addon.

THREEf geometries r136

See

GitHub - hofk/THREEf.js: three.js addon, to produce almost infinite many time-varying geometries / buffer geometries with functions (cylindrical coordinates)

As a new user of Three.js, I find that your answer raises more questions. First, what should I do to protect my application from future, breaking changes to Three.js? As for your “add-on”, does it include all of Three.js? If not, does it cause any conflicts with Three.js? What functions of Three.js are improved by the add-on? The image you posted is quite impressive, by the way.

Any chance to provide an editable working example with the code, that produces such intriguing tubes? codepen, codesandbox etc.

My expectation (actually just a guess) is that if the number of radial segments is increased (to let’s say 100), the white gaps will disappear. When a tube is constructed, the orientation of ring of vertices is somewhat tricky to calculate. So, the two tube fragments in the picture may have different orientations of the rings. If the number is increased, the difference will become smaller … and eventually invisible.

Here is an illustration of a possible reason of white gaps:

image

1 Like

There THREEf.js/THREEf.module.147/THREEf.module.js at 2ce4d04e0af7e35fb0806dd2793438504c4f01d8 · hofk/THREEf.js · GitHub you can see in the code lines


import {

    BufferGeometry, BufferAttribute, Mesh, Line, DynamicDrawUsage, LineBasicMaterial
	 
} from '../jsm/three.module.147.js';

You can see that only 6 things are used by three.js itself. There have been changes in the past with addAttribute to setAttribute and .setDynamic( true ) to .setUsage( DynamicDrawUsage ).

Such changes are shown as a note in the console for a long time.

There three.js/src/geometries/CylinderGeometry.js at 0fbae6f682f6e13dd9eb8acde02e4f50c0b73935 · mrdoob/three.js · GitHub you can see how the cylinder is created in three.js. You could copy this definition and modify it for your needs.

My addon also defines only a new geometry in a similar way. The difference to the cylinder geometry is the large amount of parameters. This makes the many shapes possible.

tube
It looks like the red section doesn’t connect because it was created separately and thus wasn’t adjusted to fit the spline. Probably I should not create the tube in sections. I should create it as one tube and then somehow re-color certain segments, if that’s possible. If I can’t do that, I could just draw an arrow to point to the segments of interest.

Unfortunately, I could not make it work on CodePen because it blocked the import of modules from the CDN… Typical environment problem having nothing to do with the actual programming!

However, here is working code for you. You will need an HTML page that contains this in the head:

<script async src="https://unpkg.com/es-module-shims@1.6.3/dist/es-module-shims.js"></script>

<script type="importmap">
  {
    "imports": {
              "three": "https://unpkg.com/three@0.151.3/build/three.module.js",
              "three/addons/": "https://unpkg.com/three@0.151.3/examples/jsm/"
    }
  }
</script>

and this in the body:

<div id="DrainPipe">Hello</div>
<script src="~/js/drainpipe.js" type="module"></script>

Of course you must adjust the file path as necessary.
And here is the entire contents of drainpipe.js. Save all as that file name.

import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import * as Curves from 'three/addons/curves/CurveExtras.js';

var container, scene, camera, renderer, controls;
let mesh, tubeGeometry;
var aPoints = new Array();

window.addEventListener('resize', drainpipeShow);

drainpipeShow();

function drainpipeShow() {
	$('#DrainPipe').empty(); // This is the div that holds the canvas.
	drainpipeGetPoints();
	drainpipeInit();
	// Uncomment one or both of the following lines to display the curve as a line, a tube, or a line inside the tube.
	//drainpipeDisplayAsLine();
	drainpipeDisplayAsTube();
	animate();
}

function drainpipeInit() {
	try {
		// SCENE
		scene = new THREE.Scene();
		scene.background = new THREE.Color(0xffffff);
		// CAMERA
		var SCREEN_WIDTH = window.innerWidth, SCREEN_HEIGHT = window.innerHeight;
		var VIEW_ANGLE = 45, ASPECT = SCREEN_WIDTH / SCREEN_HEIGHT, NEAR = 0.1, FAR = 20000;
		camera = new THREE.PerspectiveCamera(VIEW_ANGLE, ASPECT, NEAR, FAR);
		scene.add(camera);
		camera.position.set(-6000, 10, 4000);
		camera.lookAt(scene.position);
		// RENDERER
		renderer = new THREE.WebGLRenderer({ antialias: true });
		renderer.domElement.id = 'DrainPipe3DCanvas';
		renderer.setSize(SCREEN_WIDTH, SCREEN_HEIGHT);
		container = document.getElementById('DrainPipe');
		container.appendChild(renderer.domElement);
		// EVENTS
		//THREEx.WindowResize(renderer, camera);
		//THREEx.FullScreen.bindKey({ charCode: 'm'.charCodeAt(0) });
		// CONTROLS
		controls = new OrbitControls(camera, renderer.domElement);
		// LIGHT
		var light = new THREE.PointLight(0xffffff);
		light.position.set(1000, 2500, 1000);
		scene.add(light);

		////////////
		// CUSTOM //
		////////////

		var iGridSize = 2000;
		var iGridSpace = 10;

		var gridXZ = new THREE.GridHelper(iGridSize, iGridSpace, new THREE.Color(0x006600), new THREE.Color(0x006600));  // green floor/ceiling
		gridXZ.position.set(0, 1000, 0);
		scene.add(gridXZ);

		var gridXY = new THREE.GridHelper(iGridSize, iGridSpace, new THREE.Color(0x006600), new THREE.Color(0x006600));  // green floor/ceiling
		gridXY.position.set(0, 0, -1000);
		gridXY.rotation.x = Math.PI / 2;
		scene.add(gridXY);

		var gridYZ = new THREE.GridHelper(iGridSize, iGridSpace, new THREE.Color(0x006600), new THREE.Color(0x006600));  // green floor/ceiling
		gridYZ.position.set(-1000, 0, 0);
		gridYZ.rotation.z = Math.PI / 2;
		scene.add(gridYZ);
	}
	catch (err) {
		console.log('drainpipeInit: ' + err.message);
	}
}

function drainpipeDisplayAsLine() {
	try {
		let geometry = new THREE.BufferGeometry().setFromPoints(aPoints);
		let material = new THREE.LineBasicMaterial({ color: 0xff0000 });
		const curveObject = new THREE.Line(geometry, material);
		scene.add(curveObject);
	}
	catch (err) {
		console.log('drainpipeDisplayAsLine: ' + err.message);
	}
}

function drainpipeDisplayAsTube() {
	try {
		const material = new THREE.MeshPhongMaterial({
			color: 0x6495ED,
			shininess: 100,
			side: THREE.DoubleSide,
		});
		const params = {
			scale: 1,
			extrusionSegments: 25, // Bigger number yields smoother curves.
			radiusSegments: 10,
			radius: 25,
			closed: false,
		};

		let pipeSpline = new THREE.CatmullRomCurve3(aPoints);
		tubeGeometry = new THREE.TubeGeometry(pipeSpline, params.extrusionSegments, params.radius, params.radiusSegments, params.closed);
		mesh = new THREE.Mesh(tubeGeometry, material);
		mesh.scale.set(params.scale, params.scale, params.scale);
		scene.add(mesh);
	}
	catch (err) {
		console.log('drainpipeDisplayAsTube: ' + err.message);
	}
}

function animate() {
	try {
		requestAnimationFrame(animate);
	}
	catch (err) {
		console.log('animate: ' + err.message);
	}
	drainpipeRender();
	drainpipeUpdate();
}

function drainpipeUpdate() {
	try {
		controls.update();
	}
	catch (err) {
		console.log('drainpipeUpdate: ' + err.message);
	}
}

function drainpipeRender() {
	try {
		renderer.render(scene, camera);
	}
	catch (err) {
		console.log('drainpipeRender: ' + err.message);
	}
}

function drainpipeGetPoints() {
	try {
		aPoints = [];

		aPoints.push(new THREE.Vector3(-640, 641, -155));
		aPoints.push(new THREE.Vector3(-641, 600, -155));
		aPoints.push(new THREE.Vector3(-641, 569, -156));
		aPoints.push(new THREE.Vector3(-641, 537, -156));
		aPoints.push(new THREE.Vector3(-641, 506, -155));
		aPoints.push(new THREE.Vector3(-641, 475, -155));
		aPoints.push(new THREE.Vector3(-643, 444, -153));
		aPoints.push(new THREE.Vector3(-646, 413, -151));
		aPoints.push(new THREE.Vector3(-648, 392, -149));
		aPoints.push(new THREE.Vector3(-652, 362, -146));
		aPoints.push(new THREE.Vector3(-657, 330, -143));
		aPoints.push(new THREE.Vector3(-662, 300, -140));
		aPoints.push(new THREE.Vector3(-669, 269, -136));
		aPoints.push(new THREE.Vector3(-675, 239, -131));
		aPoints.push(new THREE.Vector3(-683, 209, -127));
		aPoints.push(new THREE.Vector3(-691, 179, -122));
		aPoints.push(new THREE.Vector3(-700, 150, -117));
		aPoints.push(new THREE.Vector3(-709, 120, -111));
		aPoints.push(new THREE.Vector3(-718, 90, -106));
		aPoints.push(new THREE.Vector3(-726, 60, -101));
		aPoints.push(new THREE.Vector3(-735, 31, -96));
		aPoints.push(new THREE.Vector3(-743, 1, -90));
		aPoints.push(new THREE.Vector3(-752, -29, -84));
		aPoints.push(new THREE.Vector3(-760, -58, -79));
		aPoints.push(new THREE.Vector3(-768, -87, -74));
		aPoints.push(new THREE.Vector3(-776, -118, -68));
		aPoints.push(new THREE.Vector3(-783, -148, -63));
		aPoints.push(new THREE.Vector3(-791, -178, -58));
		aPoints.push(new THREE.Vector3(-800, -207, -53));
		aPoints.push(new THREE.Vector3(-810, -237, -47));
		aPoints.push(new THREE.Vector3(-819, -266, -42));
		aPoints.push(new THREE.Vector3(-828, -296, -36));
		aPoints.push(new THREE.Vector3(-837, -325, -30));
		aPoints.push(new THREE.Vector3(-846, -355, -24));
		aPoints.push(new THREE.Vector3(-854, -385, -19));
		aPoints.push(new THREE.Vector3(-862, -415, -14));
		aPoints.push(new THREE.Vector3(-870, -445, -9));
		aPoints.push(new THREE.Vector3(-878, -475, -3));
		aPoints.push(new THREE.Vector3(-887, -505, 3));
		aPoints.push(new THREE.Vector3(-896, -534, 8));
		aPoints.push(new THREE.Vector3(-906, -563, 14));
		aPoints.push(new THREE.Vector3(-915, -593, 19));
		aPoints.push(new THREE.Vector3(-924, -623, 25));
		aPoints.push(new THREE.Vector3(-926, -632, 26));
		aPoints.push(new THREE.Vector3(-929, -643, 28));
		aPoints.push(new THREE.Vector3(-931, -652, 30));
		aPoints.push(new THREE.Vector3(-933, -663, 32));
		aPoints.push(new THREE.Vector3(-935, -673, 34));
		aPoints.push(new THREE.Vector3(-936, -683, 36));
		aPoints.push(new THREE.Vector3(-937, -693, 38));
		aPoints.push(new THREE.Vector3(-937, -704, 41));
		aPoints.push(new THREE.Vector3(-938, -714, 43));
		aPoints.push(new THREE.Vector3(-937, -724, 45));
		aPoints.push(new THREE.Vector3(-937, -735, 47));
		aPoints.push(new THREE.Vector3(-936, -745, 49));
		aPoints.push(new THREE.Vector3(-934, -755, 51));
		aPoints.push(new THREE.Vector3(-931, -775, 56));
		aPoints.push(new THREE.Vector3(-927, -795, 61));
		aPoints.push(new THREE.Vector3(-925, -805, 64));
		aPoints.push(new THREE.Vector3(-922, -814, 67));
		aPoints.push(new THREE.Vector3(-919, -824, 71));
		aPoints.push(new THREE.Vector3(-916, -833, 74));
		aPoints.push(new THREE.Vector3(-912, -842, 79));
		aPoints.push(new THREE.Vector3(-908, -850, 83));
		aPoints.push(new THREE.Vector3(-903, -858, 87));
		aPoints.push(new THREE.Vector3(-898, -866, 92));
		aPoints.push(new THREE.Vector3(-892, -873, 96));
		aPoints.push(new THREE.Vector3(-885, -880, 101));
		aPoints.push(new THREE.Vector3(-878, -887, 105));
		aPoints.push(new THREE.Vector3(-871, -893, 109));
		aPoints.push(new THREE.Vector3(-863, -899, 113));
		aPoints.push(new THREE.Vector3(-855, -904, 117));
		aPoints.push(new THREE.Vector3(-838, -915, 124));
		aPoints.push(new THREE.Vector3(-812, -930, 132));
		aPoints.push(new THREE.Vector3(-803, -934, 135));
		aPoints.push(new THREE.Vector3(-793, -938, 137));
		aPoints.push(new THREE.Vector3(-784, -941, 139));
		aPoints.push(new THREE.Vector3(-774, -944, 141));
		aPoints.push(new THREE.Vector3(-764, -946, 142));
		aPoints.push(new THREE.Vector3(-753, -948, 144));
		aPoints.push(new THREE.Vector3(-734, -949, 146));
		aPoints.push(new THREE.Vector3(-713, -950, 148));
		aPoints.push(new THREE.Vector3(-682, -950, 151));
		aPoints.push(new THREE.Vector3(-651, -950, 153));
		aPoints.push(new THREE.Vector3(-620, -949, 155));
		aPoints.push(new THREE.Vector3(-589, -948, 156));
		aPoints.push(new THREE.Vector3(-557, -947, 155));
		aPoints.push(new THREE.Vector3(-527, -946, 154));
		aPoints.push(new THREE.Vector3(-496, -945, 153));
		aPoints.push(new THREE.Vector3(-465, -945, 152));
		aPoints.push(new THREE.Vector3(-433, -944, 150));
		aPoints.push(new THREE.Vector3(-403, -944, 148));
		aPoints.push(new THREE.Vector3(-372, -944, 146));
		aPoints.push(new THREE.Vector3(-341, -943, 145));
		aPoints.push(new THREE.Vector3(-309, -943, 143));
		aPoints.push(new THREE.Vector3(-278, -943, 141));
		aPoints.push(new THREE.Vector3(-247, -942, 140));
		aPoints.push(new THREE.Vector3(-215, -942, 138));
		aPoints.push(new THREE.Vector3(-184, -943, 136));
		aPoints.push(new THREE.Vector3(-153, -943, 134));
		aPoints.push(new THREE.Vector3(-122, -942, 132));
		aPoints.push(new THREE.Vector3(-91, -942, 130));
		aPoints.push(new THREE.Vector3(-59, -942, 128));
		aPoints.push(new THREE.Vector3(-28, -941, 126));
		aPoints.push(new THREE.Vector3(3, -941, 124));
		aPoints.push(new THREE.Vector3(34, -940, 121));
		aPoints.push(new THREE.Vector3(66, -940, 119));
		aPoints.push(new THREE.Vector3(97, -940, 116));
		aPoints.push(new THREE.Vector3(128, -940, 113));
		aPoints.push(new THREE.Vector3(160, -940, 111));
		aPoints.push(new THREE.Vector3(181, -940, 109));
		aPoints.push(new THREE.Vector3(201, -940, 108));
		aPoints.push(new THREE.Vector3(222, -939, 107));
		aPoints.push(new THREE.Vector3(243, -939, 106));
		aPoints.push(new THREE.Vector3(264, -938, 105));
		aPoints.push(new THREE.Vector3(285, -938, 104));
		aPoints.push(new THREE.Vector3(306, -939, 104));
		aPoints.push(new THREE.Vector3(327, -939, 103));
		aPoints.push(new THREE.Vector3(348, -939, 102));
		aPoints.push(new THREE.Vector3(369, -939, 102));
		aPoints.push(new THREE.Vector3(390, -939, 101));
		aPoints.push(new THREE.Vector3(411, -939, 100));
		aPoints.push(new THREE.Vector3(432, -939, 99));
		aPoints.push(new THREE.Vector3(453, -938, 98));
		aPoints.push(new THREE.Vector3(474, -938, 97));
		aPoints.push(new THREE.Vector3(495, -938, 96));
	}
	catch (err) {
		console.log('drainpipeGetPoints: ' + err.message);
	}
}


Here is a template to start with. The pipe is from your code. Could you add the red fragment?

https://codepen.io/boytchev/pen/zYmNZOM?editors=0010

image

I have added the code, with the following result. Only visible when zoomed in.
CodePen

Would it be possible to share not only a snapshot, but also the code? You may fork my template, add changes, save it as your codepen and then share a link to it. If you directly change something in my template, only you can see these change.

1 Like

I’m new to CodePen and didn’t know my changes to your Pen would not be saved.

Here’s a link to a fork. https://codepen.io/ScottPendleton/pen/yLRgKgg

What we’ve learned is that the vertices of a 3D shape ending at coords x, y, z will not necessarily be reused by an identical shape starting at coords x, y, z. That’s probably true even if each tube were straight as a cylinder. And when the tubes are splines, gaps can be noticeable especially if the combined paths bend at that coordinate. Nothing really to do about it, just something to know.

Well, let me do something. Try your pen with the following two changes:

  1. Line 159: change coordinates (222, -939, 107)(221, -940, 107)
  2. Line 188: change radiusSegments 1040

Does it look a little bit better? The first change is to make tangents happy. The second change is to make (bi)normals happy.

There is another alternative, in case this fails. The end vertices of both tubes can be glued, so there will be no gap at all.

Maybe another approach is worth a try:


Demo: https://codepen.io/prisoner849/full/BaqprmN

PS It’s just an example, not the ultimate solution.

3 Likes

The two changes appear to work. For future situations, if I found the segments exhibiting an obvious gap, would I have to experiment to get the coords that closed the gap?

I actually think that this solution is the best, for my particular purpose. It would be easier programmatically to create one pipe and then change the color of a segment – provided that it’s possible to identify the segment. I can’t tell if your code picks a segment at random to make red, or if it specifies one explicitly.

I have not yet used a shader nor a GUI; I’m still quite new with ThreeJS. Thanks to all for your suggested solutions!

If by “experiment” you mean “trial and error”, the answer is no. Here is an image of 4 situations of two tubes. Your initial case was (B) – tubes had same ends points, but had different directions. What I changed was to match the directions, this is case (D), so tubes share both end points and directions.

This is called continuity and has several levels of geometric continuity. I just upgraded G0 to G1 by modifying the coordinates of the second point of the second tube, so that the deltas (i.e. directions, tangents) between the last two points and the first two points are the same:

Excellent diagram. You understand the problem deeply. I lack confidence that I could calculate the adjustment nor identify the point to adjust. In this case, you didn’t adjust the end point (aPoints2[0]) of the second tube, but rather the next point (aPoints2[1]). Is that always the correct point to adjust? And how do you determine the amount of adjustment?