'Three JS Wireframe with rounded edges
I'm working on a project with 3D objects and decided to try using Three.js. I need a wireframe with rounded corners on a single axis. I've been able to achieve this effect mostly with two different methods, but not entirely. I'm not able to get a single outline connecting the two faces on just the outside wall.
I am drawing a shape with a new THREE.Shape() and then extrude it to geometry with a new THREE.ExtrudeGeometry(), getting the edges I care about with a new THREE.EdgesGeometry() and then finally making a mesh with a new THREE.LineSegments().
I'm also rendering a version of the box within it so the lines in the back are not shown.
The first way I tried was using the depth attribute on ExtrudeGeometry. It draws the lines, but it draws way too many lines, and it still draws them on the faces showing. I tried experimenting with the second parameter in EdgesGeometry which allows you to specify the minimum angle, but it either renders all or none of the lines.
The second thing I tried was making two separate shapes, extruding them with a depth of 0 and then repositing the two meshes slightly to give them depth.
This works much closer to what I wanted, but I'm still not getting the connection at the edges as I want.
Is this reasonably possible?
Here is a snippet demonstrating method 1
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 );
const renderer = new THREE.WebGLRenderer( { alpha: true });
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );
//Shape code starts here
const w = 1.5;
const h = 1.5;
const r = 0.2;
const d = 0.4;
//Generate the rounded rect shape
const s = new THREE.Shape();
s.moveTo(-w / 2, -h / 2 + r);
s.lineTo(-w / 2, h / 2 - r);
s.absarc(-w / 2 + r, h / 2 - r, r, 1 * Math.PI, 0.5 * Math.PI, true);
s.lineTo(w / 2 - r, h / 2);
s.absarc(w / 2 - r, h / 2 - r, r, 0.5 * Math.PI, 0 * Math.PI, true);
s.lineTo(w / 2, -h / 2 + r);
s.absarc(w / 2 - r, -h / 2 + r, r, 2 * Math.PI, 1.5 * Math.PI, true);
s.lineTo(-w / 2 + r, -h / 2);
s.absarc(-w / 2 + r, -h / 2 + r, r, 1.5 * Math.PI, 1 * Math.PI, true);
const outlineMat = new THREE.LineBasicMaterial( {
color: 0x000000,
linewidth: 2,
polygonOffset: true,
polygonOffsetFactor: 1,
polygonOffsetUnits: 1
} );
//This is the material that is used to stop the
//lines in the back from showing
var innerMat = new THREE.MeshBasicMaterial( {
color: false,
side: THREE.DoubleSide,
depthTest: true,
polygonOffset: true,
polygonOffsetFactor: 1,
polygonOffsetUnits: 1
} );
//Using the shape, generate the meshes
const outlineMesh = new THREE.LineSegments(
new THREE.EdgesGeometry(
new THREE.ExtrudeGeometry(
s,
{
depth: d,
bevelEnabled: false
}
),
1 //This is the default min angle
),
outlineMat
);
const innerMesh = new THREE.Mesh(
new THREE.ExtrudeGeometry(
s,
{
depth: d,
bevelEnabled: false
}
),
innerMat
);
const group = new THREE.Group();
group.add(outlineMesh);
group.add(innerMesh);
scene.add(group);
//Shape code ends here
camera.position.z = 5;
function animate() {
requestAnimationFrame( animate );
group.rotateX(0.01);
group.rotateY(0.01);
renderer.render( scene, camera );
}
animate();
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
and here is one for method 2
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 );
const renderer = new THREE.WebGLRenderer( { alpha: true });
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );
//Shape code starts here
const w = 1.5;
const h = 1.5;
const r = 0.2;
const d = 0.4;
//Generate the rounded rect shape
const s = new THREE.Shape();
s.moveTo(-w / 2, -h / 2 + r);
s.lineTo(-w / 2, h / 2 - r);
s.absarc(-w / 2 + r, h / 2 - r, r, 1 * Math.PI, 0.5 * Math.PI, true);
s.lineTo(w / 2 - r, h / 2);
s.absarc(w / 2 - r, h / 2 - r, r, 0.5 * Math.PI, 0 * Math.PI, true);
s.lineTo(w / 2, -h / 2 + r);
s.absarc(w / 2 - r, -h / 2 + r, r, 2 * Math.PI, 1.5 * Math.PI, true);
s.lineTo(-w / 2 + r, -h / 2);
s.absarc(-w / 2 + r, -h / 2 + r, r, 1.5 * Math.PI, 1 * Math.PI, true);
const outlineMat = new THREE.LineBasicMaterial( {
color: 0x000000,
linewidth: 2,
polygonOffset: true,
polygonOffsetFactor: 1,
polygonOffsetUnits: 1
} );
//This is the material that is used to stop the
//lines in the back from showing
var innerMat = new THREE.MeshBasicMaterial( {
color: false,
side: THREE.DoubleSide,
depthTest: true,
polygonOffset: true,
polygonOffsetFactor: 1,
polygonOffsetUnits: 1
} );
//Using the shape, generate the meshes
const frontMesh = new THREE.LineSegments(
new THREE.EdgesGeometry(
new THREE.ExtrudeGeometry(
s,
{
depth: 0,
bevelEnabled: false
}
),
1 //This is the default min angle
),
outlineMat
);
const backMesh = new THREE.LineSegments(
new THREE.EdgesGeometry(
new THREE.ExtrudeGeometry(
s,
{
depth: 0,
bevelEnabled: false
}
),
1 //This is the default min angle
),
outlineMat
);
const innerMesh = new THREE.Mesh(
new THREE.ExtrudeGeometry(
s,
{
depth: d,
bevelEnabled: false
}
),
innerMat
);
//Position the meshes
frontMesh.translateZ(d / 2);
backMesh.translateZ(-d / 2);
innerMesh.translateZ(-d / 2);
const group = new THREE.Group();
group.add(frontMesh);
group.add(backMesh);
group.add(innerMesh);
scene.add(group);
//Shape code ends here
camera.position.z = 5;
function animate() {
requestAnimationFrame( animate );
group.rotateX(0.01);
group.rotateY(0.01);
renderer.render( scene, camera );
}
animate();
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
Edit
I've tried another method suggested by prisoner849 using conditional line segments here https://discourse.threejs.org/t/ldraw-like-edges/17100. I was able to achieve the same results as method one. Either rendering all the lines or none of the lines on the rounded side, depending on the minimum angle. I feel like I am much closer, but I don't know what I'm missing.
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 );
const renderer = new THREE.WebGLRenderer( { alpha: true });
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );
//Shape code starts here
const w = 1.5;
const h = 1.5;
const r = 0.2;
const d = 0.4;
//Generate the rounded rect shape
const s = new THREE.Shape();
s.moveTo(-w / 2, -h / 2 + r);
s.lineTo(-w / 2, h / 2 - r);
s.absarc(-w / 2 + r, h / 2 - r, r, 1 * Math.PI, 0.5 * Math.PI, true);
s.lineTo(w / 2 - r, h / 2);
s.absarc(w / 2 - r, h / 2 - r, r, 0.5 * Math.PI, 0 * Math.PI, true);
s.lineTo(w / 2, -h / 2 + r);
s.absarc(w / 2 - r, -h / 2 + r, r, 2 * Math.PI, 1.5 * Math.PI, true);
s.lineTo(-w / 2 + r, -h / 2);
s.absarc(-w / 2 + r, -h / 2 + r, r, 1.5 * Math.PI, 1 * Math.PI, true);
//This is the material that is used to stop the
//lines in the back from showing
var innerMat = new THREE.MeshBasicMaterial( {
color: false,
side: THREE.DoubleSide,
depthTest: true,
polygonOffset: true,
polygonOffsetFactor: 1,
polygonOffsetUnits: 1
} );
//Using the shape, generate the meshes
const outlineMesh = createOutlinedMesh(
new THREE.ExtrudeGeometry(
s,
{
depth: d,
bevelEnabled: false
}
),
'black'
);
const innerMesh = new THREE.Mesh(
new THREE.ExtrudeGeometry(
s,
{
depth: d,
bevelEnabled: false
}
),
innerMat
);
const group = new THREE.Group();
group.add(outlineMesh);
group.add(innerMesh);
scene.add(group);
//Shape code ends here
camera.position.z = 5;
function animate() {
requestAnimationFrame( animate );
group.rotateX(0.01);
group.rotateY(0.01);
renderer.render( scene, camera );
}
animate();
//Added code
function createOutlinedMesh(geometry){
let eg = EdgesGeometry(geometry, 3);
let m = new THREE.ShaderMaterial({
vertexShader: conditionalLineVertShader,
fragmentShader: conditionalLineFragShader,
uniforms: {
diffuse: {
value: 0x000000
},
opacity: {
value: 0
}
},
transparent: false
});
let o = new THREE.LineSegments(eg, m);
// let b = new THREE.Mesh(geometry, new THREE.MeshBasicMaterial({
// color: 0x444444,
// map: new THREE.TextureLoader().load("https://threejs.org/examples/textures/uv_grid_opengl.jpg"),
// polygonOffset: true,
// polygonOffsetFactor: 1
// }));
// o.add(b);
return o;
}
var conditionalLineVertShader = /* glsl */`
attribute vec3 control0;
attribute vec3 control1;
attribute vec3 direction;
attribute float collapse;
attribute vec3 instPos;
#include <common>
#include <color_pars_vertex>
#include <fog_pars_vertex>
#include <logdepthbuf_pars_vertex>
#include <clipping_planes_pars_vertex>
void main() {
#include <color_vertex>
// Transform the line segment ends and control points into camera clip space
vec4 c0 = projectionMatrix * modelViewMatrix * vec4( control0 + instPos, 1.0 );
vec4 c1 = projectionMatrix * modelViewMatrix * vec4( control1 + instPos, 1.0 );
vec4 p0 = projectionMatrix * modelViewMatrix * vec4( position + instPos, 1.0 );
vec4 p1 = projectionMatrix * modelViewMatrix * vec4( position + instPos + 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 ) );
float discardFlag = float( sign( d0 ) != sign( d1 ) );
vec3 p = position + instPos + ((discardFlag > 0.5) ? direction * collapse : vec3(0));
vec4 mvPosition = modelViewMatrix * vec4( p, 1.0 );
gl_Position = projectionMatrix * mvPosition;
#include <logdepthbuf_vertex>
#include <clipping_planes_vertex>
#include <fog_vertex>
}
`;
var conditionalLineFragShader = /* glsl */`
uniform vec3 diffuse;
uniform float opacity;
#include <common>
#include <color_pars_fragment>
#include <fog_pars_fragment>
#include <logdepthbuf_pars_fragment>
#include <clipping_planes_pars_fragment>
void main() {
#include <clipping_planes_fragment>
vec3 outgoingLight = vec3( 0.0 );
vec4 diffuseColor = vec4( diffuse, opacity );
#include <logdepthbuf_fragment>
#include <color_fragment>
outgoingLight = diffuseColor.rgb; // simple shader
gl_FragColor = vec4( outgoingLight, diffuseColor.a );
#include <tonemapping_fragment>
#include <encodings_fragment>
#include <fog_fragment>
#include <premultiplied_alpha_fragment>
}
`;
function EdgesGeometry( geometry, thresholdAngle ) {
let g = new THREE.BufferGeometry();
g.type = 'EdgesGeometry';
g.parameters = {
thresholdAngle: thresholdAngle
};
thresholdAngle = ( thresholdAngle !== undefined ) ? thresholdAngle : 1;
// buffer
const vertices = [];
const control0 = [];
const control1 = [];
const direction = [];
const collapse = [];
// helper variables
const thresholdDot = Math.cos( THREE.MathUtils.DEG2RAD * thresholdAngle );
const edge = [ 0, 0 ], edges = {};
let edge1, edge2, key;
const keys = [ 'a', 'b', 'c' ];
// prepare source geometry
let geometry2;
if ( geometry.isBufferGeometry ) {
geometry2 = new THREE.Geometry();
geometry2.fromBufferGeometry( geometry );
} else {
geometry2 = geometry.clone();
}
geometry2.mergeVertices();
geometry2.computeFaceNormals();
const sourceVertices = geometry2.vertices;
const faces = geometry2.faces;
// now create a data structure where each entry represents an edge with its adjoining faces
for ( let i = 0, l = faces.length; i < l; i ++ ) {
const face = faces[ i ];
for ( let j = 0; j < 3; j ++ ) {
edge1 = face[ keys[ j ] ];
edge2 = face[ keys[ ( j + 1 ) % 3 ] ];
edge[ 0 ] = Math.min( edge1, edge2 );
edge[ 1 ] = Math.max( edge1, edge2 );
key = edge[ 0 ] + ',' + edge[ 1 ];
if ( edges[ key ] === undefined ) {
edges[ key ] = { index1: edge[ 0 ], index2: edge[ 1 ], face1: i, face2: undefined };
} else {
edges[ key ].face2 = i;
}
}
}
// generate vertices
const v3 = new THREE.Vector3();
const n = new THREE.Vector3();
const n1 = new THREE.Vector3();
const n2 = new THREE.Vector3();
const d = new THREE.Vector3();
for ( key in edges ) {
const e = edges[ key ];
// an edge is only rendered if the angle (in degrees) between the face normals of the adjoining faces exceeds this value. default = 1 degree.
if ( e.face2 === undefined || faces[ e.face1 ].normal.dot( faces[ e.face2 ].normal ) <= thresholdDot ) {
let vertex1 = sourceVertices[ e.index1 ];
let vertex2 = sourceVertices[ e.index2 ];
vertices.push( vertex1.x, vertex1.y, vertex1.z );
vertices.push( vertex2.x, vertex2.y, vertex2.z );
d.subVectors(vertex2, vertex1);
collapse.push(0, 1);
n.copy(d).normalize();
direction.push(d.x, d.y, d.z);
n1.copy(faces[ e.face1 ].normal);
n1.crossVectors(n, n1);
d.subVectors(vertex1, vertex2);
n.copy(d).normalize();
n2.copy(faces[ e.face2 ].normal);
n2.crossVectors(n, n2);
direction.push(d.x, d.y, d.z);
v3.copy(vertex1).add(n1); // control0
control0.push(v3.x, v3.y, v3.z);
v3.copy(vertex1).add(n2); // control1
control1.push(v3.x, v3.y, v3.z);
v3.copy(vertex2).add(n1); // control0
control0.push(v3.x, v3.y, v3.z);
v3.copy(vertex2).add(n2); // control1
control1.push(v3.x, v3.y, v3.z);
}
}
// build geometry
g.setAttribute( 'position', new THREE.Float32BufferAttribute( vertices, 3 ) );
g.setAttribute( 'control0', new THREE.Float32BufferAttribute( control0, 3 ) );
g.setAttribute( 'control1', new THREE.Float32BufferAttribute( control1, 3 ) );
g.setAttribute( 'direction', new THREE.Float32BufferAttribute( direction, 3 ) );
g.setAttribute( 'collapse', new THREE.Float32BufferAttribute( collapse, 1 ) );
return g;
}
<script src="https://rawgithub.com/mrdoob/three.js/r118/build/three.js"></script>
<script src="https://rawgithub.com/mrdoob/three.js/r118/examples/js/utils/BufferGeometryUtils.js"></script>
Solution 1:[1]
Using only mesh edges data to achieve what you want is hard, let me explain.
You are basically drawing mesh edges and you want to render only some of them (the corners I think). To do this you need a property on that edges that makes them different from other edges, so you isolate them. You have tried angle but the angles are so similar (angle can be used on much more complex meshes).
I can't think of other property that can help you on this, but you can define your own property like vertex color, but it becomes a manual process. If you can design your shapes in 3D modeling tools like Blender, you can easily set vertex color for these corner edges and use that colors in rendering to only render those vertexes (with a custom shader probably). If you are generating your mesh (like what you are doing now), in the process of generating you should somehow change the vertex color of those edges (I'm not sure how to do it in three.js). If this method even works, it's really hard to get it off, because you need to color all the edges and render different edges based on the camera angleā¦.
I have made an illustration to show why this doesn't work:
This is our object, you can see it has 9 edges in each corner.
To make it more visible, I have colored the edges of a corner. As you can see, when I rotate around the mesh, each edge becomes the object corner outline each time, SO you can't show the object outline with only 1 of these edges.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 |





