前言
前面我们将了绘制一个点,并通过 JavaScript 向 webgl 系统传递变量进行绘制。但是如果我们绘制一个复杂图形,比如我们常见的三维世界。其实不管三维模型的形状多么复杂,其基本组成部分都是一个个三角形。为什么是三角形呢,因为三角形构成是三个点,三个点可以唯一确定一个面;这样就可以简化图形绘图时的一些不必要的计算。接下来我们将绘制一个 ‘F’,效果如下:
绘制一个 ‘F’
首先还是贴代码:
import React, { useRef, useEffect } from 'react'
import { VSHADERR_SOURCE, FSHADER_SOURCE } from './glsl'
import { initShader } from '../../utils/webglFunc'
import { d2_f as shape } from './df2'
import './index.css'
const DrawFShape = () => {
const canvasDom = useRef<HTMLCanvasElement | null>(null)
const initVertexBuffers = (gl: WebGLRenderingContext) => {
// 定义顶点
const verticies = shape(100, 100, 100, 150, 30)
// 创建缓冲区对象
const vertexBuffer = gl.createBuffer()
// 将缓冲区对象绑定到目标
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer)
// 向缓冲区对象中写入数据
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(verticies), gl.STATIC_DRAW)
// 在顶点着色器中获取 a_Position 的地址
// gl.program 为包含顶点着色器和片元着色器的着色器程序对象
const a_Position = gl.getAttribLocation((gl as any).program, 'a_Position')
// 获取存储位置其实是从 0 开始根据定义的位置依次计数的结果
if (a_Position < 0) {
console.log('Failed to get the storage location of a_Position')
return
}
// 将缓冲区对象分配给 a_Position 变量
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0)
// 连接 a_Position 变量与分配给他的缓冲区对象
gl.enableVertexAttribArray(a_Position)
return verticies.length
}
const main = () => {
if (!canvasDom.current) return
const gl = canvasDom.current.getContext('webgl')
if (!gl) {
console.log('Failed to get the rendering context for WebGL')
return
}
gl.canvas.width = (gl as any).canvas.clientWidth
gl.canvas.height = (gl as any).canvas.clientHeight
// 初始化着色器
if (!initShader(gl, VSHADERR_SOURCE, FSHADER_SOURCE)) {
console.log('Failed to intialize shaders.')
return
}
const n = initVertexBuffers(gl)
const u_Color = gl.getUniformLocation((gl as any).program, 'u_color')
const u_Resolution = gl.getUniformLocation((gl as any).program, 'u_resolution')
gl.uniform4fv(u_Color, [Math.random(), Math.random(), Math.random(), 1.0])
gl.uniform2fv(u_Resolution, [gl.canvas.width, gl.canvas.height])
// 指定清空 canavs 的颜色
gl.clearColor(0.0, 0.0, 0.0, 1.0)
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height)
// 清空 canvas
gl.clear(gl.COLOR_BUFFER_BIT)
// 第一个参数为绘制的类型,第二个参数是指定从哪个顶点开始绘制(整数型,从第一个点开始就是 0),
// 第三个参数是指定绘制需要用到多少个顶点(整数型)
// 当程序调用 l.drawArrays 方法时,顶点着色器将被执行 n 次,每次处理一个顶点。执行顶点着色器代码中的 main 函数。
// 一旦顶点着色器执行完之后,片元着色器开始执行。
if (!n) return
gl.drawArrays(gl.TRIANGLES, 0, n / 2)
}
useEffect(() => {
main()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
return (
<canvas ref={canvasDom}></canvas>
)
}
export default DrawFShape
着色器代码:
// ./glsl
export const VSHADERR_SOURCE = `
attribute vec2 a_Position;
uniform vec2 u_resolution;
void main() {
//[0, 1]
vec2 zeroToOne = a_Position / u_resolution;
//[0, 2]
vec2 zeroToTwo = zeroToOne * 2.0;
//[-1, 1]
vec2 clipSpace = zeroToTwo - 1.0;
gl_Position = vec4(clipSpace * vec2(1, -1), 0, 1);
}
`
export const FSHADER_SOURCE = `
precision mediump float;
uniform vec4 u_color;
void main() {
gl_FragColor = u_color;
}
`
构建坐标点代码:
注意第一个点是 “F” 的左边的点。
// ./df2
/**
*
* @param {*} x 横坐标位置 表示 F 的左下角的位置
* @param {*} y 纵坐标位置
* @param {*} width 宽度
* @param {*} height 高度
* @param {*} thickness 厚度
*/
export const d2_f = (x: number, y: number, width: number, height: number, thickness: number) => {
const data = [
// 左边
x, y,
x + thickness, y,
x, y + height,
x, y + height,
x + thickness, y,
x + thickness, y + height,
// 第一个横杠
x + thickness, y,
x + width, y,
x + thickness, y + thickness,
x + thickness, y + thickness,
x + width, y,
x + width, y + thickness,
// 第二个横杠
x + thickness, y + thickness * 2,
x + width * 2 / 3, y + thickness * 2,
x + thickness, y + thickness * 3,
x + thickness, y + thickness * 3,
x + width * 2 / 3, y + thickness * 2,
x + width * 2 / 3, y + thickness * 3,
]
return data
}
缓冲区对象
当我们只需要绘制一个点的时候调用 gl.drawArrays(gl.POINTS, 0, 1) 进行每次绘制一个点,但是如果我们需要绘制多个点,就需要用到缓冲区对象,他可以一次性向着色器传入多个顶点的数据,缓冲区对象是 webgl 系统中的一块内存区域,我们可以一次性地向缓冲区对象中填充大量的顶点数据,然后将这些数据保存在其中,供顶点着色器使用。
使用缓冲区对象向顶点着色器传入多个顶点的数据,需要遵循下面五个步骤:
- 创建缓冲区对象(gl.createBuffer())
- 绑定缓冲区对象(gl.bindBuffer())
- 将数据写入缓冲区对象(gl.bufferData())
- 将缓冲区对象分配给一个 attribute 变量(gl.vertexAttributPointer())
- 开启 attribute 变量(gl.enableVertexAttribArray())
总的来说使用缓冲区传递变量就上面几个步骤,在代码里;也是很详细的注释。
这里还需要注意的是,我们给顶点着色器传入了一个 u_resolution 的数值,是因为需要进行坐标的转换,因为在我们定义图形的宽高时,都是基于 canvas 页面的款到定义;但是在表达到 webgl 的坐标系统中,需要进行归一化处理。那么讲到 webgl 坐标系统,到底是什么样呢,来看下图:
图出自《WebGL 编程指南》
因为我们的世界是三维的,也不难理解 webgl 的坐标轴包含 x、y、z 三个轴。在 webgl 中,当你面向计算机屏幕时,z 轴是水平的(正方向为右),y 轴是垂直的(正方向为下),而 z 轴垂直于屏幕(正方向为外)。观察者的眼睛位于原点(0.0, 0.0, 0.0)处,视线是沿着 z 轴的的负方向。这套坐标系也叫右手坐标系。
如下图,webgl 的坐标系和 canvas 绘图区的坐标系不同,需要将前者映射到后者。默认情况下,webgl 坐标与 canvas 坐标的对应关系如下:
- canvas 的中心点: (0.0, 0.0, 0.0)
- canvas 的上边缘和下边缘:(-1.0, 0.0, 0.0) 和 (1.0, 0.0, 0.0)
- canvas 的左边缘和右边缘:(0.0, -1.0, 0.0) 和 (0.0, 1.0, 0.0)
上面的坐标只是将 canvas 的坐标转换到 webgl 的坐标系统;在 webgl 坐标系统中,我们将每个物体的坐标称为本地坐标,但是,在一个三维场景中,会有很多物体的;每个物体都相对于自身坐标存在 webgl 系统中,肯定不行的。我们会将本地坐标系通过模型矩阵转换为世界坐标。也就是不在相对于模型自身的坐标。然后再通过视图矩阵转换为视图坐标系,最终通过正射投影矩阵或者透视投影矩阵转换为裁剪坐标。最终显示在 webgl 系统中。后面我们会写模型矩阵、视图矩阵等。
还有一个需要注意的是,我们在顶点着色器设置 gl_Position 的时候乘了一个 vec2(1, -1):
gl_Position = vec4(clipSpace * vec2(1, -1), 0, 1);
是因为我们前面说过 webgl 系统的 y 轴跟我们设置的点的数据轴的方向是不一样的;乘以一个二维矢量,第一项跟 1 相乘,第二项跟 -1 相乘;对 y 进行了取反。
还有就是不知道大家有没有发现 gl_Position 为啥是四维的矢量呢,,是因为还有一个近大远小的一个系数。在后面透视投影会说到。
还有一个注意的问题就是,我们在片元着色器中,有这样一句话:
precision mediump float;
这句话是声明浮点数的精度的。因为我们在 u_Color 是传入的浮点数,这里不设定精度会报错。
接下来我们看看 bufferData 相关:
他是向缓冲区对象中写入数据,也就是描述 buffer 被绑定在哪里。 第一个参数有两个类型:gl.ARRAY_BUFFER 存顶点属性,gl.ELEMENT_ARRAY_BUFFER 存储顶点索引的类型;
最后一个参数是提示 webgl 数据将如何被使用 gl.STATIC_DRAW 表示数据通常不会发生变化,表示 gl.DYNAMIC_DRAW 会发生变化的数据
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(verticies), gl.STATIC_DRAW)
至此,绘制一个“F”图形已完成。