Reversible color change on raytracer, hover effect simulation

Hello guys,

As I am relatively new to Threejs my question might be a bit stupid, if so, please do tell (and point towards obvious answer :smiley: ).

What I am trying to achieve: I have a 3D buildingplan and i want to make the parts ā€œhoverableā€ by mouse. They should turn red and back to their original color afterwards.
(Ultimately I want to connect the parts with hoverable divs in my website, but for now I would be happy to take a first step)

For now the colorchange works with the geometry I put in the scene (sphere, cube) but with the mesh itself, it does not change the color back.

Where am I going wrong?

import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';

const c41aUnten = new URL('https://uploads-ssl.webflow.com/64ef0d16e7e0c9e4853f735d/663a84e1909b5bc1fabc7627_240122_Lageplan_Sigma-Technopark-Dresden_41a_unten.glb.txt');
const c41EG = new URL('https://uploads-ssl.webflow.com/64ef0d16e7e0c9e4853f735d/663a84e0761f0fddb2aa5fb0_240122_Lageplan_Sigma-Technopark-Dresden_41_EG.glb.txt');
const c39AUG = new URL('https://uploads-ssl.webflow.com/64ef0d16e7e0c9e4853f735d/663a84df345aae95b00e18c6_240122_Lageplan_Sigma-Technopark-Dresden_39A_UG.glb.txt');
const c412OG = new URL('https://uploads-ssl.webflow.com/64ef0d16e7e0c9e4853f735d/663a84dfb78a02c5fd907f37_240122_Lageplan_Sigma-Technopark-Dresden_41_2OG.glb.txt');
const c391OG = new URL('https://uploads-ssl.webflow.com/64ef0d16e7e0c9e4853f735d/663a84df23e35a1b0fdadec4_240122_Lageplan_Sigma-Technopark-Dresden_39_1OG.glb.txt');
const c395OGTurm = new URL('https://uploads-ssl.webflow.com/64ef0d16e7e0c9e4853f735d/663a84df6bc0077c66c701a8_240122_Lageplan_Sigma-Technopark-Dresden_39_5OG_Turm.glb.txt');
const c411OG = new URL('https://uploads-ssl.webflow.com/64ef0d16e7e0c9e4853f735d/663a84df712a3e2b7fd96052_240122_Lageplan_Sigma-Technopark-Dresden_41_1OG.glb.txt');
const c394OGTurm = new URL('https://uploads-ssl.webflow.com/64ef0d16e7e0c9e4853f735d/663a84df893a588c77b18b28_240122_Lageplan_Sigma-Technopark-Dresden_39_4OG_Turm.glb.txt');
const c39A2OG = new URL('https://uploads-ssl.webflow.com/64ef0d16e7e0c9e4853f735d/663a84dfa9defb609f4def6b_240122_Lageplan_Sigma-Technopark-Dresden_39A_2OG.glb.txt');
const c39EG = new URL('https://uploads-ssl.webflow.com/64ef0d16e7e0c9e4853f735d/663a84dfbd2ec3d434924d4b_240122_Lageplan_Sigma-Technopark-Dresden_39_EG.glb.txt');
const c39AEG = new URL('https://uploads-ssl.webflow.com/64ef0d16e7e0c9e4853f735d/663a84df5903f5c7ca20ee54_240122_Lageplan_Sigma-Technopark-Dresden_39A_EG.glb.txt');
const c41aVorne = new URL('https://uploads-ssl.webflow.com/64ef0d16e7e0c9e4853f735d/663a84dfcbc9e98287998256_240122_Lageplan_Sigma-Technopark-Dresden_41a_vorne.glb.txt');
const cParkplatz = new URL('https://uploads-ssl.webflow.com/64ef0d16e7e0c9e4853f735d/663a84dfdea68e744a9ac490_240122_Lageplan_Sigma-Technopark-Dresden_Parkplatz.glb.txt');
const c41aOben = new URL('https://uploads-ssl.webflow.com/64ef0d16e7e0c9e4853f735d/663a84df7541ac5601bf288e_240122_Lageplan_Sigma-Technopark-Dresden_41a_oben.glb.txt');
const c392OG = new URL('https://uploads-ssl.webflow.com/64ef0d16e7e0c9e4853f735d/663a84dff689d32d332eafaa_240122_Lageplan_Sigma-Technopark-Dresden_39_2OG.glb.txt');
const cDaecher = new URL('https://uploads-ssl.webflow.com/64ef0d16e7e0c9e4853f735d/663a84df5f3e7d109765f81a_240122_Lageplan_Sigma-Technopark-Dresden_Daecher.glb.txt');
const c39A1OG = new URL('https://uploads-ssl.webflow.com/64ef0d16e7e0c9e4853f735d/663a84dfa9defb609f4def56_240122_Lageplan_Sigma-Technopark-Dresden_39A_1OG.glb.txt');
const c39UG = new URL('https://uploads-ssl.webflow.com/64ef0d16e7e0c9e4853f735d/663a84df01080f1bc3860fe9_240122_Lageplan_Sigma-Technopark-Dresden_39_UG.glb.txt');
const c44SE = new URL('https://uploads-ssl.webflow.com/64ef0d16e7e0c9e4853f735d/663a84df01080f1bc3860fe4_240122_Lageplan_Sigma-Technopark-Dresden_44_SE.glb.txt');
const c393OGTurm = new URL('https://uploads-ssl.webflow.com/64ef0d16e7e0c9e4853f735d/663a84df7c712df11f778eaa_240122_Lageplan_Sigma-Technopark-Dresden_39_3OG_Turm.glb.txt');
const cStrassen = new URL('https://uploads-ssl.webflow.com/64ef0d16e7e0c9e4853f735d/662f899e52ec4a035f10cf88_240122_Lageplan_Sigma-Technopark-Dresden_Strassen.glb.txt');
const cBoden = new URL('https://uploads-ssl.webflow.com/64ef0d16e7e0c9e4853f735d/662f89984072b4f57c8b9833_240122_Lageplan_Sigma-Technopark-Dresden_Boden.glb.txt');

