Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to get three.js LineSegments to only render visible lines

Tags:

three.js

I’m trying to get Three.js to render only the FrontSide outlines of geometries. What I want to achieve is a look as close as possible to this:

enter image description here

With BoxGeomtry I came close to what I want, but using LineSegments on a CylinderGeometry gives vertical lines, which makes sense. Can you think of a way I can draw only the "visible" outlines?

Cylinder with LineSegments here

Here is what I tried so far:

let coloredMaterial = new THREE.MeshBasicMaterial({
  color: 0xFFD033,
  polygonOffset: true,
  polygonOffsetFactor: 1,
  polygonOffsetUnits: 1
});

let brick = new THREE.Mesh(geometry, coloredMaterial);

let edges = new THREE.EdgesGeometry(brick.geometry);
var outline = new THREE.LineSegments(edges, new THREE.LineBasicMaterial({
  color: 0x1B3740,
  linewidth: 1.5
}));

let knobGeometry = new THREE.CylinderGeometry(7, 7, 7, 20);
let knob = new THREE.Mesh(knobGeometry, coloredMaterial);

let knobOutline = new THREE.LineSegments(
  new THREE.EdgesGeometry(knob.geometry),
  new THREE.LineBasicMaterial({
    color: 0x1B3740,
    linewidth: 1.5
  })
);
like image 482
Marijan Avatar asked Oct 27 '25 10:10

Marijan


1 Answers

This answer is based on @WestLangley's suggestion on the comments, specifically the model used by LDrawLoader on conditional lines.


Control points model

The idea behind conditional lines is to use control points to determine which lines should be drawn.

If the two control points lie on the same side of the clip plane, created by extrapolating the line to infinity, then the line is drawn. Else it is discarded.

https://www.ldraw.org/uploads/images/Articles/opline.gif

Let's consider 2 lines (E,B) and (F,C):

For (E,B), let's use (A) and (C) as controls points. We can clearly see that both control points are on the same side of the plane created by (E,B). Therefore, this line is drawn.

For (F,C), let's use (B) and (D) as controls points. Now, both control points lie on different sides of the plane. So, this line is discarded.


As the implementation of this model can be quite lengthy, I've setup a JSFiddle that can be used as a reference. It's not quite perfect, but I believe it should be helpful enough.

enter image description here

We can't use CylinderBufferGeometry as a base for the edges geometry, because it uses an indexed buffer geometry. Since the control points are determined by each of the lines, and not the vertices, we don't use indices.

For edges that aren't conditional, we can use the same point for both controls, e.g top and bottom circles.

An important thing to notice is that we can't determine, with this model, if a line would be occluded by geometry (what you described as frontSide). So, I used the actual knobs to occlude the back lines.

var conditionalLineVertShader = /* glsl */ `

            attribute vec3 control0;
            attribute vec3 control1;
            attribute vec3 direction;

            varying float discardFlag;

            #include <common>
            #include <color_pars_vertex>
            #include <fog_pars_vertex>
            #include <logdepthbuf_pars_vertex>
            #include <clipping_planes_pars_vertex>

            void main() {

            #include <color_vertex>

            vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );
            gl_Position = projectionMatrix * mvPosition;

            // Transform the line segment ends and control points into camera clip space
            vec4 c0 = projectionMatrix * modelViewMatrix * vec4( control0, 1.0 );
            vec4 c1 = projectionMatrix * modelViewMatrix * vec4( control1, 1.0 );
            vec4 p0 = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
            vec4 p1 = projectionMatrix * modelViewMatrix * vec4( position + direction, 1.0 );

            c0.xy /= c0.w;
            c1.xy /= c1.w;
            p0.xy /= p0.w;
            p1.xy /= p1.w;

            // Get the direction of the segment and an orthogonal vector
            vec2 dir = p1.xy - p0.xy;
            vec2 norm = vec2( -dir.y, dir.x );

            // Get control point directions from the line
            vec2 c0dir = c0.xy - p1.xy;
            vec2 c1dir = c1.xy - p1.xy;

            // If the vectors to the controls points are pointed in different directions away
            // from the line segment then the line should not be drawn.
            float d0 = dot( normalize( norm ), normalize( c0dir ) );
            float d1 = dot( normalize( norm ), normalize( c1dir ) );

            discardFlag = float( sign( d0 ) != sign( d1 ) );

            #include <logdepthbuf_vertex>
            #include <clipping_planes_vertex>
            #include <fog_vertex>

            }
            `;

