Search code examples
reactjscolorsthree.jsvertex-shadervertex

How to assign different color for each vertex in a buffer geometry? Three.js


I am new to Three.js and I have found a few answers on the topic but none have used buffer geometry. I am making a 3D terrain project of Europe on React and I want to be able to change the water levels depending on some values. In order to do that my coloring has to be done vertex by vertex instead of adding a texture from an image where the coloring will not change if I change the vertices' values. So, depending on the "height" of the vertex I want to assign different colors.

Here's my componentDidMount function:

const SIZE_AMPLIFIER = 15;
var container = document.getElementById("main_map");
var scene, camera, renderer, controls;
var data, plane;

//load map data from bin file

 function loadTerrain(file) {
  var xhr = new XMLHttpRequest();
  xhr.responseType = 'arraybuffer';
  xhr.open('GET', file, true);
  xhr.onload = function (evt) {
    if (xhr.response) {
      data = new Uint16Array(xhr.response)
      init()
    }

  };
  xhr.send(null);
}

loadTerrain('stats.bin');

function init() {

  // initialize camera
  camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, .1, 100000);
  camera.position.set(10000, 4000, 0);

  // initialize scene
  scene = new THREE.Scene();

  // initialize directional light (sun)
  var sun = new THREE.DirectionalLight(0xFFFFFF, 1.0);
  sun.position.set(300, 400, 300);
  sun.distance = 1000;
  scene.add(sun);

  var frame = new THREE.SpotLightHelper(sun);
  scene.add(frame);

  // initialize renderer
  renderer = new THREE.WebGLRenderer();
  renderer.setClearColor(0x000000);
  renderer.setPixelRatio(window.devicePixelRatio);
  renderer.setSize(window.innerWidth, window.innerHeight);
  container.append(renderer.domElement);

  //initialize controls

  controls = new OrbitControls(camera, renderer.domElement);
  controls.enableDamping = true;
  controls.dampingFactor = 1;
  controls.rotateSpeed = .8;
  controls.maxPolarAngle = Math.PI / 2 - .3;

  // initialize plane
  plane = new THREE.PlaneBufferGeometry(1500 * SIZE_AMPLIFIER, 1500 * SIZE_AMPLIFIER, 999 , 999);
  plane.castShadow = true;
  plane.receiveShadow = true;

  var vertices = plane.attributes.position.array;
  console.log(data)

  // apply height map to vertices of plane
  // assign different color depending on the value??

  for (let i = 0, j = 2; i < data.length; i += 1, j += 3) {
    if(data[i] == 0){
      vertices[j] = 0
    }else {
      vertices[j] = data[i] / 65535 * 325 + 10
    }
  }

  // Add texture image
  var material = new THREE.MeshLambertMaterial({ 
      map:THREE.ImageUtils.loadTexture('stats_color.png')
   })

  var mesh = new THREE.Mesh(plane, material);
  mesh.rotation.x = - Math.PI / 2;
  mesh.matrixAutoUpdate = false;
  mesh.updateMatrix();

  plane.computeFaceNormals();
  plane.computeVertexNormals();

  scene.add(mesh);

  animate();
}

function animate() {
  requestAnimationFrame(animate);

  renderer.render(scene, camera);
  //controls.update();
}

The result after this (it is satisfying for me but unfortunately not effective for the purpose of the application):


