Search code examples
javascriptthree.jsglslshaderwebgl

How to mix / blend between 2 vertex positions based on distance from camera?


I'm trying to mix / blend between 2 different vertex positions depending on the distance from the camera. Specifically, I'm trying to create an effect that blends between a horizontal plane closer to the camera and a vertical plane in the distance. The result should be a curved plane going away and up from the current camera position.

I want to blend from this (a plane flat on the ground): enter image description here

To this (the same plane, just rotated 90 degrees): enter image description here

The implementation I have so far feels close but I just can't put my finger on what pieces I need to finish it. I took an approach from a similar Tangram demo (shader code), however I'm unable to get results anywhere near this. The Tangram example is also using a complete different setup to what I'm using in Three.js so I've not been able to replicate everything.

This is what I have so far: https://jsfiddle.net/robhawkes/a97tu864/

varying float distance;

mat4 rotateX(float rotationX) {
  return mat4(
    vec4(1.0,0.0,0.0,0.0),
    vec4(0.0,cos(rotationX),-sin(rotationX),0.0),
    vec4(0.0,sin(rotationX),cos(rotationX),0.0),
    vec4(0.0,0.0,0.0,1.0)
  );
}

void main() 
{
  vec4 vPosition = vec4(position, 1.0);
  vec4 modelViewPosition = modelViewMatrix * vPosition;

  float bend = radians(-90.0);
  vec4 newPos = rotateX(bend) * vPosition;

  distance = -modelViewPosition.z;

  // Show bent position
  //gl_Position = projectionMatrix * modelViewMatrix * newPos;

  float factor = 0.0;

  //if (vPosition.x > 0.0) {
  //    factor = 1.0;
  //}

  //factor = clamp(0.0, 1.0, distance / 2000.0);

  vPosition = mix(vPosition, newPos, factor);

  gl_Position = projectionMatrix * modelViewMatrix * vPosition;
}

I'm doing the following:

  • Calculate the rotated position of the vertex (the vertical version)
  • Find the distance from the vertex to the camera
  • Use mix to blend between the horizontal position and vertical position depending on the distance

I've tried multiple approaches and I just can't seem to get it to work correctly.

Any ideas? Even pointing me down the right path will be immensely helpful as my shader/matrix knowledge is limited.


Solution

  • The major issue is, that you tessellate the THREE.PlaneBufferGeometry in width segments, but not in height segments:

    groundGeometry = new THREE.PlaneBufferGeometry( 
        1000, 10000, 
        100,    // <----- widthSegments 
        100 );  // <----- heightSegments is missing
    

    Now you can use the z coordinate of the view space for the interpolation:

    float factor = -modelViewPosition.z / 2000.0;
    

    var camera, controls, scene, renderer;
    var groundGeometry, groundMaterial, groundMesh;
    var ambientLight;
    
    init();
    initLight();
    initGround();
    animate();
    
    function init() {
     
      camera = new THREE.PerspectiveCamera( 70, window.innerWidth / window.innerHeight, 0.01, 10000 );
      camera.position.y = 500;
      camera.position.z = 1000;
      
      controls = new THREE.MapControls( camera );
      controls.maxPolarAngle = Math.PI / 2;
    
      scene = new THREE.Scene();
      
      scene.add(camera);
      
      var axesHelper = new THREE.AxesHelper( 500 );
    	scene.add( axesHelper );
    
      renderer = new THREE.WebGLRenderer( { antialias: true } );
      renderer.setSize( window.innerWidth, window.innerHeight );
            
      document.body.appendChild( renderer.domElement );
     
    }
    
    function initLight() {
      ambientLight = new THREE.AmbientLight( 0x404040 );
    	scene.add( ambientLight );
    }
    
    function initGround() {
    	groundMaterial = new THREE.ShaderMaterial({
        vertexShader: document.getElementById( 'vertexShader' ).textContent,
        fragmentShader: document.getElementById( 'fragmentShader' ).textContent,
        transparent: true
      });
      
      groundGeometry = new THREE.PlaneBufferGeometry( 1000, 10000, 100, 100 );
      groundMesh = new THREE.Mesh( groundGeometry, groundMaterial );
      groundMesh.position.z = -3000;
      groundMesh.position.y = -100;
      groundMesh.rotateX(-Math.PI / 2)
      scene.add( groundMesh );
    }
    
    function animate() {
     
      requestAnimationFrame( animate );
      
      controls.update();
      
      renderer.render( scene, camera );
     
    }
    <script type="x-shader/x-vertex" id="vertexShader">
    varying float distance;
    
    mat4 rotateX(float rotationX) {
      return mat4(
        vec4(1.0,0.0,0.0,0.0),
        vec4(0.0,cos(rotationX),-sin(rotationX),0.0),
        vec4(0.0,sin(rotationX),cos(rotationX),0.0),
        vec4(0.0,0.0,0.0,1.0)
      );
    }
    
    void main() 
    {
      vec4 vPosition = vec4(position, 1.0);
      vec4 modelViewPosition = modelViewMatrix * vPosition;
        
      float bend = radians(-90.0);
      vec4 newPos = rotateX(bend) * vPosition;
      
      distance = -modelViewPosition.z;
      
      float factor = -modelViewPosition.z / 2000.0;
      
      vPosition = mix(vPosition, newPos, factor);
      
      gl_Position = projectionMatrix * modelViewMatrix * vPosition;
    }
    </script>
    <script type="x-shader/x-fragment" id="fragmentShader">
    varying float distance;
    
    void main() {
      if (distance < 3000.0) {
        gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
      } else {
        gl_FragColor = vec4(0.0, 1.0, 0.0, 1.0);
      }
    }
    </script>
    
    <script src="https://threejs.org/build/three.min.js"></script>
    
    <script src="https://rawgit.com/mrdoob/three.js/dev/examples/js/controls/MapControls.js"></script>