window.Webflow ||= [];
window.Webflow.push(() => {
  init3D();
});

function init3D() {

  // GET TRUE CANVAS SIZE THROUGH DIV ========
  // Select the div class "scene-3d" and store values

  var sigmaApp = document.getElementById('scene-3d');

  // select container
  const viewport = document.querySelector('[data-3d="c"]');
  console.log(viewport);

  // Initialize Three.js scene
  var scene = new THREE.Scene();
  var renderer = new THREE.WebGLRenderer({ antialias: true });
  renderer.setClearColor(0x000000, 0); //Scene Background
  renderer.setSize(sigmaApp.offsetWidth, sigmaApp.offsetHeight); //Container Size renderer
  renderer.shadowMap.enabled = true; //Enable Shadows
  viewport.appendChild(renderer.domElement);

  // ======== SCENE LIGHTING ========
  // Ambient Light
  scene.add(new THREE.AmbientLight(0xFFFFFF, 2));

  // Directional Light  
  const directionalLight = new THREE.DirectionalLight(0xFFFFFF);
  directionalLight.intensity = 2;
  directionalLight.position.set(70, 70, 45);
  directionalLight.castShadow = true;
  directionalLight.shadow.camera.top = 30;
  directionalLight.shadow.camera.right = 45;
  directionalLight.shadow.camera.left = -45;
  directionalLight.shadow.camera.bottom = -35;
  scene.add(directionalLight);

  // ======== CAMERA AND CONTROLS ========
  // Camera and Mousecontrols
  const camera = new THREE.PerspectiveCamera(35, sigmaApp.offsetWidth / sigmaApp.offsetHeight, 0.1, 1000);
  const controls = new OrbitControls(camera, renderer.domElement); //set Orbitcontrols
  camera.position.set(-47, 25, 37); //Set Cameraposition set(x,y,z)
  controls.target.set(0, -3, 0); //First Look Target on load
  controls.minAzimuthAngle = Math.PI / 1;
  controls.maxAzimuthAngle = Math.PI / 6;
  //controls.minPolarAngle = Math.PI / 3.2;
  //controls.maxPolarAngle = Math.PI / 2.4;
  controls.enableDamping = true;
  controls.dampingFactor = 0.07;
  controls.update();

  // Create a cube
  var geometry = new THREE.BoxGeometry();
  var material = new THREE.MeshBasicMaterial({ color: 0xC0D12C }); // Green color
  var cube = new THREE.Mesh(geometry, material);
  cube.name = 'cube'; // Set name for identification
  cube.position.set(1, 12, 3);
  scene.add(cube);

  // Create SPhere mesh for intersection testing
  var sphereGeometry = new THREE.SphereGeometry(1, 32, 32);
  var sphereMaterial = new THREE.MeshBasicMaterial({ color: 0xAF75E9 }); // Red color
  var sphere = new THREE.Mesh(sphereGeometry, sphereMaterial);
  sphere.position.set(2, 0, 0);
  sphere.name = 'sphere'; // Set name for identification
  sphere.position.set(0, 12, 0);
  scene.add(sphere);

  // ======================================

  const assetLoader = new GLTFLoader();
  assetLoader.load(c41aUnten.href, function (gltf) { const m41aUnten = gltf.scene; scene.add(m41aUnten); gltf.scene.traverse(function (node) { if (node.isMesh) { node.castShadow = true; } }) });
  assetLoader.load(c41EG.href, function (gltf) { const m41EG = gltf.scene; scene.add(m41EG); gltf.scene.traverse(function (node) { if (node.isMesh) { node.castShadow = true; } }) });
  assetLoader.load(c39AUG.href, function (gltf) { const m39AUG = gltf.scene; scene.add(m39AUG); gltf.scene.traverse(function (node) { if (node.isMesh) { node.castShadow = true; } }) });
  assetLoader.load(c412OG.href, function (gltf) { const m412OG = gltf.scene; scene.add(m412OG); gltf.scene.traverse(function (node) { if (node.isMesh) { node.castShadow = true; } }) });
  assetLoader.load(c391OG.href, function (gltf) { const m391OG = gltf.scene; scene.add(m391OG); gltf.scene.traverse(function (node) { if (node.isMesh) { node.castShadow = true; } }) });
  assetLoader.load(c395OGTurm.href, function (gltf) { const m395OGTurm = gltf.scene; scene.add(m395OGTurm); gltf.scene.traverse(function (node) { if (node.isMesh) { node.castShadow = true; } }) });
  assetLoader.load(c411OG.href, function (gltf) { const m411OG = gltf.scene; scene.add(m411OG); gltf.scene.traverse(function (node) { if (node.isMesh) { node.castShadow = true; } }) });
  assetLoader.load(c394OGTurm.href, function (gltf) { const m394OGTurm = gltf.scene; scene.add(m394OGTurm); gltf.scene.traverse(function (node) { if (node.isMesh) { node.castShadow = true; } }) });
  assetLoader.load(c39A2OG.href, function (gltf) { const m39A2OG = gltf.scene; scene.add(m39A2OG); gltf.scene.traverse(function (node) { if (node.isMesh) { node.castShadow = true; } }) });
  assetLoader.load(c39EG.href, function (gltf) { const m39EG = gltf.scene; scene.add(m39EG); gltf.scene.traverse(function (node) { if (node.isMesh) { node.castShadow = true; } }) });
  assetLoader.load(c39AEG.href, function (gltf) { const m39AEG = gltf.scene; scene.add(m39AEG); gltf.scene.traverse(function (node) { if (node.isMesh) { node.castShadow = true; } }) });
  assetLoader.load(c41aVorne.href, function (gltf) { const m41aVorne = gltf.scene; scene.add(m41aVorne); gltf.scene.traverse(function (node) { if (node.isMesh) { node.castShadow = true; } }) });
  //assetLoader.load(cParkplatz.href, function (gltf) { const mParkplatz = gltf.scene; scene.add(mParkplatz); gltf.scene.traverse(function (node) { if (node.isMesh) { node.receiveShadow = true; } }) });
  assetLoader.load(c41aOben.href, function (gltf) { const m41aOben = gltf.scene; scene.add(m41aOben); gltf.scene.traverse(function (node) { if (node.isMesh) { node.castShadow = true; } }) });
  assetLoader.load(c392OG.href, function (gltf) { const m392OG = gltf.scene; scene.add(m392OG); gltf.scene.traverse(function (node) { if (node.isMesh) { node.castShadow = true; } }) });
  assetLoader.load(cDaecher.href, function (gltf) { const mDaecher = gltf.scene; scene.add(mDaecher); gltf.scene.traverse(function (node) { if (node.isMesh) { node.castShadow = true; } }) });
  assetLoader.load(c39A1OG.href, function (gltf) { const m39A1OG = gltf.scene; scene.add(m39A1OG); gltf.scene.traverse(function (node) { if (node.isMesh) { node.castShadow = true; } }) });
  assetLoader.load(c39UG.href, function (gltf) { const m39UG = gltf.scene; scene.add(m39UG); gltf.scene.traverse(function (node) { if (node.isMesh) { node.castShadow = true; } }) });
  assetLoader.load(c44SE.href, function (gltf) { const m44SE = gltf.scene; scene.add(m44SE); gltf.scene.traverse(function (node) { if (node.isMesh) { node.castShadow = true; } }) });
  assetLoader.load(c393OGTurm.href, function (gltf) { const m393OGTurm = gltf.scene; scene.add(m393OGTurm); gltf.scene.traverse(function (node) { if (node.isMesh) { node.castShadow = true; } }) });
  assetLoader.load(cStrassen.href, function (gltf) { const mStrassen = gltf.scene; scene.add(mStrassen); gltf.scene.traverse(function (node) { if (node.isMesh) { node.receiveShadow = true; } }) });
  assetLoader.load(cBoden.href, function (gltf) { const mBoden = gltf.scene; scene.add(mBoden); gltf.scene.traverse(function (node) { if (node.isMesh) { node.receiveShadow = true; } }) });

  // ======================================

  // Set up raycaster
  var raycaster = new THREE.Raycaster();
  var mouse = new THREE.Vector2();

  // Store original material color for each mesh
  var originalColors = {};

  // Function to change mesh color to red
  function changeColor(mesh) {
    mesh.material.color.set(0xff0000); // Red color
  }

  // Function to revert mesh color to original
  function revertColor(mesh) {
    if (originalColors.hasOwnProperty(mesh.name)) {
      mesh.material.color.set(originalColors[mesh.name]);
      delete originalColors[mesh.name]; // Remove stored color after reverting
      console.log(originalColors.data)
    }
  }

  // Event listener for mouse move
  function onMouseMove(event) {
    // Calculate mouse position in normalized device coordinates
    const viewportRect = renderer.domElement.getBoundingClientRect();
    const x = event.clientX - viewportRect.left;
    const y = event.clientY - viewportRect.top;

    mouse.x = (x / sigmaApp.offsetWidth) * 2 - 1;
    mouse.y = (y / sigmaApp.offsetHeight) * -2 + 1;

    // Update the picking ray with the camera and mouse position
    raycaster.setFromCamera(mouse, camera);

    // Calculate objects intersecting the picking ray
    var intersects = raycaster.intersectObjects(scene.children);

    if (intersects.length > 0) {
      onIntersection(intersects[0].object);
    } else {
      // Revert color if not intersecting with any object
      Object.values(scene.children).forEach(mesh => {
        revertColor(mesh);
      });
    }
  }

  // Event listener for raycaster intersection
  function onIntersection(mesh) {
    if (mesh !== undefined) {
      // Check if original color is not stored
      if (!originalColors.hasOwnProperty(mesh.name)) {
        // Store original color
        originalColors[mesh.name] = mesh.material.color.clone();
        // Change color to red
        changeColor(mesh);
      }
    } else {
      // Revert color if intersection with any mesh stops
      Object.values(scene.children).forEach(mesh => {
        revertColor(mesh);
      });
    }
  }

  // Add event listener
  window.addEventListener('mousemove', onMouseMove, false);

  // Camera position
  camera.position.z = 5;

  // Render loop
  function animate() {
    requestAnimationFrame(animate);
    renderer.render(scene, camera);
  }

  animate();

  //window resize function
  window.addEventListener('resize', onWindowResize);

  function onWindowResize() {

    camera.aspect = sigmaApp.offsetWidth / sigmaApp.offsetHeight;
    camera.updateProjectionMatrix();

    renderer.setSize(sigmaApp.offsetWidth, sigmaApp.offsetHeight);

    animate();

  }

}

