Wrong tangent at the end of the curve

In this example DynamicTubeGeometryCaps I had a problem with the tangent some time ago. I solved it there by doing my own calculation. (lines 228 - 244).

I now have a project where this method doesn’t work so I use .getTangent( t, optionalTarget ).

This works in many cases, but for some parameter combinations it gives an incorrect tangent at the end of the curve.

I took a look at the source code:

 	// Returns a unit vector tangent at t
	// In case any sub curve does not implement its tangent derivation,
	// 2 points a small delta apart will be used to find its gradient
	// which seems to give a reasonable approximation

	getTangent( t, optionalTarget ) {

		const delta = 0.0001;
		let t1 = t - delta;
		let t2 = t + delta;

		// Capping in case of danger

		if ( t1 < 0 ) t1 = 0;
		if ( t2 > 1 ) t2 = 1;

		const pt1 = this.getPoint( t1 );
		const pt2 = this.getPoint( t2 );

		const tangent = optionalTarget || ( ( pt1.isVector2 ) ? new Vector2() : new Vector3() );

		tangent.copy( pt2 ).sub( pt1 ).normalize();

		return tangent;


A fixed value for delta is used. This seems to be the problem. If I set const delta = 0.01; in some of my cases it works. With smaller values the error occurs. But I could not discover a general systematic.

I will now call the method as

customGetTangent( t, optionalTarget, delta ) {

// const delta = 0.0001;


in my code. Then I check if the last tangent deviates strongly and recalculate it with larger delta until it is correct.

Would it be appropriate to have delta as a parameter in general in the method or is there something against it? Is there a better variant to avoid the error?

This is strange. Smaller deltas are supposed to give better approximation of tangents. The only problem could be lost of precision, but this will happen with extremely small deltas (0.0001 is far away from danger).

In theory, I think so too. That’s why I was a bit surprised about the solution (sham solution?).
With my frequent scatterbrains sometimes nice things come out, e.g. if one forgets to make a bracket around the difference in the numerator of the fraction.

But here it is really strange.

I have added some tests and console.log but cannot locate the problem.

There you can find the test example.

Basically it is Triangulation cylinder with holes

Added deformation similar to DynamicTubeGeometryCaps.

For this lines 1148 to 1156
If you comment out triangGeo.morph( points ); you get the original triangulation.

In the class deformableCylinderWithHoles extends THREE.BufferGeometry { ... only some things were added to the original, also some console.log.

Lines 50 to 64

Lines 201 to 265 g.morph = function ( points ) { ...

Lines 267 to 290 function getTangent( t, optionalTarget, delta ) { ... instead of the original.


Could you make a simpler example with pure TubeGeometry (i.e. without all the custom modifications like holes, etc).

In the past I had a problem with the end points of some spline curves, and I found it was due to some strange code in Three.js – it was something like “if this vector is too short, assume it is (0,1,0)”. I tried to find this code, but failed. It is either fixed, or my memory is malfunctioning. If I debug a shorter example I might be able to recover my memories. Maybe just a code fragment with how you define the curve and the tube would be sufficient to start with.

1 Like

I have removed the holes. Mandatory for the triangulation is the specification of the boundary fronts bottom and top. Otherwise, the algorithm continues to run without limits.

All other functions do the triangulation according to E. Hartmann.

I have narrowed down the error a bit, see line 950 and following.

Variant c:

now lines
185 // … morph …
251 // … getTangent - instead of original …

Wow, 1000 lines. I thought that the original TubeGeometry exibits a problem at the end for some specific curve …

The 1000 rows are not the problem since the actual triangulation ( class deformableCylinderWithHoles extends THREE.BufferGeometry { ... ) works, comment out line 989 triangGeo.morph( points ); . But in contrast to the cylinder or tube geometry, the positions are unordered. I now suspect a precision problem.

The positions are scanned in height and angle in order to obtain an assignment.
For this purpose, only a storage was made at the points where the positions are written, which does not affect the triangulation algorithm. So the positions are very random!

The total height also had to be calculated, as the height increases due to overhangs to adjust the cylinders lines 50 … 60.

line 62 g.t = [ ]; // relative height and sin cos of angle

function makeBoundaryFront( bd, divAdp, phiAdp, sign ) {...
line 312
g.t.push( ( y - g.cylBtm ) / g.height, x / g.radius, z / g.radius ); // relative height and sin cos of angle

function makeNewTriangles( m ) {...
line 671
g.t.push( ( y - g.cylBtm ) / g.height, x / g.radius, z / g.radius ); // relative height and sin cos of angle

g.morph = function ( points ) { ...
line 208
g.curve.getPoint( g.t[ idx * 3 ], v3c ); // center point

My excitement about the 1000 lines is that I don’t know what part of them are important and what are not. It will take a lot of time to understand all of them. So, in my request for a short demo I was referring to the title of the discussion – there is some curve with a wrong tangent at the end. I will try to dig out that curve from the code, in order to see how it behaves and why its tangent is wrong (or at east it is not what is expected to be). I’m not sure I understand how the triangulation causes the wrong tangent. It should not, because happens after the curve is defined.

Edit: my understanding of the issue of the problem is that because of the wrong tangent, the top “ring” of the tube is not oriented well. I have marked it with red dots. All the previous “rings” look OK.


Now I’, not sure whether this is the problem that you talk about.

Edit2: managed to extract the curve. The Red tube is made with TubeGeometry, the green tube is with your triangulation. They are almost identical in shape, except that the last ring of your tube is wrong. And you want to know why it is wrong.

(now fighting with the meaning of top, as it appears to be important parameter)

Here is what I have found so far. I hope it will help you.

First I added visualization of the tangents along the curve. These are the tiny red arrows. In this way, I got confirmation that one of the tangents (near the top) is wrong, as it points in a drastically different direction than the others:

Then I found that inside morph() you loop through array t[...]. However, it contains wrong values for the top front. For example, these lines:

g.curve.getTangent( g.t[ idx * 3 ], tangent );
console.log( idx, g.t[ idx * 3 ] ); // added by me


16 1.0018504347826087
17 1.0018504347826087
31 1.0018504347826087

So, the code tries to get a tangent at position beyond 1, while the curve is defined only in range [0,1].

When I add “protection” for out-of-range values, the top of the cylinder looks fine:

g.curve.getTangent( Math.min(g.t[ idx * 3],1) , tangent ); // added Math.min

My conclusion is that the out-of-range values in array t[...] cause the problem. The wrong values are generated by a call to makeBoundaryFront( g.top, g.div4Top, -g.phiTop, -1 ), so maybe this is the place where the error is caused.

An easy fix could be to clamp the relative height in makeBoundaryFront:

g.t.push( THREE.MathUtils.clamp( (y-g.cylBtm )/g.height,0,1), ... );

but a better solution would be to generate only good values without clamping.


Impressive how you have analyzed this! :+1: :+1: :+1:

Thanks a lot!

The calculation of g.height, y and the relative height g.t is quite complex.

const rAdpTop = g.d / Math.sin( Math.PI / g.div4Top / 4 ) / 2; // adaptation radius top
g.cylTop = g.top + rAdpTop - Math.sqrt( rAdpTop * rAdpTop - g.radius * g.radius );; // top with adjustment
const rAdpBtm = g.d / Math.sin( Math.PI / g.div4Btm / 4 ) / 2; // adaptation radius bottom
g.cylBtm = g.btm - ( rAdpBtm - Math.sqrt( rAdpBtm * rAdpBtm - g.radius * g.radius ) ); // bottom with adjustment
g.height = g.cylTop - g.cylBtm;  // cylinder height with adjustment parts top, bottom , - for external use as well

y = bd + sign * ( -rAdp + Math.sqrt( rAdp * rAdp - g.radius * g.radius * Math.cos( phi ) * Math.cos( phi ) ) );

g.t.push( ( y - g.cylBtm ) / g.height, x / g.radius, z / g.radius ); // relative height and sin cos of angle

This obviously leads to somewhat larger inaccuracies in some parameter constellations.
Since this can also happen to me with other complicated calculations, I will not compensate for the error in the calculation itself, but adjust the tangent calculation. I think then to be prepared for all cases.

Do I see this correctly?

// Capping in case of danger

if ( t1 < 0 ) {
    t1 = 0;
    t2 = delta;

if ( t2 > 1 ) {
    t2 = 1;  
    t1 = 1 - delta;