Clickable images on 3d rotating globe

I have a rotating globe created with three.js, and on that, there are some images created using svg, I wanted to make the images clickable. Attaching the code of the section below. Any help will be much appreciated. Thanks in advance!

// ------------------------------ \\\
// ---------- IMPORTS ----------- \\\
// ------------------------------ \\\
import $ from "jquery"
import * as THREE from "../libs/three.module"
import {BufferGeometryUtils} from "../libs/BufferGeometryUtils";



// ------------------------------ \\\
// ------------ VARS ------------ \\\
// ------------------------------ \\\
var renderer                = new THREE.WebGLRenderer({alpha:true, antialias: true});
var clock                   = new THREE.Clock(true);

var scene                   = null;
var _width                  = window.innerHeight;
var _height                 = window.innerHeight;
var aspect                  = _width / _height;
var camera                  = new THREE.PerspectiveCamera(45, aspect, 1, 1000);
var earth                   = null;
var earthUniforms           = null;
var atmosphereUniforms      = null;
var atmosphere              = null;
var amount                  = $('#canvas').data('people');
var mapC, group;

var _countLoad            = 0;

var  earthVertexShader      = `uniform vec3 lightDirection;

                            varying vec2 vUv;
                            varying vec3 vEyeDirectionEyeSpace;
                            varying vec3 vLightDirection;
                            attribute vec4 tangent;

                            // all in eye space
                            varying mat3 tbn;

                            void main(){

                              vUv = uv;
                              gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);

                              vLightDirection = mat3(viewMatrix) * lightDirection; // should be computed outside of shader
                              vEyeDirectionEyeSpace = mat3(viewMatrix) * normalize(position - cameraPosition).xyz;

                              // normal mapping
                              vec3 t = normalize(tangent.xyz);
                              vec3 n = normalize(normal.xyz);
                              vec3 b = normalize(cross(t, n));

                              // everything in eye space
                              t = normalize(normalMatrix * t);
                              b = normalize(normalMatrix * b);
                              n = normalize(normalMatrix * n);

                              tbn = mat3(t, b, n);

                                }`;

var earthFragmentShader     = ` uniform sampler2D diffuseTexture;
                            uniform sampler2D diffuseNight;
                            uniform sampler2D specularMap;
                            uniform sampler2D cloudsMap;
                            uniform sampler2D normalMap;

                            varying vec2 vUv;
                            varying vec3 vEyeDirectionEyeSpace;
                            varying vec3 vLightDirection;

                            // tangent-bitangent-normal matrix
                            varying mat3 tbn;

                            void main(){

                              vec3 lightDir = normalize(vLightDirection);

                              vec3 n        = texture2D(normalMap, vUv).xyz * 2.0 - 1.0;
                              vec3 normal   = normalize(tbn * n);


                              // directional light
                              float lightIntensity  = dot(normal, lightDir);
                              float selectImage     = dot(tbn[2], lightDir);

                              gl_FragColor = texture2D(diffuseTexture, vUv) * selectImage + texture2D(diffuseNight, vUv) * (1.0-selectImage);

                              //gl_FragColor = vec4(vec3(0.5), 1.0 );
                              gl_FragColor *= (1.0 + 10.0*(lightIntensity - selectImage));

                              // specular
                              vec3 reflection = reflect(lightDir, normal);
                              float specPower = texture2D(specularMap, vUv).r;

                              float spec  = 10.2;
                              float gloss = 0.1 * texture2D(specularMap, vUv).a;

                              float specular  =  pow(clamp(dot(reflection, normalize(vEyeDirectionEyeSpace)), 0.0, 1.0), spec) * gloss;
                              gl_FragColor    = gl_FragColor + specular * vec4(0.26, 0.96, 0.99, 1);

                              // cloud colors + a small bump
                              vec4 cloudsColor = texture2D(cloudsMap, vUv) * vec4(1.0, 1.5, 1.2, 1.0);

                              vec4 cloudsShadow = texture2D(cloudsMap, vec2(vUv.x+ normal.x * 0.005, vUv.y + normal.y * 0.005));

                              if (cloudsColor.r < 0.1 && cloudsShadow.r > 0.1){
                                gl_FragColor *= 0.75;
                                cloudsShadow = vec4(0);
                              }

                              gl_FragColor = gl_FragColor * (vec4(1.2) - cloudsColor) + cloudsColor * (lightIntensity * 2.0);

                                }`;