The website is setup on webflow. I load the scene in a div on the Page.

I really appreciate any help you guys can provide me!

How could I improve my post to make it more likely to get help? :hugs:

Some hints are shared here: Please Read This Before Posting a Question

For your specific question:

  • A minimal working & debuggable example is always welcome, because most bugs (besides the most obvious), can be found only by debugging. Without such example you put additional load on people who want to help, as they have to make a working example, which often requires setting up some custom assets. And sometimes the issue is some incompaibility between the code and the structure of the assets,
  • Using ā€œthanks in advanceā€ is a huge turn-off for me and maybe others. More details why it is so, are here: Never end your email with ā€œThanks in advanceā€ and Thanks in advance? No thank you
  • As for making hovered meshes reddish and reverting their color when the hover is over, a simpler approach is to keep the mesh colors as they are. Instead hovering only changes the intensity of a red emissive color. Here is a demo with a GLTF tractor with several submeshes (look at lines 65-66 and from line 94 to the end): https://codepen.io/boytchev/pen/Baqraaa
  • As for your approach of managing colors, without knowing the structure of your models it is hard to guess. The problem might be in the following code (your models might have nested meshes, so the topmost objects are not meshes, but groups; and groups have no materials, so revertColor does nothing):
function onMouseMove(event) {
   :
   // Revert color if not intersecting with any object
   Object.values(scene.children).forEach(mesh => {
        revertColor(mesh);
    });
}
1 Like

