2D zoom to point in webgl

北慕城南 提交于 2020-01-01 14:39:47

问题


I'm trying to create a 2D graph visualization using WebGL (regl, to be more specific). With my current implementation I can already see the force layout being applied to each node, which is good. The problem comes when I try to zoom with respect to the current mouse position. According to my research, to achieve this behavior, it is necessary to apply matrix transformations in the following order:

translate(nodePosition, mousePosition)
scale(scaleFactor)
translate(nodePosition, -mousePosition)

So, every time the wheel event is fired, the mouse position is recalculated and the transform matrix is updated with the new mouse position information. The current behavior is weird and I can't seem to understand what is wrong. Here is a live example.

Apparently, if I zoom in and out with the mouse fixed at the initial position, everything works just fine. However, if I move the mouse and try to focus on another node, then it fails.

The function for retrieving the mouse position is:

const getMousePosition = (event) => {
    var canvas = event.currentTarget
    var rect = canvas.getBoundingClientRect()
    var x = event.clientX - rect.left
    var y = event.clientY - rect.top
    var projection = mat3.create()
    var pos = vec2.fromValues(x,y)
    // this converts the mouse coordinates from 
    // pixel space to WebGL clipspace
    mat3.projection(projection, canvas.clientWidth, canvas.clientHeight)
    vec2.transformMat3(pos, pos, projection)
    return(pos)
}

The wheel event listener callback:

var zoomFactor = 1.0
var mouse = vec2.fromValues(0.0, 0.0)
options.canvas.addEventListener("wheel", (event) => {
    event.preventDefault()
    mouse = getMousePosition(event)
    var direction = event.deltaY < 0 ? 1 : -1
    zoomFactor = 1 + direction * 0.1
    updateTransform()
})

And the function that updates the transform:

var transform = mat3.create()
function updateTransform() {
    var negativeMouse = vec2.create()
    vec2.negate(negativeMouse, mouse)
    mat3.translate(transform, transform, mouse)
    mat3.scale(transform, transform, [zoomFactor, zoomFactor])
    mat3.translate(transform, transform, negativeMouse)
}

This transform matrix is made available as an uniform in the vertex shader:

  precision highp float;
  attribute vec2 position;

  uniform mat3 transform;

  uniform float stageWidth;
  uniform float stageHeight;

  vec2 normalizeCoords(vec2 position) {
    float x = (position[0]+ (stageWidth  / 2.0));
    float y = (position[1]+ (stageHeight / 2.0));

    return vec2(
        2.0 * ((x / stageWidth ) - 0.5),
      -(2.0 * ((y / stageHeight) - 0.5))
    );
  }

  void main () {
    gl_PointSize = 7.0;
    vec3 final = transform * vec3(normalizeCoords(position), 1);
    gl_Position = vec4(final.xy, 0, 1);
  }

where, position is the attribute holding the node position.

What I've tried, so far:

  • I already tried changing the order of the transformations. The result is even weirder.
  • When I apply either translation or scaling independently, everything looks ok.

This is my first interaction with something that is not the usual SVG/canvas stuff. The solution is probably obvious, but I really don't know where to look anymore. What am I doing wrong?

Update 06/11/2018

I followed @Johan's suggestions and implemented it on the live demo. Although the explanation was rather convincing, the result is not quite what I was expecting. The idea of inverting the transform to get the mouse position in the model space makes sense to me, but my intuition (which is probably wrong) says that applying the transform directly on the screen space should also work. Why can't I project both the nodes and the mouse in the screen space and apply the transform directly there?

Update 07/11/2018

After struggling a little, I decided to take a different approach and adapt the solution from this answer for my use case. Although things are working as expected for the zoom (with the addition of panning as well), I still believe there are solutions that do not depend on d3-zoom at all. Maybe isolating the view matrix and controlling it independently to achieve the expected behavior, as suggested in the comments. To see my current solution, check my answer bellow.


回答1:


Alright, after failing with the original approach, I managed to make this solution work for my use case.

The updateTransform function is now:

var transform = mat3.create();
function updateTransform(x, y, scale) {
    mat3.projection(transform, options.canvas.width, options.canvas.height);
    mat3.translate(transform, transform, [x,y]);
    mat3.scale(transform, transform, [scale,scale]);
    mat3.translate(transform, transform, [
      options.canvas.width / 2,
      options.canvas.height / 2
    ]);
    mat3.scale(transform, transform, [
      options.canvas.width / 2,
      options.canvas.height / 2
    ]);
    mat3.scale(transform, transform, [1, -1]);
}

And is called by d3-zoom:

import { zoom as d3Zoom } from "d3-zoom";
import { select } from "d3-selection";

var zoom = d3Zoom();

d3Event = () => require("d3-selection").event;

select(options.canvas)
      .call(zoom.on("zoom", () => {
          var t = d3Event().transform
          updateTransform(t.x, t.y, t.k)
       }));

Here is the live demonstration with this solution.




回答2:


Your transformation maps the model to the target view-port. If you want to correct for a translation due to scaling (say delta), which is a distance in target coordinates, you need to transform this delta in model coordinates. That is, determine the inverse of your transformation and calculate with that the correction in model coordinates

Simple example preparing the transformation for scaling around a center in view-port coordinates is given below:

function map (a, p) {
    return [a[0] * p[0] + a[3] * p[1] + a[6],a[1] * p[0] + a[4] * p[1] + a[7]];
}

function scale(transform,scale,viewCenter1) {    
    var inverted = mat3.create();
    mat3.invert(inverted,transform);
    var modelCenter1 = map(inverted,viewCenter1);   // scale from this point in model

    mat3.scale(transform,transform,[scale,scale]);
    var viewCenter2 = map(transform,modelCenter1);  // map model center to screen 
    var viewShift = [viewCenter1[0]-viewCenter2[0],viewCenter1[1]-viewCenter2[1]];

    mat3.invert(inverted,transform);
    var modelShift = map(inverted,viewShift) - map(inverted,[0,0]);
    mat3.translate(transform,[-modelShift[0],-modelShift[1]]);   // correct for the shift
}

// pass the transformation to webgl environment


来源:https://stackoverflow.com/questions/53163241/2d-zoom-to-point-in-webgl

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!