# 用鼠标控制物体旋转

需要根据鼠标的移动情况创建旋转矩阵,更新模型视图投影矩阵,并对物体的顶点坐标进行变化。

  • 实现思路:

在鼠标左键按下时记录鼠标的初始坐标,然后在鼠标移动的时候用当前坐标减去初始坐标,获得鼠标的位置,然后根据这个位移来计算旋转矩阵。

  • 示例程序关键代码:
function initEventHandlers(canvas, currentAngle) {
  // 是否拖动
  let draggle = false
  // 鼠标的最后位置
  let lastX = -1,
      lastY = -1
  
  // 按下鼠标
  canvas.onmousedown = (ev) => {
    const x = ev.clientX, y = ev.clientY
    // 如果鼠标在 canvas 内就开始拖动
    const rect = ev.target.getBoundingClientRect()
    if (rect.left <= x && x < rect.right && rect.top <= y && y < rect.bottom) {
      lastX = x
      lastY = y
      draggle = true
    }
  }
  // 鼠标松开
  canvas.onmouseup = (ev) => {
    draggle = false
  }
  // 鼠标移动
  canvas.onmousemove = (ev) => {
    const x = ev.clientX, y = ev.clientY
    if(draggle) {
      // 旋转因子
      const factor = 100 / canvas.height
      const dx = factor * (x - lastX)
      const dy = factor * (y - lastY)
      // 将沿 Y 轴角度的因子控制在 -90 到 90
      currentAngle[0] = Math.max(Math.min(currentAngle[0] + dy, 90.0), -90.0)
      currentAngle[1] = currentAngle[1] + dx
    }
    lastX = x
    lastY = y
  }
}

// 模型视图投影矩阵
const g_MvpMatrix = new Matrix4()

function draw(gl, n, viewProjMatrix, u_MvpMatrix, currentAngle) {
  // 计算模型视图投影矩阵
  // 复制模型矩阵
  g_MvpMatrix.set(viewProjMatrix)
  // 绕 x 轴旋转
  g_MvpMatrix.rotate(currentAngle[0], 1.0, 0.0, 0.0)
  g_MvpMatrix.rotate(currentAngle[1], 0.0, 1.0, 0.0)
  
  gl.uniformMatrix4fv(u_MvpMatrix, false, g_MvpMatrix.elements)

  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)

  gl.drawElements(gl.TRIANGLES, n, gl.UNSIGNED_BYTE, 0)
}

# 对象拾取

  • 实现思路

一个简单的做法就是:

  1. 当鼠标左键按下时,将整个立方体重绘为单一的红色;(为了使用户看不到立方体的这一闪烁过程,需要在取出像素颜色之后立即将立方体进行重绘)
  2. 读取鼠标点击处的像素颜色;
  3. 使用立方体原来的颜色对其进行重绘;
  4. 如果第二步读取的颜色是红色,就提示选中对象;
  • 示例程序关键代码

注意,上面第一步将立方体重绘为红色的过程发生在顶点着色器中,我们向其中添加了一个 u_clicked 变量,这样就可以在恰当的时候通过该变量通知顶点着色器将立方体绘制成红色。鼠标点击时, JavaScript 就会向 u_Click 变量传入 true 值,然后顶点着色器经过判断,将一个固定颜色值赋值给 v_Color 变量。如果 u_Click 为 false,那么顶点着色器就照常将立方体原来的颜色 a_Color 赋值给 v_Color,这样一来,鼠标点击,立方体就被绘制成红色。

  // 首次加载为 false
  gl.uniform1i(u_Clicked, 0)

  // 当前旋转角度
  let currentAngle = 0.0
  canvas.onmousedown = (ev) => {
    const x = ev.clientX, y = ev.clientY
    const rect = ev.target.getBoundingClientRect()
    if (rect.left <= x && x < rect.right && rect.top <= y && y < rect.bottom) {
      const x_in_canvas = x - rect.left, y_in_canvas = rect.bottom - y
      const picked = check(gl, n, x_in_canvas, y_in_canvas, currentAngle, u_Clicked, viewProjMatrix, u_MvpMatrix)
      if (picked) alert('The cube was selected! ')
    }
  }


