three.js3dtexturesvisualizationmesh

3d-force-graph and Three.js - Add geometric glow / atmospheric material / simple texture to individual nodes and node groups


I am working on a 3D force-graph visualisation and I'm using the great library:

https://github.com/vasturiano/3d-force-graph

I would like to try to get some sort of glowing effect on the nodes of the force-graph and I was having a look at something like this to accomplish it, but it is proving a challenge:

https://github.com/jeromeetienne/threex.geometricglow

So far I only have the ability to add new shapes and texture them. I have also added text sprites that follow the nodes, so I suspect an approach that combines these two might be possible. See below the code.

Adding a glow effect or texture to this base code would also be very helpful:

https://github.com/vasturiano/3d-force-graph/blob/master/example/async-load/index.html

<head>
  <style>
    body {
      margin: 0;
    }
  </style>
  <script src="//unpkg.com/three"></script>
  <script src="//unpkg.com/three-spritetext"></script>
  <script src="//unpkg.com/3d-force-graph"></script>
  <!--<script src="../../dist/3d-force-graph.js"></script>-->
</head>
<body>
  <div id="3d-graph"></div>
  <script>
    const loader = new THREE.TextureLoader();
    const Graph = ForceGraph3D()
      (document.getElementById('3d-graph')).jsonUrl('../datasets/testdata.json').nodeLabel('id').backgroundColor('#F7F8FA').nodeAutoColorBy('group').nodeThreeObjectExtend(true).nodeThreeObject(node => {
        // extend link with text sprite
        const sprite = new SpriteText(`${node.id}`);
        sprite.color = 'lightgrey';
        sprite.textHeight = 4
        sprite.fontFace = "Comic Sans MS"
        sprite.position.set(5, 5, 5)
        return sprite;
      }).nodeVal('size').linkWidth(2)
    const distance = 600;
    //
    const sphereGeometry = new THREE.SphereGeometry(18);
    const sphereMaterial = new THREE.MeshBasicMaterial({
      map: loader.load('../datasets/texture.jpg')
    });
    const mesh = new THREE.Mesh(sphereGeometry, sphereMaterial);
    mesh.position.set(9, 17, 22);
    Graph.scene().add(mesh);
    //
    // camera orbit
    let angle = 0;
    setInterval(() => {
      Graph.cameraPosition({
        x: distance * Math.sin(angle),
        z: distance * Math.cos(angle)
      });
      angle += Math.PI / 1000;
    }, 10); //
    let materialArray = [];
    let texture_ft = new THREE.TextureLoader().load('../datasets/penguins/kenon_star_ft.jpg');
    let texture_bk = new THREE.TextureLoader().load('../datasets/penguins/kenon_star_bk.jpg');
    let texture_up = new THREE.TextureLoader().load('../datasets/penguins/kenon_star_up.jpg');
    let texture_dn = new THREE.TextureLoader().load('../datasets/penguins/kenon_star_dn.jpg');
    let texture_rt = new THREE.TextureLoader().load('../datasets/penguins/kenon_star_rt.jpg');
    let texture_lf = new THREE.TextureLoader().load('../datasets/penguins/kenon_star_lf.jpg');
    materialArray.push(new THREE.MeshBasicMaterial({
      map: texture_ft
    }));
    materialArray.push(new THREE.MeshBasicMaterial({
      map: texture_bk
    }));
    materialArray.push(new THREE.MeshBasicMaterial({
      map: texture_up
    }));
    materialArray.push(new THREE.MeshBasicMaterial({
      map: texture_dn
    }));
    materialArray.push(new THREE.MeshBasicMaterial({
      map: texture_rt
    }));
    materialArray.push(new THREE.MeshBasicMaterial({
      map: texture_lf
    }));
    for (let i = 0; i < 6; i++) materialArray[i].side = THREE.BackSide;
    let skyboxGeo = new THREE.BoxGeometry(10000, 10000, 10000);
    let skybox = new THREE.Mesh(skyboxGeo, materialArray);
    Graph.scene().add(skybox);
    /* loader.load('https://images.pexels.com/photos/1205301/pexels-photo-1205301.jpeg', function(texture) {
         scene.background = texture;
     });
     */
  </script>
</body>

Thanks