const atmosphereVertexShader  = `uniform vec3 earthCenter;
                                uniform float earthRadius;
                                uniform float atmosphereRadius;
                                uniform vec3 lightDirection;

                                varying float atmosphereThickness;
                                varying vec3 vLightDirection;
                                varying vec3 vNormalEyeSpace;


                                void main(){
                                  gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);

                                  vec3 positionW = (modelMatrix * vec4(position, 1.0)).xyz;

                                  vec3 vCameraEarth   = cameraPosition.xyz - earthCenter;
                                  vec3 vCameraVertex  = normalize(cameraPosition.xyz - positionW);

                                  float tca = dot(vCameraEarth,  vCameraVertex);

                                  if (tca < 0.0){
                                    // not intesect, looking in opposite direction
                                    atmosphereThickness = 0.0;
                                    return;
                                  }

                                  float dsq = dot(vCameraEarth, vCameraEarth) - tca * tca;
                                  float thc_sq_atmosphere = max(atmosphereRadius * atmosphereRadius - dsq, 0.0);
                                  float thc_sq_earth = max(earthRadius * earthRadius - dsq, 0.0);

                                  float thc_atmosphere = 2.0 * sqrt(thc_sq_atmosphere);
                                  float thc_earth = 2.0 * sqrt(max(0.0,thc_sq_earth));

                                  float thc           = (thc_atmosphere - thc_earth) * 0.09; // 0.01 - density factor
                                  atmosphereThickness = thc;

                                  // light calculation
                                  vLightDirection = mat3(viewMatrix) * lightDirection;
                                  vNormalEyeSpace = normalize(normalMatrix * normal);

                                }`;
                                
const atmosphereFragShader    = ` varying float atmosphereThickness;
                                varying vec3 vLightDirection;
                                varying vec3 vNormalEyeSpace;

                                void main(){

                                  vec3 lightDir = normalize(vLightDirection);
                                  vec3 normal = normalize(vNormalEyeSpace);
                                  float lightIntensity = max(dot(normal, lightDir) * 1.5, -0.7);
                                  gl_FragColor = vec4( (vec3(57.0, 97.0, 162.0) / 256.0) * (1.0 + lightIntensity), atmosphereThickness);

                                }`;

var _url                    = null;

// ----------------------------------------- \\\
// ------------ PUBLIC FUNCIONS ------------ \\\
// ----------------------------------------- \\\
async function init() {

  // initialize the renderer
  renderer.setSize(_width, _height);
  renderer.autoClear = false;
  document.getElementById("canvas").appendChild(renderer.domElement);

  if(window.location.hostname == 'kcgv10.kingscrestglobal.com'){
    _url = "wp-content/themes/kcg/assets/earth/";
  }else{
    _url = "assets/earth/";
  }

  scene     = await loadObject(_url+"earth_and_water.json");
  scene.fog = new THREE.Fog( 0x000000, 1500, 2100 );

  const textureLoader = new THREE.TextureLoader();
	
  group = new THREE.Group();

  for ( let a = 0; a < amount.length; a ++ ) {

    const x = Math.random() - 0.5;
    const y = getRandomInt(-0.15, 0.15);
    const z = Math.random() - 0.5;
    

    if(amount[0].place == 'south-america'){

    } else if(amount[0].place == 'north-america'){

    } else if(amount[0].place == 'europe'){
      
    } else if(amount[0].place == 'asia'){
      
    } else if(amount[0].place == 'africa'){
      
    } else if(amount[0].place == 'oceania'){
      
    }
    

    let material;

    mapC          = textureLoader.load( amount[a].thumb );

    material      = new THREE.SpriteMaterial( { map: mapC, color: 0xffffff, fog: true } );

    const sprite  = new THREE.Sprite( material );

    sprite.position.set( x, y, z );
    sprite.position.normalize();
    sprite.position.multiplyScalar( 12.2 );

    group.add( sprite );

  }

  scene.add( group );

  camera.position.set(5,0,44);

  earth       = scene.getObjectByName("Earth");
  atmosphere  = scene.getObjectByName("Atmosphere");

  // Mesh Configurations
  earth.receiveShadow = true;
  earth.castShadow    = true;

  camera.lookAt(earth.position);

  fixMaterials().then( () => {
    renderer.autoClear = false;
    render();
  });

}

