13

Is there a way to create a Three.js 3D line series with width and thickness?

Even though the Three.js line object supports linewidth, this attribute is not yet supported in all browsers on all platforms in WebGL.

Here's where you set linewidth in Three.js:

    var material = new THREE.LineBasicMaterial({
        color: 0xff0000,
        linewidth: 5
    });

The Three.js ribbon object - which had width - has recently been dropped.

The Three.js tube object generates 3D extrusions but - being Bezier-based - the lines do not pass through the control points.

Can anybody think of a method of drawing a line series (polylines, plotlines) in Three.js that has some sort of user definable 'bulk' such as width, thickness or radius?

This question may be a restating of this question: Extruding a graph in three.js.

Given that I do not think that there is a readily available method, I would be happy to participate in an effort to create a simple function that responds to this question.

But a response that points to an existing workable method would be cool...

As WestLangley suggests, one possible solution includes the polyline being of constant pixel width - as is currently available with the Three.js canvas renderer.

A comparison of the two renderers is shown here:

Canvas and WebGL Lines Compared via GitHub Pages

Canvas and WebGL Lines Compared via jsFiddle

enter image description here

A solution where you could specify linewidth and similar results occurred on both renderers would be very cool.

There are, however, other ways of thinking of 3D lines where lines have actual physical constructs. They cast shadows, they respond to events. These also need to be looked into.

Here are links to GitHub Pages with two demos of lines made up of multiple meshes:

Spheres and Cubes Polyline

Sphere and Cylinder Polylines

An 'expensive solution. Each joint is made up of a full sphere.

Cubes Polylines

Cubes Polylines

My guess is that building either of these as smooth single meshes will be complex to problems to solve. So in the meantime here is a link to a partial visualization of 3D lines that are wide and have height:

3D Box Line on jsFiddle

3d box lines

The goal is have to code 'with a low level of complexity - in other words - for dummies'. Thus a 3D line should be as easy and as familiar as adding a sphere or cube. Geometry + material = mesh > scene. And the geometry should be quite economical in terms of creating vertices and faces.

The lines should have width and height. Up is always in the Y direction. The demo shows this. What the demo does not show is corners being mitred nicely...

Community
  • 1
  • 1
