I'm trying to create an outline effect on some 3D meshes using the following method :
- Draw the original initial shaded mesh
- smooth the mesh normals
- push the vertives outward following their normals
- draw the back faces only with a solid outline color
I got it all working except the smoothing part. Smoothing the mesh is required to create a nice continuous outside outline (else there would be irregularities at the angles). I've allready asked how to smooth a mesh normals. The smoothing process goes like so :
let geometry = new THREE.BoxGeometry();
geometry.deleteAttribute('normal');
geometry.deleteAttribute('uv');
geometry = THREE.BufferGeometryUtils.mergeVertices(geometry);
geometry.computeVertexNormals();
Although this works well for simple well defined geometries, it creates artifact on more complexe models. I believe this is due to discarding the original normals. computeVertexNormals()
then interprets some faces inside-out.
Any idea how to fix that ?
before smoothing normals
after smoothing normals
sidenotes: 1/ I can't modify the original meshes topology because they are generated at runtime using jscad.
2/ I tried to write an algo to fix that but without great success. My reasoning went as follow:
Let's assume computeVertexNormals()
interprets a triangle being face-up or face-down based on the order of the vertices composing it. I'm going to find the triangles with vertices which normals get inverted and exchange two of the triangle vertices to flip it. A vertex normal is inverted if the dot product of the normal after computeVertexNormals()
and before computeVertexNormals()
is negative.
Here is the code for that:
const fixNormals: (bufferGeometry: THREE.BufferGeometry) => THREE.BufferGeometry
= (bufferGeometry) => {
// this function is only made for non indexed buffer geometry
if (bufferGeometry.index)
return bufferGeometry;
const positionAttribute = bufferGeometry.getAttribute('position');
if (positionAttribute == undefined)
return bufferGeometry;
let oldNormalAttribute = bufferGeometry.getAttribute('normal').clone();
if (oldNormalAttribute === undefined)
return bufferGeometry;
bufferGeometry.deleteAttribute('normal');
bufferGeometry.deleteAttribute('uv');
bufferGeometry.computeVertexNormals();
let normalAttribute = bufferGeometry.getAttribute('normal');
if (normalAttribute === undefined) {
console.error("bufferGeometry.computeVertexNormals() resulted in empty normals")
return bufferGeometry;
}
const pA = new THREE.Vector3(),
pB = new THREE.Vector3(),
pC = new THREE.Vector3();
const onA = new THREE.Vector3(),
onB = new THREE.Vector3(),
onC = new THREE.Vector3();
const nA = new THREE.Vector3(),
nB = new THREE.Vector3(),
nC = new THREE.Vector3();
for (let i = 0, il = positionAttribute.count; i < il; i += 3) {
pA.fromBufferAttribute(positionAttribute, i + 0);
pB.fromBufferAttribute(positionAttribute, i + 1);
pC.fromBufferAttribute(positionAttribute, i + 2);
onA.fromBufferAttribute(oldNormalAttribute, i + 0);
onB.fromBufferAttribute(oldNormalAttribute, i + 1);
onC.fromBufferAttribute(oldNormalAttribute, i + 2);
nA.fromBufferAttribute(normalAttribute, i + 0);
nB.fromBufferAttribute(normalAttribute, i + 1);
nC.fromBufferAttribute(normalAttribute, i + 2);
// new normals for this triangle are inverted,
// need to switch 2 vertices order to keep right face up
if (onA.dot(nA) < 0 && onB.dot(nB) < 0 && onC.dot(nC) < 0) {
positionAttribute.setXYZ(i + 0, pB.x, pB.y, pB.z)
positionAttribute.setXYZ(i + 1, pA.x, pA.y, pA.z)
}
}
bufferGeometry.deleteAttribute('normal');
bufferGeometry.deleteAttribute('uv');
bufferGeometry.computeVertexNormals();
return bufferGeometry;
}