最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - How to correctly pass mouse coordinates to WebGL? - Stack Overflow

programmeradmin7浏览0评论

I want to pass canvas mouse coordinates to a function that interactively generates a circle with the mouse's coordinates as its center. Therefore, I'm using the following function to normalize:

var mousePositionX = (2*ev.clientX/canvas.width) - 1;
var mousePositionY = (2*ev.clientY/(canvas.height*-1)) + 1;

However, this works fine only for the screen center. When moving the mouse around the cursor is not located in the circle's center any more: see the picture here

The farther the mouse cursor removes from the screen center, the more it is dislocated from the circle's center. Here's some relevant code:

HTML

  body {
    border: 0;
    margin: 0;
  }
  /* make the canvas the size of the viewport */
  canvas {
    width: 100vw;
    height: 100vh;
    display: block;
  }
  ...
  <body onLoad="main()">
        <canvas id="glContext"></canvas>
  </body>

SHADER

<script id="vShaderCircle" type="notjs">
    attribute vec4 a_position;
    uniform mat4 u_viewMatrix;

    void main(){
        gl_Position = u_viewMatrix * a_position;
    }
</script>

JS

function main(){

    // PREPARING CANVAS AND WEBGL-CONTEXT
    var canvas = document.getElementById("glContext");
    var gl_Original = initWebGL(canvas);
    var gl = WebGLDebugUtils.makeDebugContext(gl_Original);

    resize(canvas);
    gl.viewport(0, 0, canvas.width, canvas.height);
    // ----------------------------------
    ...
    // MATRIX SETUP
    var viewMatrix = new Matrix4();
      viewMatrix.setPerspective(30, canvas.width/canvas.height, 1, 100);
      viewMatrix.lookAt(0, 0, 5, 0, 0, 0, 0, 1, 0);
    // ----------------------------------
    canvas.addEventListener("mousemove", function(){stencilTest(event)});

    function stencilTest(ev){
        var mousePositionX = (2*ev.clientX/canvas.width) - 1;
        var mousePositionY = (2*ev.clientY/(canvas.height*(-1))) + 1;
        ...
        ...
        drawCircle(..., mousePositionX, mousePositionY, viewMatrix);
        ...
        drawCube(...);
    }
}

How can I resolve this?

I want to pass canvas mouse coordinates to a function that interactively generates a circle with the mouse's coordinates as its center. Therefore, I'm using the following function to normalize:

var mousePositionX = (2*ev.clientX/canvas.width) - 1;
var mousePositionY = (2*ev.clientY/(canvas.height*-1)) + 1;

However, this works fine only for the screen center. When moving the mouse around the cursor is not located in the circle's center any more: see the picture here

The farther the mouse cursor removes from the screen center, the more it is dislocated from the circle's center. Here's some relevant code:

HTML

  body {
    border: 0;
    margin: 0;
  }
  /* make the canvas the size of the viewport */
  canvas {
    width: 100vw;
    height: 100vh;
    display: block;
  }
  ...
  <body onLoad="main()">
        <canvas id="glContext"></canvas>
  </body>

SHADER

<script id="vShaderCircle" type="notjs">
    attribute vec4 a_position;
    uniform mat4 u_viewMatrix;

    void main(){
        gl_Position = u_viewMatrix * a_position;
    }
</script>

JS

function main(){

    // PREPARING CANVAS AND WEBGL-CONTEXT
    var canvas = document.getElementById("glContext");
    var gl_Original = initWebGL(canvas);
    var gl = WebGLDebugUtils.makeDebugContext(gl_Original);

    resize(canvas);
    gl.viewport(0, 0, canvas.width, canvas.height);
    // ----------------------------------
    ...
    // MATRIX SETUP
    var viewMatrix = new Matrix4();
      viewMatrix.setPerspective(30, canvas.width/canvas.height, 1, 100);
      viewMatrix.lookAt(0, 0, 5, 0, 0, 0, 0, 1, 0);
    // ----------------------------------
    canvas.addEventListener("mousemove", function(){stencilTest(event)});

    function stencilTest(ev){
        var mousePositionX = (2*ev.clientX/canvas.width) - 1;
        var mousePositionY = (2*ev.clientY/(canvas.height*(-1))) + 1;
        ...
        ...
        drawCircle(..., mousePositionX, mousePositionY, viewMatrix);
        ...
        drawCube(...);
    }
}