function check(gl, n, x, y, currentAngle, u_Clicked, viewProjMatrix, u_MvpMatrix) {
  let picked = false
  // 拾取到物体
  gl.uniform1i(u_Clicked, 1)
  draw(gl, n, currentAngle, viewProjMatrix, u_MvpMatrix)
  // 获取点击位置的像素颜色值
  const pixels = new Uint8Array(4) // 存储像素的数组
  gl.readPixels(x, y, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, pixels)

  // 如果 pixels[0] 为 255 表示点击在物体上
  if (pixels[0] === 255) {
    picked = true
  }
  // 重新绘制立方体
  gl.uniform1i(u_Clicked, 0)
  draw(gl, n, currentAngle, viewProjMatrix, u_MvpMatrix)

  return picked
}

readPixels 函数使用如下:

avatar

# 拾取立方体的一个表面

跟前面拾取对象一样,只不过在选择了立方体时,将每个像素属于哪个面的信息存储到颜色缓冲区的 α 分量中;

const VSHADER_SOURCE = `
  attribute vec4 a_Position;
  attribute vec4 a_Color;
  attribute float a_Face; // 表面编号(不可以使用 int 类型)
  uniform mat4 u_MvpMatrix;
  uniform int u_PickedFace; // 被选中表面的编号
  // varying 变量只能是 float
  varying vec4 v_Color;
  void main() {
    // 如果 gl_Position 最后一个分量为 1.0,那么前三个分量就可以表示一个点的三维坐标。平移不改变缩放比例,所以 u_Translation 第四个参数为 0.0,1.0 表示不缩放
    gl_Position = u_MvpMatrix * a_Position;
    int face = int(a_Face); // 转换为 int 类型
    vec3 color = (face == u_PickedFace) ? vec3(1.0) : a_Color.rgb;
    if (u_PickedFace == 0) {
      // 将表面编号写入 α 分量
      v_Color = vec4(color, a_Face/255.0);
    } else {
      v_Color = vec4(color, a_Color.a);
    }
  }
`

function main() {
  // 当前旋转角度
  let currentAngle = 0.0
  canvas.onmousedown = (ev) => {
    const x = ev.clientX, y = ev.clientY
    const rect = ev.target.getBoundingClientRect()
    if (rect.left <= x && x < rect.right && rect.top <= y && y < rect.bottom) {
      const x_in_canvas = x - rect.left, y_in_canvas = rect.bottom - y
      const face = checkFace(gl, n, x_in_canvas, y_in_canvas, currentAngle, u_PickedFace, viewProjMatrix, u_MvpMatrix)
      // 传入表面编号
      gl.uniform1i(u_PickedFace, face)
      draw(gl, n, currentAngle, viewProjMatrix, u_MvpMatrix)
    }
  }
}
function checkFace(gl, n, x, y, currentAngle, u_PickedFace, viewProjMatrix, u_MvpMatrix) {
  // 存储像素值的数组
  const pixels = new Uint8Array(4)
  // 将表面编号写入 α
  gl.uniform1i(u_PickedFace, 0)
  draw(gl, n, currentAngle, viewProjMatrix, u_MvpMatrix)
  // 读取 x y 处的像素颜色,pixels[3] 中存储了表面编号
  gl.readPixels(x, y, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, pixels)

  return pixels[3]
}

# UHD 平视显示器

就是在三维场景中叠加文本或二维图形。可以使用 html 和 canvas 函数来实现 HUD,我们需要在 HTML 文件中,添加 canvas 元素,用于绘制 HUD,将三维场景的 canvas 与 HUD 的 canvas 重叠放置,并让 HUD 的 canvas 置顶。

关键代码如下:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>平视显示器</title>
  <style>
    #webgl {
      position: absolute;
      z-index: 0;
    }
    #hud {
      position: absolute;
      z-index: 1;
    }
  </style>
