Search code examples
javascriptthree.jsshaderfragment-shader

Mouse offset in threejs + fragment shader


Following up from this question, I'm trying to get my mouse cursor to update a Voronoi shader from the Book of Shaders.

However, I think I might be misunderstanding something with the offsets. As it stands, my mouse is slightly offset to the right with my current code (see below).

I've attempted offsetting from the bounding rectangle of my renderer.domElement, but the top/left values were 0 and had no impact. Simply trying e.pageX / window.innerWidth; e.pageY / window.innerHeight; left the y-position mirrored and significantly off.

I also tried the fairly standard mapping to [-1,1] with offsetX and whatnot (commented) and that was actually worse off. Also attempted using unproject from another SO post I found, but that didn't appear to have any effect either.

Here's a link to a jsfiddle with the same issues I'm seeing: https://jsfiddle.net/9y8hge3t/1/

And the full code for historical purposes:

<!--
  * Based on Book of Shaders 12:
  https://thebookofshaders.com/12/
-->

<!DOCTYPE HTML>
<html>

<head>
  <title>WebGL Demo - Voronoi (Mouse Move)</title>
  <meta charset="utf-8">
  <style>
    body {
      margin: 0;
      padding: 0;
      overflow: hidden;
    }
  </style>

  <script src="./libraries/threejs/three.min.js"></script>
  <!-- shaders -->
  <script type="x-shader/x-vertex" id="vertexShader">
    void main() {
      //gl_Position = vec4(position, 1.0);
      vec4 modelViewPosition = modelViewMatrix * vec4(position, 1.0);
      gl_Position = projectionMatrix * modelViewPosition;
    }
  </script>
  <script type="x-shader/x-fragment" id="fragmentShader">
    uniform vec2 u_resolution;
    uniform vec2 u_mouse;
    uniform float u_time;

    void main() {
      vec2 st = gl_FragCoord.xy/u_resolution.xy;
      st.x *= u_resolution.x/u_resolution.y;

      vec3 color = vec3(.0);

      // Cell positions
      vec2 point[5];
      point[0] = vec2(0.83,0.75);
      point[1] = vec2(0.60,0.07);
      point[2] = vec2(0.28,0.64);
      point[3] =  vec2(0.31,0.26);
      point[4] = u_mouse;

      float m_dist = 1.;  // minimum distance

      // Iterate through the points positions
      for (int i = 0; i < 5; i++) {
          float dist = distance(st, point[i]);

          // Keep the closer distance
          m_dist = min(m_dist, dist);
      }

      // Draw the min distance (distance field)
      color += m_dist;

      // Show isolines
      // color -= step(.7,abs(sin(50.0*m_dist)))*.3;

      gl_FragColor = vec4(color,1.0);
    }
  </script>
</head>

<body></body>

<script>
  let camera, scene, renderer;
  let uniforms, mesh;

  init();
  animate();

  function init() {
    scene = new THREE.Scene();

    // 2D perspective camera -- see linked article on top for full explanation of params
    camera = new THREE.Camera();
    camera.position.z = 1;
    /*
    camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 100);
    camera.position.set(0, 0, 1);
    */
    camera.lookAt(scene.position);
    scene.add(camera);

    renderer = new THREE.WebGLRenderer({ antialias: true });
    renderer.setClearColor(0x000000, 1);
    renderer.setSize(window.innerWidth, window.innerHeight);
    renderer.setPixelRatio(window.devicePixelRatio);
    document.body.appendChild(renderer.domElement);

    uniforms = {
      u_resolution: { type: 'vec2', value: new THREE.Vector2() },
      u_mouse: { type: 'vec2', value: new THREE.Vector2() },
      u_time: { type: 'float', value: 1.0 }
    };

    let vShader = document.getElementById("vertexShader").textContent;
    let fShader = document.getElementById("fragmentShader").textContent;

    let geometry = new THREE.PlaneGeometry(2, 2);

    // give it a material
    let material = new THREE.ShaderMaterial({
      uniforms: uniforms,
      fragmentShader: fShader,
      vertexShader: vShader,
    });

    // and now create the mesh (geom+mat)
    mesh = new THREE.Mesh(geometry, material);
    // mesh.position.set(0, 0, 0);//-1.5, 0.0, 4.0);
    scene.add(mesh);

    onWindowResize();
    window.addEventListener('resize', onWindowResize, false);
    renderer.domElement.addEventListener('mousemove', onDocumentMouseMove, false);
  }

  function animate() {
    requestAnimationFrame(animate);
    render();
  }

  function render() {
    uniforms.u_time.value += 0.05;
    renderer.render(scene, camera);
  }

  function onWindowResize(e) {
    renderer.setSize(window.innerWidth, window.innerHeight);
    uniforms.u_resolution.value.x = renderer.domElement.width;
    uniforms.u_resolution.value.y = renderer.domElement.height;
  }

  function onDocumentMouseMove(e) {
    // uniforms.u_mouse.value.x = (e.offsetX / window.innerWidth) * 2 - 1;//e.pageX / window.innerWidth;
    // uniforms.u_mouse.valye.y = -(e.offsetY / window.innerHeight) * 2 + 1;//e.pageY / window.innerHeight;

    uniforms.u_mouse.value.x = (e.offsetX / window.innerWidth)*2;
    uniforms.u_mouse.value.y = -(e.offsetY / window.innerHeight)*2+1;
  }

</script>
</html>

Solution

  • Something worth understanding is that your fragment shader coordinates want to be in the [0, 1] range. That's what you're doing with this line:

    vec2 st = gl_FragCoord.xy/u_resolution.xy;

    Now, I'm not sure why you're multiplying everything by 2. This gives you [0, 2] range, which is the source of your unwanted offset.

    // Wrong approach
    uniforms.u_mouse.value.x = (e.offsetX / window.innerWidth)*2;
    uniforms.u_mouse.value.y = -(e.offsetY / window.innerHeight)*2+1;
    

    Instead, just remove the *2. Since y is inverted in texture coordinates, you'll have to do 1 - y:

    // Correct approach
    uniforms.u_mouse.value.x = (e.offsetX / window.innerWidth);
    uniforms.u_mouse.value.y = 1-(e.offsetY / window.innerHeight);
    

    Additionally, since the browser window can narrow or widen upon resize, you're adjusting the x-coordinate in the shader with:

    st.x *= u_resolution.x/u_resolution.y;

    This could give you greater than 1 x-range if it's a wide landscape ratio, or less than 1 if it's a narrow portrait ratio. So you'll have to account for this also in your mouse coordinates values:

    const vpRatio = window.innerWidth / window.innerHeight;
    
    uniforms.u_mouse.value.x = (e.offsetX / window.innerWidth) * vpRatio;
    uniforms.u_mouse.value.y = 1-(e.offsetY / window.innerHeight);
    

    And there you go! Matching mouse / fragment coordinates! Here's your demo with the necessary adjustments