flipped normals after using scale x to -1 (mirror effect)

Hi guys, I have been looking for a solution for a very long time, but unfortunately I could not find one. I tried to find a solution here, stack overflow, even chatGPT and nothing could help me.

My problem is about flipping. When I use x-scaling to -1, the normals are flipped. Before that, I need to reduce the size of the object and flip it 180 degrees on the Y axis. Could someone please help? Thanks a lot.

Below my piece of code

image

import { Button, Checkbox } from "@nextui-org/react";
import { useEffect, useRef } from "react";
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
import { OBJLoader } from "three/examples/jsm/loaders/OBJLoader";
import { OBJExporter } from "three/examples/jsm/exporters/OBJExporter";
import * as BufferGeometryUtils from "three/examples/jsm/utils/BufferGeometryUtils";

interface SceneProps {
  objSrc: string | null;
  isRaf: boolean;
  setIsRaf: (value: boolean) => void;
}

const Scene = ({ objSrc, isRaf, setIsRaf }: SceneProps) => {
  const sceneRef = useRef(null);
  const renderer = new THREE.WebGLRenderer();
  const scene = new THREE.Scene();
  const camera = new THREE.PerspectiveCamera(45, 1, 0.1, 1000);

  useEffect(() => {
    const container: any = sceneRef.current!;
    scene.background = new THREE.Color("#D8D8D8");
    renderer.setSize(500, 500);
    container.appendChild(renderer.domElement);
    camera.position.z = -200;
    camera.position.y = 300;
    camera.position.x = 100;

    const controls = new OrbitControls(camera, renderer.domElement);
    controls.enableDamping = true;

    function animate() {
      requestAnimationFrame(animate);

      controls.update();

      renderer.render(scene, camera);
    }

    displayObject();

    animate();
    return () => {
      container.removeChild(renderer.domElement);
    };
  }, [objSrc, isRaf]);

  let newPositions: any = [];
  let newIndex: any = [];

  const displayObject = () => {
    const loader = new OBJLoader();
    const light = new THREE.DirectionalLight(0xffffff, 1);
    loader.load(
      objSrc!,
      function (object: THREE.Group<THREE.Object3DEventMap>) {
        const material = new THREE.MeshStandardMaterial({
          emissive: 0x3c4a53,
        });
        object.traverse((child) => {
          if (child instanceof THREE.Mesh) {
            child.geometry = BufferGeometryUtils.mergeVertices(child.geometry);
            const geometry = child.geometry as THREE.BufferGeometry;

            // Pobierz oryginalne wierzchołki i indeksy
            const positions = geometry.attributes.position.array;
            const indices = geometry.index ? geometry.index.array : null;

            // Mapuj indeksy na unikalne wierzchołki
            const vertexMap = new Map();

            if (indices) {
              for (let i = 0; i < indices.length; i += 3) {
                const index1 = indices[i];
                const index2 = indices[i + 1];
                const index3 = indices[i + 2];

                // Stworzenie klucza na podstawie współrzędnych wierzchołków
                const vertexKey1 = `${positions[index1 * 3]}_${
                  positions[index1 * 3 + 1]
                }_${positions[index1 * 3 + 2]}`;
                const vertexKey2 = `${positions[index2 * 3]}_${
                  positions[index2 * 3 + 1]
                }_${positions[index2 * 3 + 2]}`;
                const vertexKey3 = `${positions[index3 * 3]}_${
                  positions[index3 * 3 + 1]
                }_${positions[index3 * 3 + 2]}`;

                // Sprawdź, czy wierzchołek już istnieje w mapie
                if (!vertexMap.has(vertexKey1)) {
                  const newIndexValue = newPositions.length / 3;
                  vertexMap.set(vertexKey1, newIndexValue);
                  newPositions.push(
                    positions[index1 * 3],
                    positions[index1 * 3 + 1],
                    positions[index1 * 3 + 2]
                  );
                }

                if (!vertexMap.has(vertexKey2)) {
                  const newIndexValue = newPositions.length / 3;
                  vertexMap.set(vertexKey2, newIndexValue);
                  newPositions.push(
                    positions[index2 * 3],
                    positions[index2 * 3 + 1],
                    positions[index2 * 3 + 2]
                  );
                }

                if (!vertexMap.has(vertexKey3)) {
                  const newIndexValue = newPositions.length / 3;
                  vertexMap.set(vertexKey3, newIndexValue);
                  newPositions.push(
                    positions[index3 * 3],
                    positions[index3 * 3 + 1],
                    positions[index3 * 3 + 2]
                  );
                }

                // Dodaj nowe indeksy
                newIndex.push(
                  vertexMap.get(vertexKey1)!,
                  vertexMap.get(vertexKey2)!,
                  vertexMap.get(vertexKey3)!
                );
              }
            } else {
              // Jeśli nie ma indeksów, użyj prostego dodawania wierzchołków
              for (let i = 0; i < positions.length; i += 3) {
                newPositions.push(
                  positions[i],
                  positions[i + 1],
                  positions[i + 2]
                );
                newIndex.push(i / 3);
              }
            }

            child.material = material;
          }
        });
        object.rotateY(180 / 57.2958);

        if (isRaf) {
          object.scale.set(-1, 1, 1);
        }

        scene.add(object);
        scene.add(light);
      }
    );
  };

  const exportObj = () => {
    const simplifiedGeometry = new THREE.BufferGeometry();
    if (isRaf) {
      simplifiedGeometry.applyMatrix4(new THREE.Matrix4().makeScale(-1, 1, 1));
    }
    scene.traverse((child) => {
      if (child instanceof THREE.Mesh) {
        child.geometry.computeVertexNormals();

        // Aktualizuj geometriÄ™
        simplifiedGeometry.setAttribute(
          "position",
          new THREE.BufferAttribute(new Float32Array(newPositions), 3)
        );
        simplifiedGeometry.setIndex(
          new THREE.BufferAttribute(new Uint32Array(newIndex), 1)
        );

        child.geometry.dispose();
        child.geometry = simplifiedGeometry;
      }
    });
    const exporter = new OBJExporter();
    const data = exporter.parse(scene);
    const blob = new Blob([data], { type: "text/plain" });
    const link = document.createElement("a");
    link.href = URL.createObjectURL(blob);
    const lafRaf = isRaf ? "RAF" : "Default_or_LAF_";
    link.download = `${
      lafRaf + "_" + (Math.random() * 1000000).toFixed(0)
    }.obj`;
    link.click();
  };

  return (
    <>
      <Checkbox
        size="lg"
        className="font-semibold mx-auto block mb-5"
        onClick={() => setIsRaf(!isRaf)}
      >
        RAF
      </Checkbox>
      <div className="border border-black max-w-max mx-auto" ref={sceneRef} />
      <Button
        onClick={exportObj}
        className="mx-auto mt-10 bg-primary text-lg font-semibold text-zinc-50 flex"
      >
        Download
        <svg
          xmlns="http://www.w3.org/2000/svg"
          fill="none"
          viewBox="0 0 24 24"
          strokeWidth={1.5}
          stroke="currentColor"
          className="w-7 h-7 "
        >
          <path
            strokeLinecap="round"
            strokeLinejoin="round"
            d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3"
          />
        </svg>
      </Button>
    </>
  );
};