How can I resolve this?

Share Improve this question edited Feb 22, 2017 at 14:01 Muad asked Feb 18, 2017 at 0:38 MuadMuad 571 gold badge1 silver badge6 bronze badges 3
  • 3 You probably need to post some code. You mentioned you have a view matrix. If you have a view matrix in some ment in whic case you aren't working in WebGL's coordinate system which is clip space. Instead you're working in some other coordinate system you defined and we can't help you convert the mouse coordinates to that space unless you show us teh codez – user128511 Commented Feb 18, 2017 at 13:28
  • @gman So I provided a bit more code now. Can you figure out the problem by that? The individual functions (e.g. matrix functions) do what they're supposed to do. I tried your getRelativeMousePosition() functions as well, but without success... – Muad Commented Feb 18, 2017 at 15:26
  • updated my answer – user128511 Commented Feb 18, 2017 at 15:47
Add a ment  | 

3 Answers 3

Reset to default 14

This is actually a far more plicated issue than it sounds. Is your canvas's display size the same as its drawing buffer? Do you have a border on your canvas?

Here's some code that will give you a canvas relative pixel coordinate assuming you don't have a border or any padding on your canvas.

function getRelativeMousePosition(event, target) {
  target = target || event.target;
  var rect = target.getBoundingClientRect();

  return {
    x: event.clientX - rect.left,
    y: event.clientY - rect.top,
  }
}

// assumes target or event.target is canvas
function getNoPaddingNoBorderCanvasRelativeMousePosition(event, target) {
  target = target || event.target;
  var pos = getRelativeMousePosition(event, target);

  pos.x = pos.x * target.width  / target.clientWidth;
  pos.y = pos.y * target.height / target.clientHeight;

  return pos;  
}

To convert that to a WebGL coordinate

  var pos = getRelativeMousePosition(event, target);
  const x = pos.x / gl.canvas.width  *  2 - 1;
  const y = pos.y / gl.canvas.height * -2 + 1;

Working example:

function getRelativeMousePosition(event, target) {
  target = target || event.target;
  var rect = target.getBoundingClientRect();

  return {
    x: event.clientX - rect.left,
    y: event.clientY - rect.top,
  }
}

// assumes target or event.target is canvas
function getNoPaddingNoBorderCanvasRelativeMousePosition(event, target) {
  target = target || event.target;
  var pos = getRelativeMousePosition(event, target);

  pos.x = pos.x * target.width  / target.clientWidth;
  pos.y = pos.y * target.height / target.clientHeight;

  return pos;  
}

const vs = `
attribute vec4 position;
void main() {
  gl_Position = position;
  gl_PointSize = 20.;
}
`;
const fs = `
void main() {
  gl_FragColor = vec4(1,0,1,1);
}
`;
const gl = document.querySelector("canvas").getContext("webgl");
// piles and links shaders and assigns position to location 
const program = twgl.createProgramFromSources(gl, [vs, fs]);
const positionLoc = gl.getAttribLocation(program, "position");

window.addEventListener('mousemove', e => {

  const pos = getNoPaddingNoBorderCanvasRelativeMousePosition(e, gl.canvas);

  // pos is in pixel coordinates for the canvas.
  // so convert to WebGL clip space coordinates
  const x = pos.x / gl.canvas.width  *  2 - 1;
  const y = pos.y / gl.canvas.height * -2 + 1;

  gl.clear(gl.COLOR_BUFFER_BIT);
  gl.useProgram(program);
  // only drawing a single point so no need to use a buffer
  gl.vertexAttrib2f(positionLoc, x, y);
  gl.drawArrays(gl.POINTS, 0, 1);
});
canvas { 
  display: block;
  width: 400px;
  height: 100px;
}
div {
  display: inline-block;
  border: 1px solid black;
}
<div><canvas></canvas></div>
<p>move the mouse over the canvas</p>
<script src="https://twgljs/dist/3.x/twgl-full.min.js"></script>

Notice there's no matrices involved. If you're using matrices then you've defined your own space, not WebGL's space which is always clip space. In that case you either need to multiply by the inverse of your matrices and pick whatever Z value you want between -1 and +1. That way when your position is multiplied by the matrices used in your shader it will reverse the position back into the correct webgl clip space coordinates. Or, you need to get rid of your matrices or set them in the identity.

