Reputation: 57
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?
Upvotes: 3
Views: 11809
Reputation: 1620
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;
Upvotes: 0
Reputation:
This is actually a far more complicated 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");
// compiles 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.org/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];
// compute 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.org/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.
Upvotes: 14
Reputation: 8123
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.
Upvotes: 1