var conditionalLineFragShader = /* glsl */ `

            uniform vec3 diffuse;
            varying float discardFlag;

            #include <common>
            #include <color_pars_fragment>
            #include <fog_pars_fragment>
            #include <logdepthbuf_pars_fragment>
            #include <clipping_planes_pars_fragment>

            void main() {

            if ( discardFlag > 0.5 ) discard;

            #include <clipping_planes_fragment>

            vec3 outgoingLight = vec3( 0.0 );
            vec4 diffuseColor = vec4( diffuse, 1.0 );

            #include <logdepthbuf_fragment>
            #include <color_fragment>

            outgoingLight = diffuseColor.rgb; // simple shader

            gl_FragColor = vec4( outgoingLight, diffuseColor.a );

            #include <premultiplied_alpha_fragment>
            #include <tonemapping_fragment>
            #include <encodings_fragment>
            #include <fog_fragment>

            }
            `;

var renderer = new THREE.WebGLRenderer({
  antialias: true
});
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

var scene = new THREE.Scene();
scene.background = new THREE.Color(0xffffff);
var camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
var controls = new THREE.OrbitControls(camera, renderer.domElement);

camera.position.set(10, 13, 10);
controls.target.set(0, 0, 0);


// cube

var cubeGeometry = new THREE.BoxBufferGeometry(10, 5, 10);
var cubeMaterial = new THREE.MeshBasicMaterial({
  color: 0xFFD033,
  polygonOffset: true,
  polygonOffsetFactor: 1,
  polygonOffsetUnits: 1
});
var cube = new THREE.Mesh(cubeGeometry, cubeMaterial);
scene.add(cube);

var edgesGeometry = new THREE.EdgesGeometry(cubeGeometry);
var edgesCube = new THREE.LineSegments(edgesGeometry, new THREE.LineBasicMaterial({
  color: 0x1B3740,
  linewidth: 1.5
}));
edgesCube.position.y += 0.6;
scene.add(edgesCube);


// knob

var knobGeometry = new THREE.CylinderGeometry(1.4, 1.4, 0.8, 30);
var knob = new THREE.Mesh(knobGeometry, cubeMaterial);
knob.position.set(-2.5, 2.9, -2.5);
scene.add(knob);

var knob = new THREE.Mesh(knobGeometry, cubeMaterial);
knob.position.set(2.5, 2.9, 2.5);
scene.add(knob);

var knob = new THREE.Mesh(knobGeometry, cubeMaterial);
knob.position.set(-2.5, 2.9, 2.5);
scene.add(knob);

var knob = new THREE.Mesh(knobGeometry, cubeMaterial);
knob.position.set(2.5, 2.9, -2.5);
scene.add(knob);


// knob edges

var edgesMaterial = new THREE.ShaderMaterial({
  vertexShader: conditionalLineVertShader,
  fragmentShader: conditionalLineFragShader,
  uniforms: {
    diffuse: {
      value: new THREE.Color(0x1B3740)
    }
  },
  linewidth: 1.5
});

var edgesKnob = createCylinderEdges(1.4, 0.8, 30);
edgesKnob.position.set(-2.5, 2.9 + 0.6, -2.5);
scene.add(edgesKnob);

var edgesKnob = createCylinderEdges(1.4, 0.8, 30);
edgesKnob.position.set(2.5, 2.9 + 0.6, 2.5);
scene.add(edgesKnob);

var edgesKnob = createCylinderEdges(1.4, 0.8, 30);
edgesKnob.position.set(-2.5, 2.9 + 0.6, 2.5);
scene.add(edgesKnob);

var edgesKnob = createCylinderEdges(1.4, 0.8, 30);
edgesKnob.position.set(2.5, 2.9 + 0.6, -2.5);
scene.add(edgesKnob);


