在 WebGL 中,我可以使用矩阵在屏幕 space 中绘制对象偏移量吗?

In WebGL, can I use a matrix to draw an object offset in screen space?

我有一个简单的对象,它在 0, 0, 0 处绘制一个 3d gizmo。如果相机以 0, 0, 0 为中心,那么它将在屏幕中心绘制 gizmo。

我想“提升”这个 Gizmo 并在屏幕坐标中将其呈现在屏幕的右下角,而不旋转它。基本上,我希望 Gizmo 显示屏幕中心的旋转,而不会阻挡视图,也不必专注于特定点。所以我想取消模型矩阵或其他东西。

我通过转换投影矩阵得到以下结果:

this.gl.uniformMatrix4fv(this.modelMatrixUniform, false, modelMatrix);
this.gl.uniformMatrix4fv(this.viewMatrixUniform, false, viewMatrix);

const bottomRightMat = mat4.create();
mat4.translate(bottomRightMat, projectionMatrix, [5, -3, 0]);
this.gl.uniformMatrix4fv(this.projectionMatrixUniform, false, bottomRightMat);
this.gl.drawElements(this.gl.LINES, this.indexBuffer.getLength(), this.gl.UNSIGNED_SHORT, 0);

但是 Gizmo 已旋转到新位置。红线仍应指向下方和左侧,因为这是屏幕中心的正 X 轴方向。另外,数字 5 和 3 是任意的,我不认为它们适用于不同的变焦或相机位置。

有没有一种方法可以指定采用屏幕中心并将其平移到屏幕中的矩阵变换space?

一种方法是在渲染该对象时更改视口。

// size of area in bottom right
const miniWidth = 150;
const miniHeight = 100;
gl.viewport(gl.canvas.width - miniWidth, gl.canvas.height - miniHeight, miniWidth, miniHeight);

// now draw. you'll need to zoom in, like set the camera closer
// or move the object closer or add a scale matrix after the projection
// matrix as in projection * scale * view * ...

您需要一个与新视口的纵横比相匹配的投影矩阵,并且您需要缩放对象、将相机放得更近一些,或者在投影和视图矩阵之间添加 2D 比例。 请记住将视口放回完整 canvas 以渲染场景的其余部分。

const vs = `
attribute vec4 position;
uniform mat4 u_worldViewProjection;
void main() {
  gl_Position = u_worldViewProjection * position;
}
`;

const fs = `
precision mediump float;
void main() {
  gl_FragColor = vec4(vec3(0), 1);
}
`

const gl = document.querySelector("canvas").getContext("webgl");
const programInfo = twgl.createProgramInfo(gl, [vs, fs]);

const bufferInfo = twgl.createBufferInfoFromArrays(gl, {
  position: [
    -1, -1, -1,
     1, -1, -1,
     1,  1, -1,
    -1,  1, -1,
    -1, -1,  1,
     1, -1,  1,
     1,  1,  1,
    -1,  1,  1,
  ],
  indices: {
    numComponents: 2,
    data: [
      0, 1,
      1, 2,
      2, 3,
      3, 0,
      4, 5,
      5, 6,
      6, 7,
      7, 4,
      0, 4,
      1, 5,
      2, 6,
      3, 7,
    ],
  },
});

