site infoHacknerd | Tech Blog
blog cover

🧢 [WebGL编程指南] 7. 进入三维世界

WebGL

视点和视线

观察者所处的位置称为视点,从视点出发沿者观察方向的射线称作视线。

为了确定观察者的状态,需要获取视点和观察目标点(被观察的目标点),可以用来确定视线。要把观察到的景象会知道屏幕上还需要知道上方向。

image
  • 视点:观察者所在的三维空间中的位置,视线的起点
  • 观察目标点:被观察目标所在的点。同时知道视点和目标点,才能算出视线。
  • 上方向:最终绘制在屏幕上的影像的上方向。
  • webGL中默认

  • 视点处于(0,0,0)
  • 视线为z轴负方向
  • 视图矩阵

    根据1. 自定义的观察者状态,绘制观察者看到的景象,与 2. 使用默认状态名单时对三位对象进行平移、旋转等变化,再绘制看到的景象,这两种行为是等价的。

    假设 (0, 0 , 1)。则等价于将顶点坐标往z轴负方向移动1.0个单位。类似的上方向就是对顶点左边乘以旋转矩阵。

    image

    再此基础上还能添加平移、缩放、旋转矩阵,这时矩阵被称为模型矩阵

    image
    javascriptCopy
    // 顶点着色器
    const VSHADER_SOURCE =
    	"attribute vec4 a_Position;\n" +
    	"attribute vec4 a_Color;\n" + 
    	"uniform vec4 u_ViewMatrix;\n" + // 视图矩阵
      "void main() {\n" +
      "  gl_Position = a_Position * u_ViewMatrix;\n" + // 设置坐标
      "  gl_PointSize = 10.0;\n" + // 设置大小
      "  v_Color = a_Color;\n" + // 将数据传递给片元着色器
      "}\n";
     
     // ...
     const u_ViewMatrix = gl.getUniformLocation(gl.program, 'u_ViewMatrix')
     
     // 三方库创建视图矩阵
     const viewMatrix = new Matrix4()
     viewMatrix.lookAt(
    	 0.20, 0.25, 0.25,
    	 0 ,0, 0,
    	 0, 1, 0
     )
     
     gl.uniformMatrix4fv(u_ViewMatrix , false, u_ViewMatrix.elements)

    利用键盘改变视点

    javascriptCopy
    let g_eyeX = 0.20
    let g_eyeY = 0.25
    let g_eyeZ = 0.25
    
    // ...
    document.onkeydown = function (e) {
    	if (e.keyCode == 39) { // 右键
    		g_eyeX -= 0.01
    	} else if (e.keyCode == 37) { // 左键
    		g_eyeX += 0.01
    	} else {
    		return
    	}
    	
    	viewMatrix.setLookAt(
    		g_eyeX, g_eyeY, g_eyeZ,
    		0, 0, 0,
    		0, 1, 0
    	)
    	
    	
     gl.uniformMatrix4fv(u_ViewMatrix , false, u_ViewMatrix.elements)
     
     // 清除缓冲区
     gl.cear(gl.COLOR_BUFFER_BIT)
     
     gl.drawArrays(gl.TRIANGLES, 0, n)
    }

    可视范围

    当视点在极左或极右时,图形会缺一角,这是因为没有指定可视范围。图形只有在可视范围内,WebGL才会绘制它。

    image

    可视空间

  • 正射投影:长方体可视空间(不管图像大小与视点距离无关)。正射投影的盒状空间如图。由前后两个矩阵表面确定,分别称为近裁剪面和远裁剪面。
  • 分别由(right, top, -near), (-left, top, -near), (-left, -bottom -near), (right,-bottom, -near) 和 (right, top, far), (-left, top ,far), (-left, -bottom far), (right,-bottom, far) 组成。

    Canvas上显示的是可视空间中物体在近裁剪面上的投影。如果裁剪面的宽高比和canvas不同,画面会按照canvas宽高比压缩

    image
    javascriptCopy
    // 顶点着色器
    const VSHADER_SOURCE =
    	"attribute vec4 a_Position;\n" +
    	"attribute vec4 a_Color;\n" +
    	"uniform mat4 u_ProjMatrix;\n" +
      "void main() {\n" +
      "  gl_Position = u_ProjMatrix * a_Position;\n" + // 设置坐标
      "  v_Color = a_Color;\n" + // 将数据传递给片元着色器
      "}\n";
      

    通过可视空间的投影矩阵,与顶点坐标相乘,获取图像的投影坐标。通过扩大可视区域可以补上缺失的角。

  • 透视投影:四棱锥,金字塔可视空间(具有深度)。
  • image
    image

    投影矩阵可以实现通过与顶点坐标相乘,得到投影坐标。

    投影矩阵可以使较远的三角形看上去变小,会使三角形不同程度地平移以贴近视线中心

    image

    正确处理对象的前后关系

    默认情况下,WebGL为了加速绘图操作,是按照顶点在缓冲区的顺序来处理他们的。为了解决这个问题,webGL提供了隐藏面消除的功能。

    javascriptCopy
    // 开启隐藏面消除功能
    gl.enable(gl.DEPTH_TEST)
    // 在绘制之前清除深度缓冲区
    gl.clear(gl.DEPTH_BUFFER_BIT)

    深度冲突

    隐藏面消除大多情况下都能很好的完成问题,但当集合体的表面极为接近时,会出现新的问题,使得表面斑斑驳驳,这种现象被称为深度冲突。

    image

    WebGL提供多边形偏移(polygon offset)的机制来解决这个问题。它会自动在Z值上加一个偏移量,偏移量物体表面和视线角度决定。

    javascriptCopy
    // 启用多边形偏移
    gl.enable(gl.POLYFON_OFFSET_FILL)
    
    // 绘制之前指定计算偏移量的参数
    gl.polygonOffset(1.0, 1.0)

    image

    立方体

    绘制立方体可以1. 通过三角形组成,这样一个面需要6个顶点。也可以通过gl.TRIANGLE_FAN 模式绘制,这样一个面只需要4个顶点。不过WebGL也提供了一个完美方案gl.drawElements 可以避免重复定义顶点,使顶点数量保持最小。

    需要在gl.ELEMENT_ARRAY_BUFFER 指定顶点(gl.drawArray 时用的gl.ARRAY_BUFFER)

    image
    image

    需要将立方体拆成顶点和三角形。立方体有前后左右6个面,每个面由两个三角形组成,每个三角形有三个顶点,因此定义一个面需要指定6个顶点的索引

    javascriptCopy
      const VSHADER_SOURCE =
    	  "..."
        "void main() {\n" +
        "  gl_Position = u_MvpMatrix * a_Position;\n" + // 设置坐标
        "  v_Color = a_Color;\n" +
        "}\n";
      // 片元着色器
      const FSHADER_SOURCE =
        "void main() {\n" +
        "  gl_FragColor = v_Color;\n" + // 设置颜色
        "}\n";
        
     // ...
     // 定义顶点和颜色
     const verticesColors = new Float32Array([
    	 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, // v0 白色
    	 ...
    	 -1.0, -1.0, -1.0, 0.0, 0.0 ,0.0 // v7 黑色
     ])
     
     // 顶点索引
     const indeces = new Unit8Array([
    	 0, 1, 2, 0, 2, 3,
    	 0, 3, 4, 0, 4, 5,
    	 0, 5, 6, 0, 6, 1,
    	 1, 6, 7, 1, 7, 2,
    	 7, 4, 3, 7, 3, 2,
    	 4, 7, 6, 4, 6, 5
     ])
     
     // 创建顶点坐标buffer
     // ...
     
     // 创建索引buffer
     const indeBuffer = gl.createBuffer()
     
     gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer)
     gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW)
     
     // ...
     
     gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
     
     gl.drawElements(gl.TRIANLGES, n, gl.UNSIGNED_BYTE, 0)

  • 但这样,颜色依然依赖于顶点。如果想要每个表面是不同的单一颜色或纹理图像,而不是随着顶点颜色进行渐变,则需要将每个面的颜色信息,写入三角形列表、索引、顶点数据。
  • 顶点着色器是逐顶点计算的,接收的也是逐顶点数据,这说明,想要指定表面的颜色比如指定立方体前表面为蓝色,则响应体将v0, v1, v2, v3这四个点都指定为蓝色。
  • 但顶点v0不仅在前表面上出现,所以需要创建一些相同的坐标(每个面每个点都需要重复创建一遍,同时指定颜色)
  • javascriptCopy
    // ...
    
    
     // 定义顶点
     const vertices = new Float32Array([
    	 1.0, 1.0, 1.0, -1.0, 1.0, 1.0, -1.0, -1.0, 1.0, 1.0, -1.0, 1.0, // 第一个面
    	 ...
     ])
     
     // 颜色
     const colors = new Float32Array([
    	 0.4, 0.4, 1.0, 0.4, 0.4, 1.0, 0.4, 0.4, 1.0, 0.4, 0.4, 1.0,  // 第一个面的颜色
    	 ...
     ])
     
     
     const indeces = new Unit8Array([
    	 0, 1, 2, 0, 2, 3, // 前
    	 4, 5, 6, 4, 6, 7, //右
    	 ...
     ])
     
     // 创建顶点缓冲区
     // 写数据
     
     // 创建颜色缓冲区
     // 写数据

    Contents

    • 视点和视线
    • 视图矩阵
    • 利用键盘改变视点
    • 可视范围
    • 可视空间
    • 正确处理对象的前后关系
    • 深度冲突
    • 立方体

    2024/10/15 07:37