WebGL,如何从 3D 转换到 2D 并返回

gma*_*man 2 webgl

如何在 WebGL 中在 3D 视图和 2D 视图之间转换?

我有一个场景的 3D 视图,并且还想显示 2D 视图,例如地图视图。如何在两种类型的视图之间切换?

gma*_*man 5

一般来说,要从 3d 切换到 2d,您只需使用正交投影而不是透视投影

如果你想动画化两者之间的过渡似乎可以

 const ortho = someOrthoFunc(left, right, top, bottom, orthoZNear, orthZFar);
 const persp = somePerspFunc(fov, aspect, perspZNear, perspZFar);
 const projection = [];
 for (let i = 0; i < 16; ++i) {
   projection[i] = lerp(ortho[i], persp[i], mixAmount);     
 }


 function lerp(a, b, l) {
   return a + (b - a) * l;
 }
Run Code Online (Sandbox Code Playgroud)

mixAmount您需要正交视图(2d-ish)时,其中 0 为mixAmount1;当您需要透视视图(3d)时,其中 0 为 1,并且您可以在 0 和 1 之间对其进行动画处理。

请注意,如果您希望正交视图与透视视图匹配,您需要选择与您的应用程序相匹配的topbottomleft、值。right要在两种不同的视图之间进行转换(例如地面上的第一人称与直视向下的人称),您可以选择您想要的任何设置。但是假设您正在向下看,只是想以相同的视图从 3D 切换到 2D。在这种情况下,您需要选择与给定数量的单元的透视图相匹配的左、右、上、下。对于顶部和底部,这可能是有多少单位垂直适合距相机的“地面”距离。

请参阅此答案,其中距离是到地面的距离,然后公式将为您提供该距离处单位数量一半的数量,然后您可以将其代入topbottom。并left乘以right画布显示尺寸的纵横比

另一件改变的是相机。定位相机的常见方法是使用一个lookAt函数,根据库的不同,该函数可能会生成视图矩阵或相机矩阵。

往下看

const cameraPosition = [x, groundHeight + distanceAboveGround, z];
const target = [x, groundHeight, z];
const up = [0, 0, 1];
const camera = someLookAtFunction(camearPosition, target, up);
Run Code Online (Sandbox Code Playgroud)

您将为3D 相机准备一组不同的cameraPosition, target, 。up您可以通过 lerping 这 3 个变量来动画化它们之间的转换。

 const ortho = someOrthoFunc(left, right, top, bottom, orthoZNear, orthZFar);
 const persp = somePerspFunc(fov, aspect, perspZNear, perspZFar);
 const projection = [];
 for (let i = 0; i < 16; ++i) {
   projection[i] = lerp(ortho[i], persp[i], mixAmount);     
 }


 function lerp(a, b, l) {
   return a + (b - a) * l;
 }
Run Code Online (Sandbox Code Playgroud)
const cameraPosition = [x, groundHeight + distanceAboveGround, z];
const target = [x, groundHeight, z];
const up = [0, 0, 1];
const camera = someLookAtFunction(camearPosition, target, up);
Run Code Online (Sandbox Code Playgroud)
const vs = `
uniform mat4 u_worldViewProjection;

attribute vec4 a_position;
attribute vec2 a_texcoord;

varying vec4 v_position;
varying vec2 v_texcoord;

void main() {
  v_texcoord = a_texcoord;
  gl_Position = u_worldViewProjection * a_position;
}
`;
const fs = `
precision mediump float;

varying vec2 v_texcoord;

uniform sampler2D u_texture;

void main() {
  gl_FragColor = texture2D(u_texture, v_texcoord);
}
`;


"use strict";
twgl.setDefaults({attribPrefix: "a_"});
const m4 = twgl.m4;
const v3 = twgl.v3;
const gl = document.getElementById("c").getContext("webgl");

// compiles shaders, links program, looks up locations
const programInfo = twgl.createProgramInfo(gl, [vs, fs]);

// calls gl.createBuffer, gl.bindBuffer, gl.bufferData for positions, texcoords
const bufferInfo = twgl.primitives.createCubeBufferInfo(gl);

// calls gl.createTexture, gl.bindTexture, gl.texImage2D, gl.texParameteri
const tex = twgl.createTexture(gl, {
  min: gl.NEAREST,
  mag: gl.NEAREST,
  src: [
    255, 0, 0, 255,
    0, 192, 0, 255,
    0, 0, 255, 255,
    255, 224, 0, 255,
  ],
});