I already agreeded, but It is much more clear to me now after reading your links, thanks @PavelBoytchev for pointing this out :+1:

1 Like

Thank you Pavel for pointing me in that direction despite my plunder. I edited it.
Sorry for choosing my words that way.

to the topic:

Modifying the emission worked very well. Combined with metalness it even looks like solid color.
Now I just have to find a way to ease the transitions. Do you guys have any suggestions for me in that regard? (I read that tween.js would be a way, but implementing it does not sound easy)

you can see the application here: Vor-Ort

My code looks like this now:

/* setup of scene (camera, light etc.) */
  

  // ======== ALL GEOMETRY / MESHES ========
  // Import Sigma Meshes
  const assetLoader = new GLTFLoader();
  const loadAsset = (url, onLoad) => assetLoader.load(url.href, onLoad);
  const addToScene = gltf => {
    const obj = gltf.scene;
    scene.add(obj);
    obj.traverse(node => {
      if (node.isMesh) node.receiveShadow = true;
    });
  };

  loadAsset(cStrassen, addToScene);
  loadAsset(cBoden, addToScene);
  // Import Sigma HƤuser
  assetLoader.load(cHaeuser, gltf => {
    model = gltf.scene;
    collectibles = [];
    model.traverse(child => {
      if (child.name) collectibles.push(child);
      if (child.isMesh) {
        const material = child.material.clone();
        material.metalness = 0;
        material.emissive.setHex(0x89152e);
        material.emissiveIntensity = 0;
        child.material = material;
        child.castShadow = true;

        //console.log(`Mesh name: ${child.name}`);
      }
    });
    scene.add(model);
  });

  //window resize function
  window.addEventListener('resize', () => {
    camera.aspect = sigmaApp.offsetWidth / sigmaApp.offsetHeight;
    camera.updateProjectionMatrix();
    renderer.setSize(sigmaApp.offsetWidth, sigmaApp.offsetHeight);
    animate();
  });

  //==========================

  const raycaster = new THREE.Raycaster();
  const pointer = new THREE.Vector2(Infinity, Infinity);
  const cursorPosition = { x: Infinity, y: Infinity };
  let cursorInside = false;
  const hoveredElements = {}; // Track hovered states of divs

  // Define pairings of div IDs and mesh names
  const pairings = [
    { divId: 'Overb_44_SE', meshName: '44_Siemens-Energy_1' },
    { divId: 'Overb_39_UG', meshName: '39_UG' },
    { divId: 'Overb_39_EG', meshName: '39_EG' },
    { divId: 'Overb_39_1OG', meshName: '39_2' },
    { divId: 'Overb_39_2OG', meshName: '39_3' },
    //{ divId: 'Overb_39a_UG', meshName: '39a_UG' },
    { divId: 'Overb_39a_EG', meshName: '39a_EG' },
    { divId: 'Overb_39a_1OG', meshName: '39a_2' },
    { divId: 'Overb_39a_2OG', meshName: '39a_3' },
    { divId: 'Overb_41_EG', meshName: '41_EG' },
    { divId: 'Overb_41_1OG', meshName: '41_2' },
    // { divId: 'Overb_41_2OG', meshName: '41_3' },
    { divId: 'Overb_41a_vorne', meshName: '41a_Haus-vorn' },
    { divId: 'Overb_41a_unten', meshName: '41a_Haus-unten' },
    { divId: 'Overb_41a_oben', meshName: '41a_Haus-oben' },
    { divId: 'Overb_39_Turm1', meshName: '39_Etage_Tower-1' },
    { divId: 'Overb_39_Turm2', meshName: '39_Etage_Tower-2' },
    //{ divId: 'Overb_39_Turm3', meshName: '39_Etage_Tower-3' },

    // Add more pairings here
    // { divId: 'anotherDivId', meshName: 'anotherMeshName' },
  ];

  // Initialize hovered states for each pairing and add event listeners
  pairings.forEach(pair => {
    const hoverDivs = document.querySelectorAll(`#${pair.divId}`);
    hoverDivs.forEach(hoverDiv => {
      // Add event listeners to each matching hoverDiv
      if (hoverDiv) {
        hoverDiv.addEventListener('mouseenter', () => {
          hoveredElements[pair.divId] = true;
        });
        hoverDiv.addEventListener('mouseleave', () => {
          hoveredElements[pair.divId] = false;
        });
      }
    });
  });

  // Track mouse motion
  const onPointerMove = event => {
    const viewportRect = renderer.domElement.getBoundingClientRect();
    pointer.x = 2 * (event.clientX - viewportRect.left) / viewportRect.width - 1;
    pointer.y = -2 * (event.clientY - viewportRect.top) / viewportRect.height + 1;
    cursorPosition.x = event.clientX - viewportRect.left;
    cursorPosition.y = event.clientY - viewportRect.top;
  };

  viewport.addEventListener('pointermove', onPointerMove);
  viewport.addEventListener('pointerenter', () => cursorInside = true);
  viewport.addEventListener('pointerleave', () => {
    cursorInside = false;
    cursorPosition.x = Infinity;
    cursorPosition.y = Infinity;
    pointer.set(Infinity, Infinity);
  });

  const setEmissive = (child, value) => {
    if (child.isMesh) child.material.emissiveIntensity = value;
  };

  const setMetalness = (child, value) => {
    if (child.isMesh) child.material.metalness = value;
  };

  // Make all objects normal
  const selectNothing = () => {
    if (model) {
      model.traverse(child => setEmissive(child, 0));
      model.traverse(child => setMetalness(child, 0));
    }
  };

  // Mark selected object
  const selectElementByName = name => {
    if (model) {
      model.traverse(child => {
        if (child.isMesh && child.name === name) {
          child.material.metalness = 1;
          child.material.emissiveIntensity = 1;
        }
      });
    }
  };

  const animationLoop = () => {
    if (model) {
      selectNothing();
      if (cursorInside) {
        raycaster.setFromCamera(pointer, camera);
        const intersects = raycaster.intersectObjects(collectibles);
        if (intersects.length) {
          const intersectedObject = intersects[0].object;
          pairings.forEach(pair => {
            const hoverDiv = document.getElementById(pair.divId);
            if (intersectedObject.name === pair.meshName) {
              selectElementByName(pair.meshName);
              if (hoverDiv) hoverDiv.classList.add('hover');
            } else if (hoverDiv) {
              hoverDiv.classList.remove('hover');
            }
          });
        } else {
          pairings.forEach(pair => {
            const hoverDiv = document.getElementById(pair.divId);
            if (hoverDiv) hoverDiv.classList.remove('hover');
          });
        }
      }
      pairings.forEach(pair => {
        if (hoveredElements[pair.divId]) selectElementByName(pair.meshName);
      });
    }
    controls.update();
    renderer.render(scene, camera);
  };

  renderer.setAnimationLoop(animationLoop);

}

Any help you guys can provide me is greatly appreciated!