function resize() {

  _width    = $(window).height();
  _height   = $(window).height();

  renderer.setSize(_width, _height);

  // console.log(_width)

  if(camera != null) {
    camera.aspect = _width / _height;
    camera.updateProjectionMatrix();
  }
}



// ----------------------------------------- \\\
// ------------ PRIVATE FUNCIONS ----------- \\\
// ----------------------------------------- \\\
function update(dt){

  let lightPos  = new THREE.Vector3( -10, 10, -3 );
  let lightPosU = new THREE.Uniform(newVector(lightPos));

  earthUniforms.lightDirection = lightPosU;
  earth.rotation.y += 0.05 * dt;

  atmosphereUniforms.lightDirection = lightPosU;
  atmosphere.rotation.y += 0.05 * dt;

    for ( let i = 0, l = group.children.length; i < l; i ++ ) {

      const sprite 	= group.children[ i ];

      sprite.scale.set( 2, 2, 1 );

    }

    group.rotation.y += 0.05 * dt;

}

function render(){

  requestAnimationFrame(render);
  
  update(clock.getDelta())
  renderer.clear();

  renderer.render(scene, camera);
  
}

function newVector(v){
  return new THREE.Vector3(v.x, v.y, v.z);
}

function getRandomInt(min, max) {
  return Math.floor(Math.random() * (max - min + 1)) + min;
}

async function loadTexture(texture){
  let imgLoader = new THREE.TextureLoader();
  
  return new Promise( (resolve, reject) => imgLoader.load(texture, 
    
    function (tex){

      tex.anisotropy = renderer.capabilities.getMaxAnisotropy();
      
      resolve(tex);

      
      if(_countLoad >= 4){
        // console.log('LOADER ASSETS');
        $(window).trigger('LOADER_ALL');
      }

      // console.log(_countLoad);

      _countLoad++;
    

    }, null, reject))
}

async function fixMaterials() {


  atmosphereUniforms = {

    earthCenter: new THREE.Uniform(earth.position),
    earthRadius: new THREE.Uniform(10.0),
    atmosphereRadius: new THREE.Uniform(10.4),

  }

  earthUniforms = {

    diffuseTexture: {
      type: "t",
      value: await loadTexture(_url+"earth_diffuse.jpg")
    },
    diffuseNight: {
      type: "t",
      value: await loadTexture(_url+"earth_diffuse_night.jpg")
    },
    normalMap: {
      type: "t",
      value: await loadTexture(_url+"earth_normal_map.jpg")
    },
    specularMap: {
      type: "t",
      value: await loadTexture( _url+"earth_specular_map.png")
    },
    cloudsMap: {
      type: "t",
      value: await loadTexture( _url+"earth_diffuse_clouds.jpg")
    }

  }

  update(0);

  BufferGeometryUtils.computeTangents(earth.geometry);

  earth.material      = new THREE.ShaderMaterial({
    uniforms          : earthUniforms,
    vertexShader      : earthVertexShader,
    fragmentShader    : earthFragmentShader,
    side              : THREE.FrontSide
  });

  atmosphere.material = new THREE.ShaderMaterial({
    uniforms          : atmosphereUniforms,
    vertexShader      : atmosphereVertexShader,
    fragmentShader    : atmosphereFragShader,
    blending          : THREE.CustomBlending,
    blendEquation     : THREE.AddEquation,
    blendSrc          : THREE.SrcAlphaFactor,
    blendDst          : THREE.OneMinusSrcAlphaFactor,
    side              : THREE.FrontSide,
    transparent       : true,
  });

}

