How can we achieve a sleek outline effect by smoothing mesh normals in Three.js / jsCAD?

I've been experimenting with a method to create an outline effect on 3D meshes, and here is my approach:

  • Start by drawing the original shaded mesh
  • Smooth out the mesh normals
  • Move the vertices outward according to their normals
  • Render only the back faces with a solid outline color

Everything seems to be in working order except for the smoothing part. Smoothing the mesh is crucial for achieving a seamless outer outline (otherwise, there might be discrepancies at the angles). I've previously inquired about smoothing mesh normals. The process of smoothing goes like this:

let geometry = new THREE.BoxGeometry();
geometry.deleteAttribute('normal');
geometry.deleteAttribute('uv');
geometry = THREE.BufferGeometryUtils.mergeVertices(geometry);
geometry.computeVertexNormals();

While this method works well for simple, well-defined geometries, it tends to produce artifacts on more complex models. I suspect this is due to discarding the original normals. computeVertexNormals() ends up interpreting some faces incorrectly.

Any suggestions on how to resolve this issue?

Before smoothing normals:

https://i.sstatic.net/JHgZj.png

After smoothing normals:

https://i.sstatic.net/7fxbV.png


Sidenotes: 1/ Modifying the original mesh topology isn't feasible as they are dynamically generated using jscad.

2/ I attempted to devise an algorithm to address this but with limited success. My thought process was as follows:

Assuming computeVertexNormals() determines whether a triangle is face-up or face-down based on the vertex order, I planned to identify triangles with inverted normal vertices and swap two vertices to correct it. A vertex normal is considered inverted if the dot product of the normal after computeVertexNormals() differs from before.

Below is the code snippet for that:

const fixNormals: (bufferGeometry: THREE.BufferGeometry) => THREE.BufferGeometry
= (bufferGeometry) => {
// This function is designed specifically for non-indexed buffer geometry
if (bufferGeometry.index)
return bufferGeometry;
const positionAttribute = bufferGeometry.getAttribute('position');
if (!positionAttribute)
return bufferGeometry;
let oldNormalAttribute = bufferGeometry.getAttribute('normal').clone();
if (!oldNormalAttribute)
return bufferGeometry;

bufferGeometry.deleteAttribute('normal');
bufferGeometry.deleteAttribute('uv');
bufferGeometry.computeVertexNormals();

let normalAttribute = bufferGeometry.getAttribute('normal');
if (!normalAttribute) {
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);

// Determine new normals for this triangle -- invert them,
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;
}

Answer №1

THREE utilizes the winding order of a face to determine its normal when performing computeVertexNormals. If a face is defined with left-hand winding, the normals may point in the wrong direction, potentially affecting lighting unless a shaded material is not being used.

In some cases, computed normals may differ between faces. To prevent this issue, it is necessary to keep track of all normals for a specific vertex position, as duplicate positions within the buffer can occur.

Step 1: Establish a Vertex Map

The goal here is to create a map linking actual vertex positions to defined attributes for future use. While indices and groups may complicate matters, ultimately, a Map should be created, with the serialized vertex values serving as keys. For instance, vertex values like x = 1.23, y = 1.45, z = 1.67 could be transformed into the key 1.23|1.45|1.67. This serialization ensures proper referencing of vertex positions, as different Vector3s would lead to distinct keys. Anyway...

The data to store includes the normal for each vertex and the resulting smoothed normal. Therefore, an object should be pushed to each key:

let vertMap = new Map()

// As you iterate through vertices...
if (vertMap.has(serializedVertex)) {
  vertMap.get(serializedVert).array.push(vector3Normal);
} else {
  vertMap.set(serializedVertex, {
    normals: [vector3Normal],
    final: new Vector3()
  });
}

Step 2: Smooth Out Normals

With all normals now stored per vertex position, loop through vertMap, averaging the normals array and assigning the mean value to the final property for each entry.

vertMap.forEach((entry, key, map) => {

  let unifiedNormal = entry.normals.reduce((acc, normal) => {
    acc.add(normal);
    return acc;
  }, new Vector3());

  unifiedNormal.x /= entry.normals.length;
  unifiedNormal.y /= entry.normals.length;
  unifiedNormal.z /= entry.normals.length;
  unifiedNormal.normalize();

  entry.final.copy(unifiedNormal);

});

Step 3: Update the Normals

Revisit your vertices (the position attribute). Since vertices and normals are paired, the current vertex's index can be used to access its corresponding normal index. Serialize the current vertex again to retrieve its associated normal from vertMap and assign the final values to the matching normals.

let pos = geometry.attributes.position.array;
let nor = geometry.attributes.normal.array;

for (let i = 0, l = pos.length; i < l; i += 3) {
  //serializedVertex = how you handled it during vertex mapping
  let newNor = vertMap.get(serializedVertex).final;
  nor[i] = newNor.x;
  nor[i + 1] = newNor.y;
  nor[i + 2] = newNor.z;
}
geometry.attributes.normal.needsUpdate = true;

Note

Although there may be more elegant or API-driven solutions for the tasks outlined above, the provided code wasn't optimized or exhaustive. Its purpose was simply to offer guidance on addressing the issue at hand.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Instructions for including packages in .vue files

Within the script section of my .vue file, I have the following code: <script> import get from 'lodash.get'; ... </script> Despite trying to import lodash.get, I keep encountering an error message stating ReferenceError: ge ...

Leveraging the power of Framer Motion in combination with Typescript

While utilizing Framer Motion with TypeScript, I found myself pondering if there is a method to ensure that variants are typesafe for improved autocomplete and reduced mistakes. Additionally, I was exploring the custom prop for handling custom data and des ...

Scope ng-repeat with Angular's $compile function is not functioning as expected

I am facing an issue with my directive that uses the $compile service to create a template. Despite having the users array defined in my scope, it does not populate the select options using either ng-options or ng-repeat. I am puzzled as to why this is h ...

Unable to retrieve information from req.body, however able to retrieve data from req.params

I'm facing an issue with my code where I am not able to retrieve user data from req.body as it returns undefined. However, I can successfully access the required information from req.params. Can anyone provide guidance on how to resolve this issue? co ...

Achieving functionality with Twitter Bootstrap's "Tabs" Javascript in a Ruby on Rails application

Encountering a JavaScript issue within a Rails application. Although Twitter's documentation is very clear, I'm still struggling to make dynamic tabs work (tab switching and dropdown). I even went ahead and copied their source code, downloaded t ...

How to extract the value of inner HTML tags when submitting a form in a React component using HTML

Hello, I am currently working on retrieving the value of the nested HTML tag 'option' upon submitting the form. Essentially, I need to access the value of the option tag in the handleSubmit function when the form is submitted. Please note that th ...

"Positioning a div around a Bootstrap form-inline: A step-by-step guide

When using Bootstrap 3 "form-inline" within a <div>, I noticed that the div seems to be nested within the form-inline. This is how my code looks: HTML <div class="wrapper"> <div class="form-inline"> <div ...

Separating Angular controllers into their own files can help prevent errors like "undefined is not a

I am currently revamping some Angular code and my goal is to organize things by type, such as keeping controllers in a separate folder. Currently, I have two controllers and an app.js file. However, I am encountering an issue with the error message "undef ...

What is the best way to ensure all components in React have synchronized authentication status?

I've been grappling with this dilemma for quite some time now. I was diligently working on an App using React, and in my homepage layout, I have a navigation bar along with other components. My goal is to achieve synchronous changes in both the naviga ...

In Vue using Typescript, how would I go about defining a local data property that utilizes a prop as its initial value?

When passing a prop to a child component in Vue, the documentation states: The parent component updates will refresh all props in the child component with the latest value. Avoid mutating a prop inside a child component as Vue will warn you in the consol ...

Issue with applying value changes in Timeout on Angular Material components

I'm currently experimenting with Angular, and I seem to be struggling with displaying a fake progress bar using the "angular/material/progress-bar" component. (https://material.angular.io/components/progress-bar/) In my "app.component.html", I have m ...

How wide can we make the Outerbounds in the Chrome app?

My setup includes a chrome box that can accommodate two monitors. I am interested in creating a single chrome app with dimensions of 3840x1080 (width =3840, height =1080) to cover both screens. I attempted this but was unsuccessful. Are there any alterna ...

Avoiding the repetition of CSS animations during Gatsby page hydration

I am facing an issue in Gatsby where I have an element with an initial CSS animation. It works perfectly when the static site loads, but after hydration, it keeps repeating. Is there a way to prevent this from happening? Below is my styled components code ...

jQuery enables the updating of field values while maintaining the cursor's position within the text

Currently, I am in the process of developing a jQuery plugin for a custom application of mine. One of the functionalities that I am working on involves slugifying a specific field. Below is the code snippet that I have implemented for this purpose: $site ...

Having trouble importing a modified package forked for use in a Next.JS project

I've implemented react-headroom in my current project and had to modify its code so that the <header> wouldn't change height on different pages. To achieve this, I forked the original repository from here, made the necessary changes on my v ...

Tips on automatically focusing on an input within a Semantic UI React dropdown

I'm attempting to automatically focus on an input located within a Semantic UI React dropdown by using a ref, but unfortunately, it's not functioning as expected. The input should be focused when the dropdown is opened. Check out the code sandbox ...

Days and Their Corresponding Names Listed in Table

Currently, I am working on creating a "timesheet" feature that requires the user to input 2 dates. The goal is to calculate each month and year based on the input dates and then display them in a dropdown menu combobox. I would like to dynamically update ...

Determining the Existence of a Model in Backbone/Marionette

I've built a simple backbone application, but I'm struggling with a more complex check that needs to be performed. Below is my code. I'm creating a list of chat participants. Eventually, I'll pass this list into a JavaScript function. ...

Using React to implement a two-way binding for text input, where the format differs between the stored value and the

I am seeking a solution for synchronizing a list of text inputs with values stored in the backend. Essentially, I want changes made to the text inputs to update the corresponding values and vice versa. This scenario calls for a typical 2-way binding funct ...

Is there a way to solve this problem by creating a loop for the code?

document.getElementById("output1").innerHTML = totalDistance + ", " + hours + ":" + minutes+", $" + cost; this code combines input values to create an output. I am currently facing an issue where I need to loop thi ...