ThreeJS - Using Raycaster to select faces on model with OBJ Loader

I have been trying to get use raycaster to be able to select faces on the OBJ Model and make them transparent when the cursor is over them, but I am unable to get it working.

Heres the code:

<!DOCTYPE html>

<html lang="en">

<head>

    <meta charset="UTF-8">

    <meta http-equiv="X-UA-Compatible" content="IE=edge">

    <meta name="viewport" content="width=device-width, initial-scale=1.0">

    <title>Document</title>

    <link rel="stylesheet" href="css/main.css">

</head>

<body>

    <!-- <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/102/three.js"></script> -->

    <script src="Three.js"></script>

    <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/2.1.2/TweenMax.min.js"></script>

    <script type = "module">

    import * as THREE from './three.module.js';

    import { MTLLoader } from './MTLLoader.js';

    import { OBJLoader } from './OBJLoader.js';

    import { OrbitControls } from './OrbitControls.js';

    let camera, scene, renderer;

    init();

    animate();

    var controls, mouse, raycaster;

        

function init() {

    

    const container = document.createElement( 'div' );

    document.body.appendChild( container );

    camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 0.1, 20 );

    camera.position.set( 2, 0, 2 );

    // scene

    scene = new THREE.Scene();

    camera.lookAt( scene.position );

    const ambientLight = new THREE.AmbientLight( 0xcccccc, 0.4 );

    scene.add( ambientLight );

    const pointLight = new THREE.PointLight( 0xffffff, 0.8 );

    camera.add( pointLight );

    scene.add( camera );

    // model

    const onProgress = function ( xhr ) {

        if ( xhr.lengthComputable ) {

            const percentComplete = xhr.loaded / xhr.total * 100;

            console.log( Math.round( percentComplete, 2 ) + '% downloaded' );

        }

    };

    const onError = function () { };

    new MTLLoader()

        .setPath( 'models/obj/' )

        .load( 'Solid1v2.mtl', function ( materials ) {

            

            materials.preload();

            new OBJLoader()

                .setMaterials( materials )

                .setPath( 'models/obj/' )

                .load( 'Solid1v2.obj', function ( object ) {

                    object.name = "Solid1";

                    scene.add( object );

                    console.log(object.name);

                }, onProgress, onError );

        } );

    //

    renderer = new THREE.WebGLRenderer();

    renderer.setPixelRatio( window.devicePixelRatio );

    renderer.setSize( window.innerWidth, window.innerHeight );

    container.appendChild( renderer.domElement );

    //

    mouse = new THREE.Vector2();

    raycaster = new THREE.Raycaster();

    function onMouseMove( event ) {

        // calculate mouse position in normalized device coordinates

        // (-1 to +1) for both components

        mouse.x = ( event.clientX / window.innerWidth ) * 2 - 1; //need to make the window.innerWidth same as the size of renderer.

        mouse.y = - ( event.clientY / window.innerHeight ) * 2 + 1; //need to make the window.innerHeight same as the size of the renderer.

        /* console.log(mouse); */

        /* console.log(scene.children) */

    }

    

    //

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

    controls.update();

    window.addEventListener( 'resize', onWindowResize );

    window.addEventListener( 'mousemove', onMouseMove, false );

}

    

function onWindowResize() {

    camera.aspect = window.innerWidth / window.innerHeight;

    camera.updateProjectionMatrix();

    renderer.setSize( window.innerWidth, window.innerHeight );

}

function hoverPieces() {

    raycaster.setFromCamera(mouse, camera);

    console.log(scene.getObjectByName("Solid1"))

    const intersects = raycaster.intersectObjects(scene.getObjectByName("Solid1"),true); 

    if (intersects == true){

        console.log("hello")

    }

    for (let i = 0; i<intersects.length; i++) {

        intersects[i].object.material.transparent = true;

        intersects[i].object.material.opacity = 0.5;

        

    }

}

function animate() {

    requestAnimationFrame( animate );

    render();

}

function render() {

    hoverPieces();

    /*  console.log(scene.getObjectByName("B1D239E1-056E-4455-B185-124CE29A8968",true)) */

    /* console.log(scene.getObjectByName("Solid1")); */

    renderer.render( scene, camera );

}

    

    </script>

</body>

</html>

Here is the model I am using:

Solid1v2.obj (15.5 KB)
Solid1v2.mtl (619 Bytes)

This will make the material of the entire object transparent.

Notice that the faces of your geometry do no have individual materials. So you can’t configure material properties.

You should consider to use vertex colors for this use case. Meaning you create an additional four-component color buffer attribute, apply (1,1,1,1) as default color for all vertices and then set the vertexColors property of the material to true. When you click on the model, you can extract the face index from the intersection object and then modify the color buffer data (or more precise the alpha component of the color).

1 Like

Have you considered using materialIndex?

I do not use BufferGeometry, if you are OK with regular geometry ( pre 125 ), here is woking demo.

Hold down the Ctrl key and click on the mesh.
Z_Trans.html (90.2 KB)

1 Like

The problem with using multi-materials is that the OP would require a material per face. That would potentially kill performance since each face would produce a draw call.

I am still new to ThreeJS so I am not really sure how to implement this, is there an example or tutorial I could follow?

Just wanted to make sure that one call per face applies only to BufferGeometry, not to regular geometry.

Definitely there is a bug on how number of draw calls are calculated, when comes to regular geometry and material index.

Thanks a lot!.

Being new to three.js, how to write the actual code of the steps you described took a bit of time going back and forth with documentation, but in general the process you described was very useful.

  1. Add vertexColors property to the material.
          var material = new THREE.MeshBasicMaterial({
            map: texture,
            vertexColors: true,
          });
  1. Add the ‘color’ attribute to the BufferGeometry.
            // Get the length of attributes.
            const count = geometry.attributes.position.count;

            // Add color attribute
            geometry.setAttribute(
              "color",
              new THREE.BufferAttribute(new Float32Array(count * 3), 3)
            );
  1. Initialize the colors of all the attributes (usually would be white, but I randomnized it so it could be easier to visualize the faces/vertices on my mesh)
            // Initialize the colors to white
            const colors = geometry.attributes.color;
            for (let i = 0; i < count; i++) {
              colors.setXYZ(i, Math.random(), Math.random(), Math.random());
            }
  1. When raycasting, get the intersection faces, set the color to black.
  intersections.forEach((intersection) => {
    const colorAttribute = bufferedGeometry.getAttribute("color");
    colorAttribute.setXYZ(intersection.face.a, 0, 0, 0);
    colorAttribute.setXYZ(intersection.face.b, 0, 0, 0);
    colorAttribute.setXYZ(intersection.face.c, 0, 0, 0);
    colorAttribute.needsUpdate = true;
  });
1 Like