async function loadObject(json){
  let objLoader = new THREE.ObjectLoader();
  return new Promise( (accept, reject) => objLoader.load(json, accept, null ,reject));

}



// ----------------------------------------- \\\
// ---------------- EXPORTS ---------------- \\\
// ----------------------------------------- \\\
export { init, resize }


        <div class="home-globe">
          <div class="g-wrapper">
            <div class="circle"></div>
            <div id="canvas" class="canvas" data-people='[{"person":"Aline", "thumb":"assets/photos/person-1.png", "place":"south-america"},{"person":"Saul", "thumb":"assets/photos/person-2.png", "place":"europe"},{"person":"Rita", "thumb":"assets/photos/person-3.png", "place":"europe"},{"person":"Fillipa", "thumb":"assets/photos/person-4.png", "place":"north-america"},{"person":"Salman", "thumb":"assets/photos/person-5.png", "place":"asia"},{"person":"Russel", "thumb":"assets/photos/person-6.png", "place":"oceania"},{"person":"Luiz", "thumb":"assets/photos/person-7.png", "place":"africa"}, {"person":"Luiz", "thumb":"assets/photos/person-8.png", "place":"africa"}, {"person":"Luiz", "thumb":"assets/photos/person-9.png", "place":"africa"}]'>
            </div>
          </div>
        </div>

Which part of the code loads / stores the SVGs :thinking: ?

Adding clickability should be very simple - itā€™s a matter of using a Raycaster.intersectObjects with proper target objects.

Itā€™s present on html file, loading via an object

How do they be rendered on the globe though?

Actually, I havenā€™t use three.js before and this code was done by my senior. I am clueless about the above codes. All i was asked to make it clickable and which I couldnā€™t fix. Can you please have a look and tell me how to work with this

I donā€™t know which part of it should be made clickable though ._. ? Your code does not load nor append SVGs at any point.

I guess you want to click the images of the people?
These are loaded as sprites

Yes, I want to make the images clickable

Hereā€™s the append code

  // initialize the renderer
  renderer.setSize(_width, _height);
  renderer.autoClear = false;
  document.getElementById("canvas").appendChild(renderer.domElement);
  1. That code is not appending SVGs.
  2. If itā€™s indeed the sprites that youā€™d like to make clickable - see my earlier answer. Since you put all sprites into group - you can use a Raycaster, pass group.children as targets, and listen to interactions on every frame (hereā€™s an example of how it can be done.)

She wants the:

data-people='[{"person":"Aline", "thumb":"assets/photos/person-1.png", "place":"south-america"},{"person":"Saul", "thumb":"assets/photos/person-2.png", "place":"europe"},{"person":"Rita", "thumb":"assets/photos/person-3.png", "place":"europe"},{"person":"Fillipa", "thumb":"assets/photos/person-4.png", "place":"north-america"},{"person":"Salman", "thumb":"assets/photos/person-5.png", "place":"asia"},{"person":"Russel", "thumb":"assets/photos/person-6.png", "place":"oceania"},{"person":"Luiz", "thumb":"assets/photos/person-7.png", "place":"africa"}, {"person":"Luiz", "thumb":"assets/photos/person-8.png", "place":"africa"}, {"person":"Luiz", "thumb":"assets/photos/person-9.png", "place":"africa"}]'

So, in essence her Senior is attempting to get clickable geo-interpolation out of this texture wrapped sphere. I suppose if the use-case is limited to just a few this will work but its not the right way to do it IMO. These are essentially 3D objects and not a clickable < d i v >. The HTML DOM has no idea these objects exist. The GPU does but in order for the GPU to be able to pick these objects then you must use GPU picking/Raycasting. So, the response provided was to show how to set a ray to cast out across the model (from the camera) and return when it intersects with one of your 3D objects (images/SVG). To know if you have intersected the right object then you must have an identifying ā€œtypeā€ assigned to the object (or GUID, name, id, etc.) something that can identify this object out of the other 3D objects on the scene. This intersected object then has to be accompanied by an event listener and a dispatcher to actually perform the click action when the user clicks on object.