export default Scene;

Did you try just flipping the normals .negate() ? This is the same behavior in Blender, you have to run “flip normals outside” function when scale x -1

1 Like

Specifically… something like…

let a = mesh.geometry.attributes.normal.array;
a.forEach((e,i)=>a[i]*=-1);

@manthrax @nameo9283 Hi, thanks for replies,
unfortunately this didn’t help. This has the same result as before

im not sure if im using negate() correctly. If i use negate() on scale Vector:

scaleVector.negate()

Object displays upside down.

I don’t know if i did it correctly, but here’s how i implemented this:

if (isRaf) {
              const scaleVector = new THREE.Vector3(-1, 1, 1);
              mesh.scale.multiply(scaleVector);
              const normals = mesh.geometry.attributes.normal.array;
              normals.forEach((e, i) => (normals[i] *= -1));
            }
}

might need a mesh.geometry.attributes.normal.needsUpdate = true after that too…

No changes :(((

try something small first. Set up a half box and turn on normals and try flipping those outside of a loop

I can’t deal with this. Anymore any ideas :c ?

Maybe it is about time to provide online live and debuggable example of your case. When I try to mirror an object via a negative scale, I’m not able to reproduce your situation. I have picked half of a head and tried to mirror it like this:

headMirror = headHalf.clone();
headMirror.scale.set( -1, 1, 1 );

and the mirrored half-of-the-head looks fine, just like the original half-of-a-head:

Add-on:

As for the discussion about flipping, is it sure that a complete vector flipping is needed? Mirroring is flipping of one coordinate, not all three coordinates. See the next image. The left object has its normals in black. The right object is a horizontal flip. The blue vectors are horizontally flipped normals and they are correct. The red vectors are completely flipped normals, and they are mostly wrong.

3 Likes