</head>
<body onload="main()">
  <canvas id="webgl" width="400" height="400">
    Please use a browser that supports "canvas"
  </canvas>
  <canvas id="hud" width="400" height="400"></canvas>
  <script src="../lib/webgl-utils.js"></script>
  <script src="../lib/matrix.js"></script>
  <script src="index.js"></script>
</body>
</html>

这个例子是在拾取对象例子的基础上实现 hud,我们需要获取 hud 的 canvas 的绘图上下文,用来绘制三角形和文本。然后将鼠标点击事件响应函数注册到 HUD 的 canvas 上,上面的例子是注册到了 webgl 的 canvas 上;关键代码如下:

function main() {
  const hud = document.getElementById('hud')
  // 获取二维绘图上下文
  const ctx = hud.getContext('2d')

  const trick = () => {
    currentAngle = animate(currentAngle)
    draw(gl, n, currentAngle, viewProjMatrix, u_MvpMatrix)
    // 绘制二维图形
    draw2D(ctx, currentAngle)
    requestAnimationFrame(trick, canvas)
  }
  trick()
}
function draw2D(ctx, currentAngle) {
  // 清空 canvas
  ctx.clearRect(0, 0, 400, 400)
  // 用白色的线条绘制三角形
  ctx.beginPath() // 开始绘制
  ctx.moveTo(120, 10)
  ctx.lineTo(200, 150)
  ctx.lineTo(40, 150)
  ctx.closePath()
  // 设置线条颜色
  ctx.strokeStyle = 'rgba(255, 255, 255, 1)'
  // 用白色的线条绘制三角形
  ctx.stroke()
  // 绘制白色文本
  ctx.font = '18px "Times New Roman"'
  // 设置文本颜色
  ctx.fillStyle = 'rgba(255, 255, 255, 1)'
  ctx.fillText('HUD: Head Up Display', 40, 180)
  ctx.fillText('Triangle is drawn by Canvas 2D API.', 40, 200)
  ctx.fillText('Cube is drawn by WebGL API.', 40, 220)
  ctx.fillText('Current Angle: '+ Math.floor(currentAngle), 40, 240)
}

# 雾化(大气效果)

实现雾化有很多种,这里使用最简单的一种:线性雾化。在线性雾化中,某一点的雾化程度取决于它与视点之间的距离,距离越远雾化程度越高。线性雾化有起点和终点,起点表示开始雾化之处,终点表示完全雾化之处,两点之间某一点的雾化程度与该点与视点的距离呈线性关系。注意,比终点更远的点完全雾化了,也就是完全看不见。某一点雾化的程度可以被定义为雾化因子,并在线性雾化公式中被计算出来:

雾化因子 = ( 终点 - 当前与视点间的距离 ) / ( 终点 - 起点 )

如果雾化因子为 1.0,表示该点完全没有雾化,可以很清晰地看到此处的物体。如果为 0.0,表示完全雾化。

avatar

在片元着色器中根据雾化因子计算片元的颜色:

片元颜色 = 物体表面颜色 × 雾化因子 + 雾的颜色 × (1 - 雾化因子)

  • 实现代码:
  1. 顶点着色器计算出当前顶点与视点的距离,并传入片元着色器;
  2. 片元着色器根据片元与视点的距离,计算雾化因子,最终计算出片元的颜色。

通过上下键改变雾化距离

const VSHADER_SOURCE = `
  attribute vec4 a_Position;
  attribute vec4 a_Color;
  uniform mat4 u_MvpMatrix;
  uniform mat4 u_ModelMatrix;
  uniform vec4 u_Eye; // 世界坐标系下视点的位置
  // varying 变量只能是 float
  varying vec4 v_Color;
  varying float v_Dist;
  void main() {
    // 如果 gl_Position 最后一个分量为 1.0,那么前三个分量就可以表示一个点的三维坐标。平移不改变缩放比例,所以 u_Translation 第四个参数为 0.0,1.0 表示不缩放
    gl_Position = u_MvpMatrix * a_Position;
    v_Color = a_Color;
    // 计算顶点与视点的矩阵
    v_Dist = distance(u_ModelMatrix * a_Position, u_Eye);
  }
`

