Flipping Face Normal ThreeJS but not on floor

I try to build a house generator based on a floorplan. Generating the mesh works fine, but now I need to flip the normals on some faces.

buildRoomMeshFromPoints(planeScalar, heightScalar){
    var pointsAsVector2 = []
    this.points.map(e => {
        pointsAsVector2.push(new THREE.Vector2(e.x * planeScalar, e.y * planeScalar))
    })
    var shape = new THREE.Shape();

    shape.moveTo(pointsAsVector2[0].x, pointsAsVector2[0].y)
    pointsAsVector2.shift()
    pointsAsVector2.forEach(e => shape.lineTo(e.x, e.y))
    
    const extrusionSettings = {
        steps: 2,
        depth: heightScalar,
        bevelEnabled: false
    };
    
    var roomGeometry = new THREE.ExtrudeGeometry( shape, extrusionSettings );
    var materialFront = new THREE.MeshBasicMaterial( { color: 0xffff00 } );
    var materialSide = new THREE.MeshBasicMaterial( { color: 0xff8800 } );
    var materialArray = [ materialFront, materialSide ];
    var roomMaterial = new THREE.MeshFaceMaterial(materialArray);

    var room = new THREE.Mesh(roomGeometry, roomMaterial);
    room.position.set(0,0,0);
    room.rotation.set(THREE.MathUtils.degToRad(-90),0,0)
    return room;
}

This is the code that generates the house based on a collection of 2D points. To make the walls see through, I wanna change the normals of all walls and the roof.

My approach would be to compare each face normals angle to an up vector (THREE.Vector3(0,1,0)) and if the angle is greater then 0.0xx then flip it. I simply have no idea how to flip them :)

Thanks for any help!

Greetings pascal


Solution 1:

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.