Here's an example, note I don't have/know your math library so you'll have to translate to your's

function getRelativeMousePosition(event, target) {
  target = target || event.target;
  var rect = target.getBoundingClientRect();

  return {
    x: event.clientX - rect.left,
    y: event.clientY - rect.top,
  }
}

// assumes target or event.target is canvas
function getNoPaddingNoBorderCanvasRelativeMousePosition(event, target) {
  target = target || event.target;
  var pos = getRelativeMousePosition(event, target);

  pos.x = pos.x * target.width  / target.clientWidth;
  pos.y = pos.y * target.height / target.clientHeight;

  return pos;  
}

const vs = `
attribute vec4 position;
uniform mat4 matrix;
void main() {
  gl_Position = matrix * position;
}
`;
const fs = `
void main() {
  gl_FragColor = vec4(1,0,0,1);
}
`;
const m4 = twgl.m4;
const gl = document.querySelector("canvas").getContext("webgl");
const programInfo = twgl.createProgramInfo(gl, [vs, fs]);
const bufferInfo = twgl.primitives.createSphereBufferInfo(gl, .5, 12, 8);

window.addEventListener('mousemove', e => {

  const pos = getNoPaddingNoBorderCanvasRelativeMousePosition(e, gl.canvas);

  // pos is in pixel coordinates for the canvas.
  // so convert to WebGL clip space coordinates
  const x = pos.x / gl.canvas.width  *  2 - 1;
  const y = pos.y / gl.canvas.height * -2 + 1;
  
  // use a projection and view matrix
  const projection = m4.perspective(
     30 * Math.PI / 180, 
     gl.canvas.clientWidth / gl.canvas.clientHeight, 
     1, 
     100);
  const camera = m4.lookAt([0, 0, 15], [0, 0, 0], [0, 1, 0]);
  const view = m4.inverse(camera);
  const viewProjection = m4.multiply(projection, view);
  
  // pick a clipsace Z value between -1 and 1 
  // we'll zNear to zFar and convert back to clip space
  const viewZ = -5;  // 5 units back from the camera
  const clip = m4.transformPoint(projection, [0, 0, viewZ]);
  const z = clip[2];
  
  // pute the world space position needed to put the sphere
  // under the cursor at this clipspace position
  const inverseViewProjection = m4.inverse(viewProjection);
  const worldPos = m4.transformPoint(inverseViewProjection, [x, y, z]);

  // add that world position to our matrix
  const mat = m4.translate(viewProjection, worldPos);

  gl.clear(gl.COLOR_BUFFER_BIT);
  gl.useProgram(programInfo.program);
  
  twgl.setBuffersAndAttributes(gl, programInfo, bufferInfo);
  twgl.setUniforms(programInfo, {
    matrix: mat,
  });
  gl.drawElements(gl.LINES, bufferInfo.numElements, gl.UNSIGNED_SHORT, 0);
});
canvas { 
  display: block;
  width: 400px;
  height: 100px;
}
div {
  display: inline-block;
  border: 1px solid black;
}
<div><canvas></canvas></div>
<p>move the mouse over the canvas</p>
<script src="https://twgljs/dist/3.x/twgl-full.min.js"></script>

Also note I deliberately made the canvas's display size not match it's drawing buffer size to show the math works.

Just bind your mousemove event to the canvas itself and use offsetX and offsetY

var mouseX = (e.offsetX / canvas.clientWidth)*2-1;
var mouseY = ((canvas.clientHeight - e.offsetY) / canvas.clientHeight)*2-1;

Note that this all depends on what transforms you do in your shaders.

Assuming this is called in a mousemove callback event and canvas is defined as a proper reference to an HTML CANVAS element, the relative position of the mouse pointer to the canvas space should be:

var rect = gl.canvas.getBoundingClientRect();
var mousePositionX = ev.clientX - rect.left;
var mousePositionY = ev.clientY - rect.top;

To convert from pixel coordinates to WebGL coordinate system:

var rect = gl.canvas.getBoundingClientRect();
var x = (ev.clientX - rect.left) / canvas.width *  2 - 1;
var y = (ev.clientY - rect.top) / canvas.height * -2 + 1;
发布评论

评论列表(0)

  1. 暂无评论