Theo
  • 376
  • 1
  • 3
  • 13
  • Would a piecewise-linear `TubeGeometry` suffice? (You would have to avoid sharp corners.) – WestLangley Feb 06 '14 at 15:42
  • Hi WestLangley. TubeGeometry is indeed a powerful tool but it does not even draw a rectangle. Control points are not corners. And there are a good number of diagrammatic things where the line mast mast through the vertex or it is not believable... – Theo Feb 08 '14 at 10:01
  • Also, I am not sure what you mean by 'piecewise-linear'. Can you describe this term more fully? – Theo Feb 08 '14 at 10:30
  • (1) http://en.wikipedia.org/wiki/Piecewise_linear_curve. (2) After thinking a bit, tube geometry can be made to work with some changes, but it is overkill. Perhaps a linked chain of rectangular sprites (so they always face the camera), with a circular sprite at each control point to cover up the joint. – WestLangley Feb 08 '14 at 17:04
  • @WestLangley >> Perhaps a linked chain of rectangular sprites. Good one! This might work well in some situations. But the real need is for the piecewise-linear line string - or polyline - with width and thickness that goes from 3D vertex to 3D vertex. Since you are interested I will write up a more detailed spec and add a jsFiddle or two in the next day or so... – Theo Feb 10 '14 at 06:12
  • Please do, @Theo... Also, please specify if you want the polyline to have a constant pixel-width, or if you want the thickness to be a function of distance from the camera. – WestLangley Feb 10 '14 at 16:20
  • +1 to getting this---we've been needing it, since lines don't have width in windows. – Jason Grout Feb 14 '14 at 03:16
  • On OSX, the demos are identical -- except with Canvas, the end-caps look cleaner. – WestLangley Feb 15 '14 at 14:49
  • Very interesting. On my Android Nexus 5 phone the lines are the same in both versions - and perhaps a couple of pixels wide, certainly wider than 1 pixel. And I cannot change the width. On my Chromebook Pixel, the lines in both are about 5 pixels wide - And I can't change the width. On My Lenovo nvidia gpu Win 8.1 with Chrome, the canvas lines are much thicker then the WebGL and I can edit the width. Ditto on IE 11 and Latest FF - but the slider does not update the width. What a mess! – Theo Feb 16 '14 at 07:40
  • I fixed and issue in my code. Lines are updating width on Chromebook and Android Nexus phone - with both the canvas and WebGL Renderers. On Windows, with all three browsers only the canvas renderer is changing widths. Lines in the WebGL renderer stay at one pixel. – Theo Feb 16 '14 at 08:56
  • @Theo TubeGeometry can render your cubes example and will create beautiful 3d lines in that case, and they will cast shadows. Is the problem you are concerned with the previous example, where the bends are tight? Tube geometry does not handle that well. – WestLangley Feb 16 '14 at 12:08
  • @WestLangley zz85's TubeGeometry is certainly a lovely tool and very useful in many situations. The issue with TubeGeometry is that the vertices are used as control points. The generated geometry does not follow the actual path of the vertices. Thus, as you point out, it does not do tight corners. And you could use it for an accurate depiction of, say, the trajectory of a bullet. – Theo Feb 18 '14 at 00:33
  • @Theo Actually, `TubeGeometry` supports polylines. See http://jsfiddle.net/6m2Fh/. The thing is, the tube slices are equally-spaced, and a high number of slices are required. A simple modification to TubeGeometry could include the original points. On acute angles, the corners do not look good. It would be great to add support for a minimum curvature. – WestLangley Feb 18 '14 at 01:12
  • @WestLangley. Nice demo! TubeGeometry has a lot of good features. And, yet, we need more. I'm working on a project to map all approaches to all airports in the world with instrument landing systems. There will be so many wiggly routes that the code name is 'Flying Spaghetti Monster'. If each waypoint creates four vertices, that's cool. But if each line segment takes hundred of vertices then that is not so good. The other aspect that has to be considered is that Three.js is about 'coding for dummies'. Drawing a 3D polyline should be as easy as drawing a torus or even regular Three.js line. – Theo Feb 19 '14 at 09:40
  • @WestLangley, I have added two demos. The first a jsFiddle version of 'Canvas and WebGL Lines' and the second is titled '3D Box lines' - which is where we could start thinking about some actual code to put in place that answers the original question. Separately but related, regarding the canvas and WebGL lines demo, you mentioned that the demo works on OSX. Which browsers on the Mac is it working on? BTW, I also note that the demo works on my Android Nexus 5, Ubuntu 13.04 with FF and two of my Chromebooks. But not om Win 8.1. – Theo Feb 23 '14 at 07:24
  • ...Or use Cylnders: http://jsfiddle.net/eX4ba/1 ... On Mac, line width is currently supported by Chrome and Safari, at least. – WestLangley Feb 25 '14 at 17:07

2 Answers2

8

I cooked up a possible solution which I believe meets most of your requirements:

http://codepen.io/garciahurtado/pen/AGEsf?editors=001

enter image description here

The concept is fairly simple: render any arbitrary geometry in "wireframe mode", then apply a full screen GLSL shader to it to add thickness to the wireframe lines.

The shader is inspired by the blur shaders in the ThreeJS distro, which essentially copy the image a bunch of times along the horizontal and vertical axis. I automated that process and made the number of copies a user defined parameter, while ensuring that the copies were offset by 1 pixel.

I used a 3D cube mesh in my demo (with an ortho camera), but it should be trivial to convert it to a poly line.