Here is a modified version of such a event setter. I put into a module that you can use, or, you can gut the important pieces. Perhaps someone here, or your Senior, can help apply it:

import {
    Vector3,
    Raycaster
} from '../../../build/three.module.js';

function setCustomEvents ( camera, items, type, wait ) {

    let debounce = ( func, delay ) => {

        let debounceTimer
       
        return function() {

            const context = this;
            const args = arguments;

                clearTimeout( debounceTimer );
                debounceTimer = setTimeout(() => func.apply( context, args ), delay)
        }
    }  

    let listener = function ( event ) {

        let mouse = {

            x: (( event.clientX - 1) / window.innerWidth ) * 2 - 1,
            y: -(( event.clientY - 1 ) / window.innerHeight ) * 2 + 1

        };

        let raycaster = new Raycaster();
        let vector = new Vector3();

            vector.set( mouse.x, mouse.y, 0.5 );
            vector.unproject( camera );
            raycaster.ray.set( camera.position, vector.sub( camera.position ).normalize() );

        let intersects = raycaster.intersectObjects( items );

            if( intersects.length > 0 ) {

                intersects[ 0 ].type = type;
     
                const instanceId = intersects[ 0 ].instanceId;
                undefined !== instanceId ? console.log( 'instanceId', instanceId ) : null;
                undefined !== intersects[ 0 ].object ? intersects[ 0 ].object.dispatchEvent( intersects[ 0 ] ) : null;

            }
    };

    false == wait ? document.addEventListener( type, listener, false ) : document.addEventListener( type, debounce( listener, wait ), false );

}

export { setCustomEvents };

This will allow you to cast a ray ā€˜at time of clickā€™ on the appropriate object. You would use it in combination with an event listener on the actual object like so:

Your3DObject.addEventListener( 'click', yourCustomFunction );
setCustomEvents( camera, [ Your3DObject], 'click' );

I hope this helps.

1 Like

My senior is not available at the moment! And can not even take help from others as theyā€™re super busy with their projects! can you please do me this favour and add this to my code! when users will click they will get the link from the data passed on the Html div above the canvas.
earth.js (13.4 KB)


        <div class="home-globe">
          <div class="g-wrapper">
            <div class="circle"></div>
            <div id="canvas" class="canvas" data-people='[{"person":"Aline", "thumb":"assets/photos/person-1.png", "place":"south-america"},{"person":"Saul", "thumb":"assets/photos/person-2.png", "place":"europe"},{"person":"Rita", "thumb":"assets/photos/person-3.png", "place":"europe"},{"person":"Fillipa", "thumb":"assets/photos/person-4.png", "place":"north-america"},{"person":"Salman", "thumb":"assets/photos/person-5.png", "place":"asia"},{"person":"Russel", "thumb":"assets/photos/person-6.png", "place":"oceania"},{"person":"Luiz", "thumb":"assets/photos/person-7.png", "place":"africa"}, {"person":"Luiz", "thumb":"assets/photos/person-8.png", "place":"africa"}, {"person":"Luiz", "thumb":"assets/photos/person-9.png", "place":"africa"},{"person":"Nazifa", "thumb":"assets/photos/person-9.png", "place":"Bangladesh"}]'>
            </div>
          </div>
        </div>

If you have no particular issues stopping you, neither you ask specific questions thatā€™d help you move forward, and you just want someone to do the entire work for you - wouldnā€™t it be more fair to post it in Jobs and offer a compensation in return :thinking: ?

2 Likes

I am sorry Nazifa but I am currently working on my own project and my time is limited. As I was learning ThreeJS myself I was not able to give back to the community. I have received a ton of help from these guys though and in what little time I have I am trying to give back where I can. I hope you can get the help you need and definitely continue to reach out for help or, as others have suggested, post a job to appropriate section if you cannot figure it out.

Btw, I have built such a globe myself: https://electronnight.netlify.app

WebGL is absolutely where the UI is going.

Thanks a lot! Iā€™ll do that :slight_smile: