In simplest terms, "flipping" or finding the negative of the normal (or any) vector is a matter of negating each of its components. So if your normal vector n
is a THREE.Vector3
instance, then its negative is n.multiplyScalar(-1)
, or if it's in an array of the form [ x, y, z ]
, then its negative is [ -1 * x, -1 * y, -1 * z ]
.
Flipping the normal vectors won't do all of what you're looking to accomplish, though. Normals in Three.js (and many other engines and renderers) are separate and distinct from the notion of the side of a triangle that's being rendered. So if you only flip the vectors, Three.js will continue to render the front side of the triangles, which form the exterior of the mesh; those faces will appear darker, though, because they're reflecting light in exactly the wrong direction.
For each triangle, you need to both (a) flip the normals of its vertices; and (b) either render the back side of that triangle or reverse the facing of the triangle.
To render the back side of the triangle, you can set the .side
property of your material to THREE.BackSide
. (I have not tested this, and it may have other implications; among other things, you may come across other parts of your codebase that have to be specifically written with an eye to the fact that you're rendering backfaces.)
A more robust solution would be to make the triangles themselves face the other way.
ExtrudeGeometry
is a factory for BufferGeometry
, and the vertex positions are stored in a flat array in the .attributes.position.array
property of the generated geometry. You can swap every 3rd-5th element in the array with every 6th-9th element to reverse the winding order of the triangle, which changes the side that Three.js considers to be the front. Thus, a triangle defined as (0, 0, 0), (1, 0, 1), (1, 1, 1) and represented in the array as [ 0, 0, 0, 1, 0, 1, 1, 1, 1 ]
becomes (0, 0, 0), (1, 1, 1), (1, 0, 1) and [ 0, 0, 0, 1, 1, 1, 1, 0, 1 ]
. (Put differently, ABC becomes ACB.)
To accomplish this in code requires something like the following.
/**
* @param { import("THREE").BufferGeometry } geom
* @return { undefined }
*/
flipSides = (geom) => {
const positions = geom.getAttribute("position");
const normals = geom.getAttribute("normal");
const newNormals = Array.from(normals.array);
for (let attrName of ["position", "normal", "uv"]) {
// for (let i = 0; i < positions.count; i += 3) {
// ExtrudeGeometry generates a non-indexed BufferGeometry. To flip
// the faces, we must reverse the winding order, i.e., for each triangle
// ABC, we must change it to ACB. We must do this for the position,
// normal, and uv buffers.
const attr = geom.getAttribute(attrName);
let newArr = Array.from(attr.array)
const sz = attr.itemSize;
for (let i = 0; i < attr.count; i++) {
const offset = sz * 3 * i;
// i is the index of the first of three vertices of a triangle.
// Sample the buffer for the second and third vertices, which
// we'll swap.
const tempB = newArr.slice(
offset + sz,
offset + 2 * sz
);
const tempC = newArr.slice(
offset + 2 * sz,
offset + 3 * sz
);
newArr.splice(offset + sz, sz, ...tempC);
newArr.splice(offset + 2 * sz, sz, ...tempB);
}
// If we're working on the normals buffer, we also need to reverse
// the normals. Since reversing a vector simply entails a
// scalar-vector multiplication by -1, and since the array is
// flat, we can do this with one map() operation.
if (attrName == "normal") {
newArr = newArr.map((e) => e * -1);
}
// Replace the position buffer with our new array
geom.setAttribute(
attrName,
new THREE.BufferAttribute(
Float32Array.from(newArr),
sz
));
attr.needsUpdate = true;
}
I've posted a demonstration of this approach on CodePen.