window.addEventListener('resize', onResize);


function animate() {

  requestAnimationFrame(animate);
  controls.update();
  renderer.render(scene, camera);

};


function createCylinderEdges(radius, height, segments) {

  var geometry = new THREE.BufferGeometry();

  var v0 = new THREE.Vector3();
  var v1 = new THREE.Vector3();

  var vertices = [];
  var control0 = [];
  var control1 = [];
  var directions = [];

  // top / bottom circles

  for (var v = 0; v <= 1; v++) {

    for (var x = 0; x < segments; x++) {

      var th = (x / segments) * Math.PI * 2;
      var c0 = ((x - 1) / segments) * Math.PI * 2;
      var c1 = ((x + 1) / segments) * Math.PI * 2;

      var sinTheta = Math.sin(th);
      var cosTheta = Math.cos(th);

      v0.x = radius * sinTheta;
      v0.y = -v * height + height / 2;
      v0.z = radius * cosTheta;

      sinTheta = Math.sin(c1);
      cosTheta = Math.cos(c1);

      v1.x = radius * sinTheta;
      v1.y = -v * height + height / 2;
      v1.z = radius * cosTheta;

      vertices.push(v0.x, v0.y, v0.z);
      vertices.push(v1.x, v1.y, v1.z);

      control0.push(v0.x, v0.y, v0.z);
      control0.push(v0.x, v0.y, v0.z);

      control1.push(v0.x, v0.y, v0.z);
      control1.push(v0.x, v0.y, v0.z);

      directions.push(v1.x - v0.x, v1.y - v0.y, v1.z - v0.z);
      directions.push(v1.x - v0.x, v1.y - v0.y, v1.z - v0.z);

    }


  }

  // vertical edges

  for (var x = 0; x < segments; x++) {

    var th = (x / segments) * Math.PI * 2;
    var c0 = ((x - 1) / segments) * Math.PI * 2;
    var c1 = ((x + 1) / segments) * Math.PI * 2;

    var sinTheta = Math.sin(th);
    var cosTheta = Math.cos(th);

    v0.x = radius * sinTheta;
    v0.y = height / 2;
    v0.z = radius * cosTheta;

    v1.x = radius * sinTheta;
    v1.y = -height + height / 2;
    v1.z = radius * cosTheta;

    vertices.push(v0.x, v0.y, v0.z);
    vertices.push(v1.x, v1.y, v1.z);

    directions.push(v1.x - v0.x, v1.y - v0.y, v1.z - v0.z);
    directions.push(v1.x - v0.x, v1.y - v0.y, v1.z - v0.z);

    var sinTheta = Math.sin(c0);
    var cosTheta = Math.cos(c0);

    v0.x = radius * sinTheta;
    v0.y = height / 2;
    v0.z = radius * cosTheta;

    control0.push(v0.x, v0.y, v0.z);
    control0.push(v0.x, v0.y, v0.z);

    var sinTheta = Math.sin(c1);
    var cosTheta = Math.cos(c1);

    v0.x = radius * sinTheta;
    v0.y = height / 2;
    v0.z = radius * cosTheta;

    control1.push(v0.x, v0.y, v0.z);
    control1.push(v0.x, v0.y, v0.z);

  }


  geometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3));
  geometry.setAttribute('control0', new THREE.Float32BufferAttribute(control0, 3, false));
  geometry.setAttribute('control1', new THREE.Float32BufferAttribute(control1, 3, false));
  geometry.setAttribute('direction', new THREE.Float32BufferAttribute(directions, 3, false));

  return new THREE.LineSegments(geometry, edgesMaterial);

}

function onResize() {

  var w = window.innerWidth;
  var h = window.innerHeight;

  camera.aspect = w / h;
  camera.updateProjectionMatrix();

  renderer.setSize(w, h);

}

animate();
body {
  margin: 0;
  position: fixed;
}

canvas {
  width: 100%;
  height: 100%;
  display: block;
}
<script src="https://cdn.jsdelivr.net/npm/[email protected]/build/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/controls/OrbitControls.min.js"></script>
like image 181
ScieCode Avatar answered Oct 30 '25 01:10

ScieCode