Solution

  • I've been interested in getting this to work as well.

    I used this threex example and seen that these two lines were necessary:

    var glowMesh    = new THREEx.GeometricGlowMesh(mesh)
    mesh.add(glowMesh.object3d)
    

    Combine that with this 3d-force-graph example for the custom node geometry.

    The whole basic working thing is below:

    // Random tree data
    var generateData = function() {
      const N = 50;
      const gData = {
        nodes: [...Array(N).keys()].map(i => ({ id: i })),
        links: [...Array(N).keys()]
      .filter(id => id)
        .map(id => ({
          source: id,
          target: Math.round(Math.random() * (id-1))
        }))
      };
      
      return gData;
    }
    
    // Create a three.js sphere mesh.
    var sphereMesh = function(id) {
      var mesh =  new THREE.Mesh(
        [
          new THREE.SphereGeometry(10, 32, 32)
        ][id%1],
        new THREE.MeshLambertMaterial({
          color: '#277ec9',
          transparent: true,
          opacity: 1.0
        })
      )
    
      // Make it glow.
      var glowMesh = new THREEx.GeometricGlowMesh(mesh);
      mesh.add(glowMesh.object3d);
    
      var insideUniforms  = glowMesh.insideMesh.material.uniforms;
      insideUniforms.glowColor.value.set('yellow');
    
      var outsideUniforms = glowMesh.outsideMesh.material.uniforms;
      outsideUniforms.glowColor.value.set('yellow');
      
      return mesh
    }
    
    const Graph = ForceGraph3D()
      (document.getElementById('3d-graph'))
        .graphData(generateData())
        .nodeThreeObject(({ id }) => sphereMesh(id))
        .nodeLabel('id');
     
     /*
     The following three snippets of code are the minimum required from the THREEx library.
     */
     
     // ========== threex.dilategeometry.js =============
     
     /**
     * @namespace
     */
    var THREEx  = THREEx || {}
    
    /**
     * dilate a geometry inplace
     * @param  {THREE.Geometry} geometry geometry to dilate
     * @param  {Number} length   percent to dilate, use negative value to erode
     */
    THREEx.dilateGeometry = function(geometry, length){
      // gather vertexNormals from geometry.faces
      var vertexNormals = new Array(geometry.vertices.length);
      geometry.faces.forEach(function(face){
        if( face instanceof THREE.Face4 ){
          vertexNormals[face.a] = face.vertexNormals[0];
          vertexNormals[face.b] = face.vertexNormals[1];
          vertexNormals[face.c] = face.vertexNormals[2];
          vertexNormals[face.d] = face.vertexNormals[3];    
        }else if( face instanceof THREE.Face3 ){
          vertexNormals[face.a] = face.vertexNormals[0];
          vertexNormals[face.b] = face.vertexNormals[1];
          vertexNormals[face.c] = face.vertexNormals[2];
        }else console.assert(false);
      });
      // modify the vertices according to vertextNormal
      geometry.vertices.forEach(function(vertex, idx){
        var vertexNormal = vertexNormals[idx];
        vertex.x  += vertexNormal.x * length;
        vertex.y  += vertexNormal.y * length;
        vertex.z  += vertexNormal.z * length;
      });   
    };
    
    
    // ========== threex.atmospherematerial.js =============
     
    var THREEx = THREEx || {}
    
    /**
     * from http://stemkoski.blogspot.fr/2013/07/shaders-in-threejs-glow-and-halo.html
     * @return {[type]} [description]
     */
    THREEx.createAtmosphereMaterial = function(){
      var vertexShader  = [
        'varying vec3 vVertexWorldPosition;',
        'varying vec3 vVertexNormal;',
    
        'varying vec4 vFragColor;',
    
        'void main(){',
        ' vVertexNormal = normalize(normalMatrix * normal);',
    
        ' vVertexWorldPosition  = (modelMatrix * vec4(position, 1.0)).xyz;',
    
        ' // set gl_Position',
        ' gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);',
        '}',
    
        ].join('\n')
      var fragmentShader  = [
        'uniform vec3 glowColor;',
        'uniform float  coeficient;',
        'uniform float  power;',
    
        'varying vec3 vVertexNormal;',
        'varying vec3 vVertexWorldPosition;',
    
        'varying vec4 vFragColor;',
    
        'void main(){',
        ' vec3 worldCameraToVertex= vVertexWorldPosition - cameraPosition;',
        ' vec3 viewCameraToVertex = (viewMatrix * vec4(worldCameraToVertex, 0.0)).xyz;',
        ' viewCameraToVertex  = normalize(viewCameraToVertex);',
        ' float intensity   = pow(coeficient + dot(vVertexNormal, viewCameraToVertex), power);',
        ' gl_FragColor    = vec4(glowColor, intensity);',
        '}',
      ].join('\n')
    
      // create custom material from the shader code above
      //   that is within specially labeled script tags
      var material  = new THREE.ShaderMaterial({
        uniforms: { 
          coeficient  : {
            type  : "f", 
            value : 1.0
          },
          power   : {
            type  : "f",
            value : 2
          },
          glowColor : {
            type  : "c",
            value : new THREE.Color('pink')
          },
        },
        vertexShader  : vertexShader,
        fragmentShader  : fragmentShader,
        //blending  : THREE.AdditiveBlending,
        transparent : true,
        depthWrite  : false,
      });
      return material
    }
    
    
    // ========== threex.geometricglowmesh.js =============
     
    var THREEx  = THREEx || {}
    
    THREEx.GeometricGlowMesh  = function(mesh){
      var object3d  = new THREE.Object3D
    
      var geometry  = mesh.geometry.clone()
      THREEx.dilateGeometry(geometry, 0.01)
      var material  = THREEx.createAtmosphereMaterial()
      material.uniforms.glowColor.value = new THREE.Color('cyan')
      material.uniforms.coeficient.value  = 1.1
      material.uniforms.power.value   = 1.4
      var insideMesh  = new THREE.Mesh(geometry, material );
      object3d.add( insideMesh );
    
    
      var geometry  = mesh.geometry.clone()
      THREEx.dilateGeometry(geometry, 0.1)
      var material  = THREEx.createAtmosphereMaterial()
      material.uniforms.glowColor.value = new THREE.Color('cyan')
      material.uniforms.coeficient.value  = 0.1
      material.uniforms.power.value   = 1.2
      material.side = THREE.BackSide
      var outsideMesh = new THREE.Mesh( geometry, material );
      object3d.add( outsideMesh );
    
      // expose a few variable
      this.object3d = object3d
      this.insideMesh = insideMesh
      this.outsideMesh= outsideMesh
    }
    <script src="https://threejs.org/build/three.js"></script>
    
    <head>
      <style> body { margin: 0; } </style>
      <script src="//unpkg.com/3d-force-graph"></script>
    </head>
    
    <body>
      <div id="3d-graph"></div>
    </body>

    Here's the same thing on jsfiddle. You can see the results better here.