const FSHADER_SOURCE = `
  #ifdef GL_ES
  precision mediump float;
  #endif
  uniform vec3 u_FogColor; // 雾的颜色
  uniform vec2 u_FogDist; // 雾化的起点和终点
  varying vec4 v_Color;
  varying float v_Dist;
  void main() {
    // 计算雾化因子
    float fogFactor = clamp((u_FogDist.y - v_Dist) / (u_FogDist.y - u_FogDist.x), 0.0, 1.0);
    vec3 color = mix(u_FogColor, vec3(v_Color), fogFactor);
    gl_FragColor = vec4(color, v_Color.a);
  }
`

function main() {
  // 雾的颜色
  const fogColor = new Float32Array([0.137, 0.231, 0.423])
  // 雾化的起点和终点与视点间的距离 [起点距离,终点距离]
  const fogDist = new Float32Array([55, 80])
  // 视点在世界坐标系下的坐标
  const eye = new Float32Array([25, 65, 35, 1.0])
 
  var u_ModelMatrix = gl.getUniformLocation(gl.program, 'u_ModelMatrix')
  var u_Eye = gl.getUniformLocation(gl.program, 'u_Eye')
  var u_FogColor = gl.getUniformLocation(gl.program, 'u_FogColor')
  var u_FogDist = gl.getUniformLocation(gl.program, 'u_FogDist')
  if (!u_ModelMatrix || !u_Eye || !u_FogColor || !u_FogDist) {
    console.log('Failed to get the storage location')
    return
  }

  gl.uniform3fv(u_FogColor, fogColor)
  gl.uniform2fv(u_FogDist, fogDist)
  gl.uniform4fv(u_Eye, eye)

  // 设置背景色
  gl.clearColor(fogColor[0], fogColor[1], fogColor[2], 1.0)

  // 开启隐藏面消除
  gl.enable(gl.DEPTH_TEST)

  // 获取 u_MvpMatrix 变量的存储位置
  const u_MvpMatrix = gl.getUniformLocation(gl.program, 'u_MvpMatrix')
  if (u_MvpMatrix < 0) {
    console.log('Failed to get the storage location of u_MvpMatrix')
    return -1
  }

  const modelMatrix = new Matrix4()
  modelMatrix.setScale(10, 10, 10)
  gl.uniformMatrix4fv(u_ModelMatrix, false, modelMatrix.elements)

  // 模型视图投影矩阵
  const mvpMatrix = new Matrix4()
  
  // 计算模型矩阵、视图矩阵、投影矩阵
  mvpMatrix.setPerspective(30, canvas.width/canvas.height, 1, 1000)
  mvpMatrix.lookAt(eye[0], eye[1], eye[2], 0, 2, 0, 0, 1, 0)
  mvpMatrix.multiply(modelMatrix)
  gl.uniformMatrix4fv(u_MvpMatrix, false, mvpMatrix.elements)

  document.onkeydown = (ev) => {
    keydown(ev, gl, n, u_FogDist, fogDist)
  }

  // 清空颜色和深度缓冲区
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)

  // 绘制立方体
  gl.drawElements(gl.TRIANGLES, n, gl.UNSIGNED_BYTE, 0)
}

function keydown(ev, gl, n, u_FogDist, fogDist) {
  switch (ev.keyCode) {
    case 38:
      fogDist[1]  += 1
      break;
    case 40:
      if (fogDist[1] > fogDist[0]) fogDist[1] -= 1
      break
    default: return
  }
  gl.uniform2fv(u_FogDist, fogDist)

  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)

  gl.drawElements(gl.TRIANGLES, n, gl.UNSIGNED_BYTE, 0)
}

评 论:

更新: 11/25/2020, 1:40:07 AM