function render(time) {
  time *= 0.001;
  twgl.resizeCanvasToDisplaySize(gl.canvas);
  gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);

  const fov = 90 * Math.PI / 180;
  const zNear = 0.5;
  const zFar = 100;
  const projection = mat4.perspective(mat4.create(),
      fov, gl.canvas.clientWidth / gl.canvas.clientHeight, zNear, zFar);
  const eye = [0, 0, 10];
  const target = [0, 0, 0];
  const up = [0, 1, 0];
  const view = mat4.lookAt(mat4.create(), eye, target, up);

  drawCube([-8, 0, 0], projection, view);
  drawCube([-4, 0, 0], projection, view);
  drawCube([ 0, 0, 0], projection, view);
  drawCube([ 4, 0, 0], projection, view);
  drawCube([ 8, 0, 0], projection, view);

  const iconAreaWidth  = 100;
  const iconAreaHeight = 75;
  gl.viewport(
      gl.canvas.width - iconAreaWidth, 0, 
      iconAreaWidth, iconAreaHeight);     
  const iconProjection = mat4.perspective(mat4.create(),
      fov, iconAreaWidth / iconAreaHeight, zNear, zFar);
  // compute the zoom size need to make things the sngs
  const scale = gl.canvas.clientHeight / iconAreaHeight;
  mat4.scale(iconProjection, iconProjection, [scale, scale, 1]);
  drawCube([ 0, 0, 0], iconProjection, view);

  function drawCube(translation, projection, view) {
    const viewProjection = mat4.multiply(mat4.create(), projection, view);
    const world = mat4.multiply(
       mat4.create(),
       mat4.fromTranslation(mat4.create(), translation),
       mat4.fromRotation(mat4.create(), time, [0.42, 0.56, 0.70]));
    const worldViewProjection = mat4.multiply(mat4.create(), viewProjection, world);

    gl.useProgram(programInfo.program);
    twgl.setBuffersAndAttributes(gl, programInfo, bufferInfo);
    twgl.setUniforms(programInfo, {
      u_worldViewProjection: worldViewProjection,
    });
    twgl.drawBufferInfo(gl, bufferInfo, gl.LINES);
  }
  
  requestAnimationFrame(render);
}
requestAnimationFrame(render);
body { margin: 0; }
canvas { width: 100vw; height: 100vh; display: block; background: #CDE; }
<canvas></canvas>
<script src="https://twgljs.org/dist/4.x/twgl-full.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gl-matrix/2.8.1/gl-matrix.js"></script>

另一种是计算偏心平截头体投影矩阵。而不是 mat4.perspective 使用 mat4.frustum

function perspectiveWithCenter(
    fieldOfView, width, height, near, far, centerX = 0, centerY = 0) {
  const aspect = width / height;

  // compute the top and bottom of the near plane of the view frustum
  const top = Math.tan(fieldOfView * 0.5) * near;
  const bottom = -top;

  // compute the left and right of the near plane of the view frustum
  const left = aspect * bottom;
  const right = aspect * top;

  // compute width and height of the near plane of the view frustum
  const nearWidth = right - left;
  const nearHeight = top - bottom;

  // convert the offset from canvas units to near plane units
  const offX = centerX * nearWidth / width;
  const offY = centerY * nearHeight / height;

  const m = mat4.create();
  mat4.frustum(
        m,
        left + offX,
        right + offX,
        bottom + offY,
        top + offY,
        near,
        far);
  return m;
}

所以要绘制你的小发明集调用类似

  const gizmoCenterX = -gl.canvas.clientWidth  / 2 + 50;
  const gizmoCenterY =  gl.canvas.clientHeight / 2 - 50;
  const offsetProjection = perspectiveWithCenter(
      fov, gl.canvas.clientWidth, gl.canvas.clientHeight, zNear, zFar,
      gizmoCenterX, gizmoCenterY);

const vs = `
attribute vec4 position;
uniform mat4 u_worldViewProjection;
void main() {
  gl_Position = u_worldViewProjection * position;
}
`;

const fs = `
precision mediump float;
void main() {
  gl_FragColor = vec4(vec3(0), 1);
}
`

const gl = document.querySelector("canvas").getContext("webgl");
const programInfo = twgl.createProgramInfo(gl, [vs, fs]);

const bufferInfo = twgl.createBufferInfoFromArrays(gl, {
  position: [
    -1, -1, -1,
     1, -1, -1,
     1,  1, -1,
    -1,  1, -1,
    -1, -1,  1,
     1, -1,  1,
     1,  1,  1,
    -1,  1,  1,
  ],
  indices: {
    numComponents: 2,
    data: [
      0, 1,
      1, 2,
      2, 3,
      3, 0,
      4, 5,
      5, 6,
      6, 7,
      7, 4,
      0, 4,
      1, 5,
      2, 6,
      3, 7,
    ],
  },
});

function perspectiveWithCenter(
    fieldOfView, width, height, near, far, centerX = 0, centerY = 0) {
  const aspect = width / height;

  // compute the top and bottom of the near plane of the view frustum
  const top = Math.tan(fieldOfView * 0.5) * near;
  const bottom = -top;

  // compute the left and right of the near plane of the view frustum
  const left = aspect * bottom;
  const right = aspect * top;

  // compute width and height of the near plane of the view frustum
  const nearWidth = right - left;
  const nearHeight = top - bottom;

  // convert the offset from canvas units to near plane units
  const offX = centerX * nearWidth / width;
  const offY = centerY * nearHeight / height;

  const m = mat4.create();
  mat4.frustum(
        m,
        left + offX,
        right + offX,
        bottom + offY,
        top + offY,
        near,
        far);
  return m;
}

function render(time) {
  time *= 0.001;
  twgl.resizeCanvasToDisplaySize(gl.canvas);
  gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);

  const fov = 90 * Math.PI / 180;
  const zNear = 0.5;
  const zFar = 100;
  const projection = perspectiveWithCenter(
      fov, gl.canvas.clientWidth, gl.canvas.clientHeight, zNear, zFar);
  const eye = [0, 0, 10];
  const target = [0, 0, 0];
  const up = [0, 1, 0];
  const view = mat4.lookAt(mat4.create(), eye, target, up);

  drawCube([-8, 0, 0], projection, view);
  drawCube([-4, 0, 0], projection, view);
  drawCube([ 0, 0, 0], projection, view);
  drawCube([ 4, 0, 0], projection, view);
  drawCube([ 8, 0, 0], projection, view);

  const gizmoCenterX = -gl.canvas.clientWidth  / 2 + 50;
  const gizmoCenterY =  gl.canvas.clientHeight / 2 - 50;
  const offsetProjection = perspectiveWithCenter(
      fov, gl.canvas.clientWidth, gl.canvas.clientHeight, zNear, zFar,
      gizmoCenterX, gizmoCenterY);
  drawCube([ 0, 0, 0], offsetProjection, view);

  function drawCube(translation, projection, view) {
    const viewProjection = mat4.multiply(mat4.create(), projection, view);
    const world = mat4.multiply(
       mat4.create(),
       mat4.fromTranslation(mat4.create(), translation),
       mat4.fromRotation(mat4.create(), time, [0.42, 0.56, 0.70]));
    const worldViewProjection = mat4.multiply(mat4.create(), viewProjection, world);

    gl.useProgram(programInfo.program);
    twgl.setBuffersAndAttributes(gl, programInfo, bufferInfo);
    twgl.setUniforms(programInfo, {
      u_worldViewProjection: worldViewProjection,
    });
    twgl.drawBufferInfo(gl, bufferInfo, gl.LINES);
  }
  
  requestAnimationFrame(render);
}
requestAnimationFrame(render);
body { margin: 0; }
canvas { width: 100vw; height: 100vh; display: block; background: #CDE; }
<canvas></canvas>
<script src="https://twgljs.org/dist/4.x/twgl-full.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gl-matrix/2.8.1/gl-matrix.js"></script>