The real meat and potatoes of this thing is in the custom shader (fragment shader portion):

    uniform sampler2D tDiffuse;
    uniform int edgeWidth;
    uniform int diagOffset;
    uniform float totalWidth;
    uniform float totalHeight;
    const int MAX_LINE_WIDTH = 30; // Needed due to weird limitations in GLSL around for loops
    varying vec2 vUv;

    void main() {
        int offset = int( floor(float(edgeWidth) / float(2) + 0.5) );
        vec4 color = vec4( 0.0, 0.0, 0.0, 0.0);

        // Horizontal copies of the wireframe first
        for (int i = 0; i < MAX_LINE_WIDTH; i++) {
            float uvFactor = (float(1) / totalWidth);
            float newUvX = vUv.x + float(i - offset) * uvFactor;
            float newUvY = vUv.y + (float(i - offset) * float(diagOffset) ) * uvFactor;  // only modifies vUv.y if diagOffset > 0
            color = max(color, texture2D( tDiffuse, vec2( newUvX,  newUvY  ) ));    
            // GLSL does not allow loop comparisons against dynamic variables. Workaround below
            if(i == edgeWidth) break; 
        }

        // Now we create the vertical copies
        for (int i = 0; i < MAX_LINE_WIDTH; i++) {
            float uvFactor = (float(1) / totalHeight);
            float newUvX = vUv.x + (float(i - offset) * float(-diagOffset) ) * uvFactor; // only modifies vUv.x if diagOffset > 0
            float newUvY = vUv.y + float(i - offset) * uvFactor;
            color = max(color, texture2D( tDiffuse, vec2( newUvX, newUvY ) ));  
            if(i == edgeWidth) break;
        }

        gl_FragColor = color;
    }

Pros:

  • No need for additional geometry beyond the line vertices
  • Line thickness is user definable
  • A full screen shader should be relatively gentle on the GPU
  • Can be implemented fully within the WebGL canvas

Cons:

  • Line thickness is close to pixel perfect on horizontal and vertical edges, but slightly off on diagonal edges. This is due to the algorithm used and is a limitation of the solution. Having said that, for low line thickness and complex geometries, this is barely noticeable with the naked eye.
  • The joints between lines will show gaps for large enough line thickness. You can play with the Codepen demo to see what I mean. I started to implement a solution to this by adding a second "diagonal pass", but it got a little hairy and I think this would only be an issue for higher line thicknesses (+8 pixels) or extreme line angles. If you are interested in this solution, you can look at the original source to see where I was going with it.
  • Since this uses a full screen filter, you can only use the WebGL context for displaying objects of this thickness. Showing various line widths would require additional rendering passes.
Garcia Hurtado
  • 952
  • 9
  • 15
  • I don't like that I'm posting a conversational comment but wow this is a really great answer, above and beyond. The pros v. cons is an excellent addition. Wish I could give you more upvotes! – Jacksonkr May 13 '21 at 18:21
2

As a potential solution. You could take your 3d points, then use THREE.Vector3.project method to figure out screen-space coordinates. Then simply use canvas and it's lineTo and moveTo operations. Canvas 2d context does support variable line thickness.

var w = renderer.domElement.innerWidth;
var h = renderer.domElement.innerHeight;
vector.project(camera);
context2d.lineWidth = 3;
var x = (vector.x+1)*(w/2);
var y = h - (vector.y+1)*(h/2);
context2d.lineTo(x,y);

Also, i don't think you can use the same canvas for that, so it would have to be a layer (another canvas) above your gl rendering context canvas.

If you have infrequent camera changes - it is also possible to construct line out of polygons and update it's vertex positions based on camera transform. For orthographic camera this would work best as only rotations would require vertex position manipulation.

Lastly, you could disable canvas clearing and draw your lines several times with offset inside a circle or a box. After that you can re-enable clearing. This would require several extra draw operations, but it's probably the most scalable approach.

The reason lines don't work as you'd expect out of the box is due to how ANGLE works, it's used in Chrome and in Firefox to my knowledge, it emulates OpenGL via DirectX. Guys from ANGLE state that WebGL spec only requires support of line thickness up-to 1, so they do not see it as a bug and don't intend to "fix" it. Line thickness should work on non-windows OSs though, where ANGLE is not used.

travnik
  • 690
  • 7
  • 18