Solution

  • Here is an article on how to use BufferGeometry

    To add colors you make a Float32Array of colors and add it as an attribute called 'color'.

      geometry.setAttribute(
          'color',
          new THREE.BufferAttribute(new Float32Array(colors), uvNumComponents));
    

    You then set material.vertexColors to true;

      const material = new THREE.MeshPhongMaterial({
        vertexColors: true,
      });
    

    body {
      margin: 0;
    }
    #c {
      width: 100vw;
      height: 100vh;
      display: block;
    }
    <canvas id="c"></canvas>
    <script type="module">
    import * as THREE from 'https://threejsfundamentals.org/threejs/resources/threejs/r115/build/three.module.js';
    
    function main() {
      const canvas = document.querySelector('#c');
      const renderer = new THREE.WebGLRenderer({canvas});
    
      const fov = 75;
      const aspect = 2;  // the canvas default
      const near = 0.1;
      const far = 100;
      const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
      camera.position.z = 3;
    
      const scene = new THREE.Scene();
    
      {
        const color = 0xFFFFFF;
        const intensity = 1;
        const light = new THREE.DirectionalLight(color, intensity);
        light.position.set(-1, 2, 4);
        scene.add(light);
      }
    
      // NOT A GOOD EXAMPLE OF HOW TO MAKE A CUBE!
      // Only trying to make it clear most vertices are unique
      const vertices = [
        // front
        { pos: [-1, -1,  1], norm: [ 0,  0,  1], uv: [0, 1], },
        { pos: [ 1, -1,  1], norm: [ 0,  0,  1], uv: [1, 1], },
        { pos: [-1,  1,  1], norm: [ 0,  0,  1], uv: [0, 0], },
    
        { pos: [-1,  1,  1], norm: [ 0,  0,  1], uv: [0, 0], },
        { pos: [ 1, -1,  1], norm: [ 0,  0,  1], uv: [1, 1], },
        { pos: [ 1,  1,  1], norm: [ 0,  0,  1], uv: [1, 0], },
        // right
        { pos: [ 1, -1,  1], norm: [ 1,  0,  0], uv: [0, 1], },
        { pos: [ 1, -1, -1], norm: [ 1,  0,  0], uv: [1, 1], },
        { pos: [ 1,  1,  1], norm: [ 1,  0,  0], uv: [0, 0], },
    
        { pos: [ 1,  1,  1], norm: [ 1,  0,  0], uv: [0, 0], },
        { pos: [ 1, -1, -1], norm: [ 1,  0,  0], uv: [1, 1], },
        { pos: [ 1,  1, -1], norm: [ 1,  0,  0], uv: [1, 0], },
        // back
        { pos: [ 1, -1, -1], norm: [ 0,  0, -1], uv: [0, 1], },
        { pos: [-1, -1, -1], norm: [ 0,  0, -1], uv: [1, 1], },
        { pos: [ 1,  1, -1], norm: [ 0,  0, -1], uv: [0, 0], },
    
        { pos: [ 1,  1, -1], norm: [ 0,  0, -1], uv: [0, 0], },
        { pos: [-1, -1, -1], norm: [ 0,  0, -1], uv: [1, 1], },
        { pos: [-1,  1, -1], norm: [ 0,  0, -1], uv: [1, 0], },
        // left
        { pos: [-1, -1, -1], norm: [-1,  0,  0], uv: [0, 1], },
        { pos: [-1, -1,  1], norm: [-1,  0,  0], uv: [1, 1], },
        { pos: [-1,  1, -1], norm: [-1,  0,  0], uv: [0, 0], },
    
        { pos: [-1,  1, -1], norm: [-1,  0,  0], uv: [0, 0], },
        { pos: [-1, -1,  1], norm: [-1,  0,  0], uv: [1, 1], },
        { pos: [-1,  1,  1], norm: [-1,  0,  0], uv: [1, 0], },
        // top
        { pos: [ 1,  1, -1], norm: [ 0,  1,  0], uv: [0, 1], },
        { pos: [-1,  1, -1], norm: [ 0,  1,  0], uv: [1, 1], },
        { pos: [ 1,  1,  1], norm: [ 0,  1,  0], uv: [0, 0], },
    
        { pos: [ 1,  1,  1], norm: [ 0,  1,  0], uv: [0, 0], },
        { pos: [-1,  1, -1], norm: [ 0,  1,  0], uv: [1, 1], },
        { pos: [-1,  1,  1], norm: [ 0,  1,  0], uv: [1, 0], },
        // bottom
        { pos: [ 1, -1,  1], norm: [ 0, -1,  0], uv: [0, 1], },
        { pos: [-1, -1,  1], norm: [ 0, -1,  0], uv: [1, 1], },
        { pos: [ 1, -1, -1], norm: [ 0, -1,  0], uv: [0, 0], },
    
        { pos: [ 1, -1, -1], norm: [ 0, -1,  0], uv: [0, 0], },
        { pos: [-1, -1,  1], norm: [ 0, -1,  0], uv: [1, 1], },
        { pos: [-1, -1, -1], norm: [ 0, -1,  0], uv: [1, 0], },
      ];
      const positions = [];
      const normals = [];
      const uvs = [];
      const colors = [];
      for (const vertex of vertices) {
        positions.push(...vertex.pos);
        normals.push(...vertex.norm);
        uvs.push(...vertex.uv);
        colors.push(Math.random(), Math.random(), Math.random());
      }
    
      const geometry = new THREE.BufferGeometry();
      const positionNumComponents = 3;
      const normalNumComponents = 3;
      const uvNumComponents = 2;
      const colorNumComponents = 3;
      geometry.setAttribute(
          'position',
          new THREE.BufferAttribute(new Float32Array(positions), positionNumComponents));
      geometry.setAttribute(
          'normal',
          new THREE.BufferAttribute(new Float32Array(normals), normalNumComponents));
      geometry.setAttribute(
          'uv',
          new THREE.BufferAttribute(new Float32Array(uvs), uvNumComponents));
      geometry.setAttribute(
          'color',
          new THREE.BufferAttribute(new Float32Array(colors), colorNumComponents));
    
      const material = new THREE.MeshPhongMaterial({
        vertexColors: true,
      });
    
      const cube = new THREE.Mesh(geometry, material);
      scene.add(cube);
    
      function resizeRendererToDisplaySize(renderer) {
        const canvas = renderer.domElement;
        const width = canvas.clientWidth;
        const height = canvas.clientHeight;
        const needResize = canvas.width !== width || canvas.height !== height;
        if (needResize) {
          renderer.setSize(width, height, false);
        }
        return needResize;
      }
    
      function render(time) {
        time *= 0.001;
    
        if (resizeRendererToDisplaySize(renderer)) {
          const canvas = renderer.domElement;
          camera.aspect = canvas.clientWidth / canvas.clientHeight;
          camera.updateProjectionMatrix();
        }
    
        cube.rotation.x = time;
        cube.rotation.y = time;
    
        renderer.render(scene, camera);
    
        requestAnimationFrame(render);
      }
    
      requestAnimationFrame(render);
    }
    
    main();
    </script>