const settings = {
  projectionMode: 2,
  cameraMode: 2,
  fov: 30,
};

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

  gl.enable(gl.DEPTH_TEST);
  gl.enable(gl.CULL_FACE);
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

  const fov = settings.fov * Math.PI / 180;
  const aspect = gl.canvas.clientWidth / gl.canvas.clientHeight;
  const perspZNear = 0.5;
  const perspZFar = 10;
  const persp = m4.perspective(fov, aspect, perspZNear, perspZFar);

  // the size to make the orthographic view is arbitrary.
  // here we're choosing the number of units at ground level
  // away from the top perspective camera
  const heightAboveGroundInTopView = 7;
  const halfSizeToFitOnScreen = heightAboveGroundInTopView * Math.tan(fov / 2);

  const top = -halfSizeToFitOnScreen;
  const bottom = +halfSizeToFitOnScreen;
  const left = top * aspect;
  const right = bottom * aspect;
  const orthoZNear = 0.5;
  const orthoZFar = 10;
  const ortho = m4.ortho(left, right, top, bottom, orthoZNear, orthoZFar);

  let perspMixAmount;
  let camMixAmount;
  switch (settings.projectionMode) {
    case 0: // 2d
      perspMixAmount = 0;
      break;
    case 1: // 3d
      perspMixAmount = 1;
      break;
    case 2: // animated
      perspMixAmount = Math.sin(time) * .5 + .5;
      break;
  }

  switch (settings.cameraMode) {
    case 0: // top
      camMixAmount = 0;
      break;
    case 1: // angle
      camMixAmount = 1;
      break;
    case 2: // animated
      camMixAmount = Math.sin(time) * .5 + .5;
      break;
  }

  const projection = [];
  for (let i = 0; i < 16; ++i) {
    projection[i] = lerp(ortho[i], persp[i], perspMixAmount);
  }

  const perspEye = [1, 4, -6];
  const perspTarget = [0, 0, 0];
  const perspUp = [0, 1, 0];

  const orthoEye = [0, heightAboveGroundInTopView, 0];
  const orthoTarget = [0, 0, 0];
  const orthoUp = [0, 0, 1];

  const eye = v3.lerp(orthoEye, perspEye, camMixAmount);
  const target = v3.lerp(orthoTarget, perspTarget, camMixAmount);
  const up = v3.lerp(orthoUp, perspUp, camMixAmount);

  const camera = m4.lookAt(eye, target, up);
  const view = m4.inverse(camera);
  const viewProjection = m4.multiply(projection, view);

  gl.useProgram(programInfo.program);

  // calls gl.bindBuffer, gl.enableVertexAttribArray, gl.vertexAttribPointer
  twgl.setBuffersAndAttributes(gl, programInfo, bufferInfo);
  const t = time * .1;
  for (let z = -1; z <= 1; ++z) {
    for (let x = -1; x <= 1; ++x) {
      const world = m4.translation([x * 1.4, 0, z * 1.4]);
      m4.rotateY(world, t + z + x, world);

      // calls gl.uniformXXX
      twgl.setUniforms(programInfo, {
        u_texture: tex,
        u_worldViewProjection: m4.multiply(viewProjection, world),
      });

      // calls gl.drawArrays or gl.drawElements
      twgl.drawBufferInfo(gl, bufferInfo);
    }
  }

  requestAnimationFrame(render);
}
requestAnimationFrame(render);

setupRadioButtons("proj", "projectionMode");
setupRadioButtons("cam", "cameraMode");
setupSlider("#fovSlider", "#fov", "fov");

function setupSlider(sliderId, labelId, property) {
  const slider = document.querySelector(sliderId);
  const label = document.querySelector(labelId);

  function updateLabel() {
    label.textContent = settings[property];
  }

  slider.addEventListener('input', e => {
    settings[property] = parseInt(slider.value);
    updateLabel();
  });

  updateLabel();
  slider.value = settings[property];
}

function setupRadioButtons(name, property) {
  document.querySelectorAll(`input[name=${name}]`).forEach(elem => {
    elem.addEventListener('change', e => {
      if (e.target.checked) {
        settings[property] = parseInt(e.target.value);
      }
    });
  });
}

function lerp(a, b, l) {
  return a + (b - a) * l;
}
Run Code Online (Sandbox Code Playgroud)