WebGL 2D 相机缩放到鼠标点
WebGL 2D camera zoom to mouse point
我目前正在使用 WebGL 构建 2D 绘图应用程序。我想实现缩放以指向鼠标光标下方,类似于 中的示例。但我无法弄清楚如何在我的案例中应用该答案的解决方案。
我已经通过缩放相机矩阵完成了基本的缩放。但它会缩放到 canvas 的左上角,因为这是投影设置的原点 (0,0)(据我所知)。
已实现基本平移和缩放:
我的绘图函数(包括矩阵计算)如下所示:
var projection = null;
var view = null;
var viewProjection = null;
function draw(gl, camera, sceneTree){
// projection matrix
projection = new Float32Array(9);
mat3.projection(projection, gl.canvas.clientWidth, gl.canvas.clientHeight);
// camera matrix
view = new Float32Array(9);
mat3.fromTranslation(view, camera.translation);
mat3.rotate(view, view, toRadians(camera.rotation));
mat3.scale(view, view, camera.scale);
// view matrix
mat3.invert(view, view)
// VP matrix
viewProjection = new Float32Array(9);
mat3.multiply(viewProjection, projection, view);
// go through scene tree:
// - build final matrix for each object
// e.g: u_matrix = VP x Model (translate x rotate x scale)
// draw each object in scene tree
// ...
}
顶点着色器:
attribute vec2 a_position;
uniform mat3 u_matrix;
void main() {
gl_Position = vec4((u_matrix * vec3(a_position, 1)).xy, 0, 1);
}
缩放功能:
function screenToWorld(screenPos){
// normalized screen position
let nsp = [
2.0 * screenPos[0] / this.gl.canvas.width - 1,
- 2.0 * screenPos[1] / this.gl.canvas.height + 1
];
let inverseVP = new Float32Array(9);
mat3.invert(inverseVP, viewProjection);
let worldPos = [0, 0];
return vec2.transformMat3(worldPos, nsp, inverseVP);
}
var zoomRange = [0.01, 2];
canvas.addEventListener('wheel', (e) => {
let oldZoom = camera.scale[0];
let zoom = Math.min(Math.max(oldZoom + e.deltaX / 100, zoomRange[0]), zoomRange[1]);
camera.scale = [zoom, zoom];
let zoomPoint = screenToWorld([e.clientX, e.clientY]);
// totally breaks if enable this line
//vec2.copy(camera.translation, zoomPoint);
// call draw function again
draw();
}, false);
如果我将 zoomPoint
应用于相机平移,zoomPoint
的值(以及相应的相机位置)开始随着每个缩放事件不受控制地升高(不管我放大或缩小) 并且场景中绘制的对象立即消失。
非常感谢任何关于我在这里做错了什么的见解或建议。谢谢。
由于您没有post问题本身中的最小可重现示例,所以我无法使用您的数学库进行测试。使用我自己的虽然我能够像这样缩放
const [clipX, clipY] = getClipSpaceMousePosition(e);
// position before zooming
const [preZoomX, preZoomY] = m3.transformPoint(
m3.inverse(viewProjectionMat),
[clipX, clipY]);
// multiply the wheel movement by the current zoom level
// so we zoom less when zoomed in and more when zoomed out
const newZoom = camera.zoom * Math.pow(2, e.deltaY * -0.01);
camera.zoom = Math.max(0.02, Math.min(100, newZoom));
updateViewProjection();
// position after zooming
const [postZoomX, postZoomY] = m3.transformPoint(
m3.inverse(viewProjectionMat),
[clipX, clipY]);
// camera needs to be moved the difference of before and after
camera.x += preZoomX - postZoomX;
camera.y += preZoomY - postZoomY;
请注意,缩放与缩放相反。如果 zoom = 2 那么我希望一切看起来都大 2 倍。为此需要 缩小 相机 space 所以我们将 space 缩放 1 / zoom
示例:
const canvas = document.querySelector('canvas');
const gl = canvas.getContext('webgl');
const vs = `
attribute vec2 a_position;
uniform mat3 u_matrix;
void main() {
gl_Position = vec4((u_matrix * vec3(a_position, 1)).xy, 0, 1);
}
`;
const fs = `
precision mediump float;
uniform vec4 u_color;
void main() {
gl_FragColor = u_color;
}
`;
// compiles shaders, links program, looks up locations
const programInfo = twgl.createProgramInfo(gl, [vs, fs]);
// calls gl.createBuffer, gl.bindBuffer, gl.bufferData
const bufferInfo = twgl.createBufferInfoFromArrays(gl, {
a_position: {
numComponents: 2,
data: [
0, 0, // 0----1
40, 0, // | |
40, 10, // | 3--2
10, 10, // | |
10, 20, // | 4-5
30, 20, // | |
30, 30, // | 7-6
10, 30, // | |
10, 50, // 9-8
0, 50,
],
},
indices: [
0, 1, 2,
0, 2, 3,
0, 3, 8,
0, 8, 9,
4, 5, 6,
4, 6, 7,
],
});
const camera = {
x: 0,
y: 0,
rotation: 0,
zoom: 1,
};
const scene = [
{ x: 20, y: 20, rotation: 0, scale: 1, color: [1, 0, 0, 1], bufferInfo},
{ x: 100, y: 50, rotation: Math.PI, scale: 0.5, color: [0, 0.5, 0, 1], bufferInfo},
{ x: 100, y: 50, rotation: 0, scale: 2, color: [0, 0, 1, 1], bufferInfo},
{ x: 200, y: 100, rotation: 0.7, scale: 1, color: [1, 0, 1, 1], bufferInfo},
];
let viewProjectionMat;
function makeCameraMatrix() {
const zoomScale = 1 / camera.zoom;
let cameraMat = m3.identity();
cameraMat = m3.translate(cameraMat, camera.x, camera.y);
cameraMat = m3.rotate(cameraMat, camera.rotation);
cameraMat = m3.scale(cameraMat, zoomScale, zoomScale);
return cameraMat;
}
function updateViewProjection() {
// same as ortho(0, width, height, 0, -1, 1)
const projectionMat = m3.projection(gl.canvas.width, gl.canvas.height);
const cameraMat = makeCameraMatrix();
let viewMat = m3.inverse(cameraMat);
viewProjectionMat = m3.multiply(projectionMat, viewMat);
}
function draw() {
gl.clear(gl.COLOR_BUFFER_BIT);
updateViewProjection();
gl.useProgram(programInfo.program);
for (const {x, y, rotation, scale, color, bufferInfo} of scene) {
// calls gl.bindBuffer, gl.enableVertexAttribArray, gl.vertexAttribPointer
twgl.setBuffersAndAttributes(gl, programInfo, bufferInfo);
let mat = m3.identity();
mat = m3.translate(mat, x, y);
mat = m3.rotate(mat, rotation);
mat = m3.scale(mat, scale, scale);
// calls gl.uniformXXX
twgl.setUniforms(programInfo, {
u_matrix: m3.multiply(viewProjectionMat, mat),
u_color: color,
});
// calls gl.drawArrays or gl.drawElements
twgl.drawBufferInfo(gl, bufferInfo);
}
}
draw();
function getClipSpaceMousePosition(e) {
// get canvas relative css position
const rect = canvas.getBoundingClientRect();
const cssX = e.clientX - rect.left;
const cssY = e.clientY - rect.top;
// get normalized 0 to 1 position across and down canvas
const normalizedX = cssX / canvas.clientWidth;
const normalizedY = cssY / canvas.clientHeight;
// convert to clip space
const clipX = normalizedX * 2 - 1;
const clipY = normalizedY * -2 + 1;
return [clipX, clipY];
}
canvas.addEventListener('wheel', (e) => {
e.preventDefault();
const [clipX, clipY] = getClipSpaceMousePosition(e);
// position before zooming
const [preZoomX, preZoomY] = m3.transformPoint(
m3.inverse(viewProjectionMat),
[clipX, clipY]);
// multiply the wheel movement by the current zoom level
// so we zoom less when zoomed in and more when zoomed out
const newZoom = camera.zoom * Math.pow(2, e.deltaY * -0.01);
camera.zoom = Math.max(0.02, Math.min(100, newZoom));
updateViewProjection();
// position after zooming
const [postZoomX, postZoomY] = m3.transformPoint(
m3.inverse(viewProjectionMat),
[clipX, clipY]);
// camera needs to be moved the difference of before and after
camera.x += preZoomX - postZoomX;
camera.y += preZoomY - postZoomY;
draw();
});
canvas { border: 1px solid black; display: block; }
<canvas></canvas>
<script src="https://twgljs.org/dist/4.x/twgl-full.min.js"></script>
<script src="https://webglfundamentals.org/webgl/resources/m3.js"></script>
请注意,我包含 camera.rotation 只是为了确保旋转后一切正常。他们似乎。 Here's one with zoom, pan, and rotate
我目前正在使用 WebGL 构建 2D 绘图应用程序。我想实现缩放以指向鼠标光标下方,类似于
我已经通过缩放相机矩阵完成了基本的缩放。但它会缩放到 canvas 的左上角,因为这是投影设置的原点 (0,0)(据我所知)。
已实现基本平移和缩放:
我的绘图函数(包括矩阵计算)如下所示:
var projection = null;
var view = null;
var viewProjection = null;
function draw(gl, camera, sceneTree){
// projection matrix
projection = new Float32Array(9);
mat3.projection(projection, gl.canvas.clientWidth, gl.canvas.clientHeight);
// camera matrix
view = new Float32Array(9);
mat3.fromTranslation(view, camera.translation);
mat3.rotate(view, view, toRadians(camera.rotation));
mat3.scale(view, view, camera.scale);
// view matrix
mat3.invert(view, view)
// VP matrix
viewProjection = new Float32Array(9);
mat3.multiply(viewProjection, projection, view);
// go through scene tree:
// - build final matrix for each object
// e.g: u_matrix = VP x Model (translate x rotate x scale)
// draw each object in scene tree
// ...
}
顶点着色器:
attribute vec2 a_position;
uniform mat3 u_matrix;
void main() {
gl_Position = vec4((u_matrix * vec3(a_position, 1)).xy, 0, 1);
}
缩放功能:
function screenToWorld(screenPos){
// normalized screen position
let nsp = [
2.0 * screenPos[0] / this.gl.canvas.width - 1,
- 2.0 * screenPos[1] / this.gl.canvas.height + 1
];
let inverseVP = new Float32Array(9);
mat3.invert(inverseVP, viewProjection);
let worldPos = [0, 0];
return vec2.transformMat3(worldPos, nsp, inverseVP);
}
var zoomRange = [0.01, 2];
canvas.addEventListener('wheel', (e) => {
let oldZoom = camera.scale[0];
let zoom = Math.min(Math.max(oldZoom + e.deltaX / 100, zoomRange[0]), zoomRange[1]);
camera.scale = [zoom, zoom];
let zoomPoint = screenToWorld([e.clientX, e.clientY]);
// totally breaks if enable this line
//vec2.copy(camera.translation, zoomPoint);
// call draw function again
draw();
}, false);
如果我将 zoomPoint
应用于相机平移,zoomPoint
的值(以及相应的相机位置)开始随着每个缩放事件不受控制地升高(不管我放大或缩小) 并且场景中绘制的对象立即消失。
非常感谢任何关于我在这里做错了什么的见解或建议。谢谢。
由于您没有post问题本身中的最小可重现示例,所以我无法使用您的数学库进行测试。使用我自己的虽然我能够像这样缩放
const [clipX, clipY] = getClipSpaceMousePosition(e);
// position before zooming
const [preZoomX, preZoomY] = m3.transformPoint(
m3.inverse(viewProjectionMat),
[clipX, clipY]);
// multiply the wheel movement by the current zoom level
// so we zoom less when zoomed in and more when zoomed out
const newZoom = camera.zoom * Math.pow(2, e.deltaY * -0.01);
camera.zoom = Math.max(0.02, Math.min(100, newZoom));
updateViewProjection();
// position after zooming
const [postZoomX, postZoomY] = m3.transformPoint(
m3.inverse(viewProjectionMat),
[clipX, clipY]);
// camera needs to be moved the difference of before and after
camera.x += preZoomX - postZoomX;
camera.y += preZoomY - postZoomY;
请注意,缩放与缩放相反。如果 zoom = 2 那么我希望一切看起来都大 2 倍。为此需要 缩小 相机 space 所以我们将 space 缩放 1 / zoom
示例:
const canvas = document.querySelector('canvas');
const gl = canvas.getContext('webgl');
const vs = `
attribute vec2 a_position;
uniform mat3 u_matrix;
void main() {
gl_Position = vec4((u_matrix * vec3(a_position, 1)).xy, 0, 1);
}
`;
const fs = `
precision mediump float;
uniform vec4 u_color;
void main() {
gl_FragColor = u_color;
}
`;
// compiles shaders, links program, looks up locations
const programInfo = twgl.createProgramInfo(gl, [vs, fs]);
// calls gl.createBuffer, gl.bindBuffer, gl.bufferData
const bufferInfo = twgl.createBufferInfoFromArrays(gl, {
a_position: {
numComponents: 2,
data: [
0, 0, // 0----1
40, 0, // | |
40, 10, // | 3--2
10, 10, // | |
10, 20, // | 4-5
30, 20, // | |
30, 30, // | 7-6
10, 30, // | |
10, 50, // 9-8
0, 50,
],
},
indices: [
0, 1, 2,
0, 2, 3,
0, 3, 8,
0, 8, 9,
4, 5, 6,
4, 6, 7,
],
});
const camera = {
x: 0,
y: 0,
rotation: 0,
zoom: 1,
};
const scene = [
{ x: 20, y: 20, rotation: 0, scale: 1, color: [1, 0, 0, 1], bufferInfo},
{ x: 100, y: 50, rotation: Math.PI, scale: 0.5, color: [0, 0.5, 0, 1], bufferInfo},
{ x: 100, y: 50, rotation: 0, scale: 2, color: [0, 0, 1, 1], bufferInfo},
{ x: 200, y: 100, rotation: 0.7, scale: 1, color: [1, 0, 1, 1], bufferInfo},
];
let viewProjectionMat;
function makeCameraMatrix() {
const zoomScale = 1 / camera.zoom;
let cameraMat = m3.identity();
cameraMat = m3.translate(cameraMat, camera.x, camera.y);
cameraMat = m3.rotate(cameraMat, camera.rotation);
cameraMat = m3.scale(cameraMat, zoomScale, zoomScale);
return cameraMat;
}
function updateViewProjection() {
// same as ortho(0, width, height, 0, -1, 1)
const projectionMat = m3.projection(gl.canvas.width, gl.canvas.height);
const cameraMat = makeCameraMatrix();
let viewMat = m3.inverse(cameraMat);
viewProjectionMat = m3.multiply(projectionMat, viewMat);
}
function draw() {
gl.clear(gl.COLOR_BUFFER_BIT);
updateViewProjection();
gl.useProgram(programInfo.program);
for (const {x, y, rotation, scale, color, bufferInfo} of scene) {
// calls gl.bindBuffer, gl.enableVertexAttribArray, gl.vertexAttribPointer
twgl.setBuffersAndAttributes(gl, programInfo, bufferInfo);
let mat = m3.identity();
mat = m3.translate(mat, x, y);
mat = m3.rotate(mat, rotation);
mat = m3.scale(mat, scale, scale);
// calls gl.uniformXXX
twgl.setUniforms(programInfo, {
u_matrix: m3.multiply(viewProjectionMat, mat),
u_color: color,
});
// calls gl.drawArrays or gl.drawElements
twgl.drawBufferInfo(gl, bufferInfo);
}
}
draw();
function getClipSpaceMousePosition(e) {
// get canvas relative css position
const rect = canvas.getBoundingClientRect();
const cssX = e.clientX - rect.left;
const cssY = e.clientY - rect.top;
// get normalized 0 to 1 position across and down canvas
const normalizedX = cssX / canvas.clientWidth;
const normalizedY = cssY / canvas.clientHeight;
// convert to clip space
const clipX = normalizedX * 2 - 1;
const clipY = normalizedY * -2 + 1;
return [clipX, clipY];
}
canvas.addEventListener('wheel', (e) => {
e.preventDefault();
const [clipX, clipY] = getClipSpaceMousePosition(e);
// position before zooming
const [preZoomX, preZoomY] = m3.transformPoint(
m3.inverse(viewProjectionMat),
[clipX, clipY]);
// multiply the wheel movement by the current zoom level
// so we zoom less when zoomed in and more when zoomed out
const newZoom = camera.zoom * Math.pow(2, e.deltaY * -0.01);
camera.zoom = Math.max(0.02, Math.min(100, newZoom));
updateViewProjection();
// position after zooming
const [postZoomX, postZoomY] = m3.transformPoint(
m3.inverse(viewProjectionMat),
[clipX, clipY]);
// camera needs to be moved the difference of before and after
camera.x += preZoomX - postZoomX;
camera.y += preZoomY - postZoomY;
draw();
});
canvas { border: 1px solid black; display: block; }
<canvas></canvas>
<script src="https://twgljs.org/dist/4.x/twgl-full.min.js"></script>
<script src="https://webglfundamentals.org/webgl/resources/m3.js"></script>
请注意,我包含 camera.rotation 只是为了确保旋转后一切正常。他们似乎。 Here's one with zoom, pan, and rotate