• 如何使用webgl绘制光照,投影等效果?
  • 发布于 2个月前
  • 175 热度
    0 评论
  • 雾月
  • 0 粉丝 27 篇博客
  •   
写在最前面

关于webgl具有哪方面的优势,已经是个老生常谈的问题了,主要是利用了其并行化的优势,这对于图形学领域尤为重要,可以利用这个优势大批量绘制复杂图形,其次,webgl相对于别的图形系统,类似canvas、svg等,webgl相对而言更底层一些,可以完整参与一个图形生成到渲染的每一个细节,这也是我在稍微了解了three.js的相关api后选择直接all in webgl的理由。


接下来会从webgl的简单绘制,光照,投影变换等多个维度去介绍,记录一下自己的理解,仅供大家参考。

第一个可以运行的webgl demo
webgl枯涩的理论往往会让人望而却步,我打算从零到一完成一个demo快速入门。
在最开始,我们需要有一个html模板:
<body>
    <canvas id="canvas"></canvas>
</body>
首先对比一下canvas的绘图流程,首先会有一个上下文,对于webgl也是同理:
const canvas = document.querySelector('#canvas');
const gl = canvas.getContext('webgl');
接下来我们就需要了解着色器的概念了,就是大名鼎鼎的shader。着色器分为顶点着色器与片元着色器,两者都是采用GLSL语法编写,关于语法的细节后面会介绍,熟悉简单C语言即可看懂代码,这两者的区别在于:顶点着色器负责处理图形顶点信息,片元着色器负责处理图形的颜色信息!
关于两者是如何协作将图形呈现在我们面前的?我们也有必要进行一些梳理。
1. 首先,顶点着色器接收外界输入的坐标,然后将顶点装配成相应的图元类型,这一步由WEBGL的绘图模式决定,包括点、线与三角形。
2. 然后WEBGL会根据顶点与对应模式计算出需要着色的像素点,这一步称之为光栅化。
3. 接下来需要用到我们参与到的片元着色器对光栅化后的像素信息进行处理。上述是一个WEBGL绘制的基本流程

下面先以一个简单的三角形举例,给出两段着色器代码,稍后给出解释。
const vertex = `
    attribute vec2 position;
    void main() {
        gl_PointSize = 1.0;
        gl_Position = vec4(position, 1.0, 1.0);
    }
`;
// 堆代码 duidaima.com
const fragment = `
    precision mediump float;
    void main() {
        gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
    }
`;
初学者看到这GLSL写的东西肯定是一脸懵逼,我们仅针对当前的例子解释一下吧:
gl_Position: webgl程序接收的坐标称之为设备坐标系(NDC),可以理解为一个边长为2的正方体,x/y/z三个方向分别落在(-1, 1)的范围,这是一个右手坐标系,也就是外界比如真实canvas上的坐标都要先转为NDC坐标才能被webgl所识别

gl_FragColor: vec4类型,包括R、G、B、A四个颜色分量,每个分量的2取值在0-1之间
gl_PointSize: 仅在绘制图元为点的时候生效,代表绘制点的大小

以上都属于内置的变量,需要注意的是,GLSL是一种强类型语言,没有支持自动强制转换类型,类似gL_fragColor是一种四元浮点向量,不能用[1, 1, 1, 0]来表示。
到这一步为止,我们基本上了解了着色器的概念,接下我们就需要去创建一个GPU可以执行的程序。
直接上相关代码:
// 创建shader对象
const vertexShader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vertexShader, vertex);
gl.compileShader(vertexShader);

const fragmentShader = gl.createShader(gl.FRAG MENT_SHADER);
gl.shaderSource(fragmentShader, fragment);
gl.compileShader(fragmentShader);
首先是创建shader对象,用gl.createShader分别创建顶点着色器与片元着色器对象,gl.shaderSource将shader源码分配给着色器对象,最后gl.compilerShader用于编译
// 创建webglProgram对象 关联shader
const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
gl.useProgram(program);
其次是创建webglProgram对象,gl.createProgram用于创建着色器程序,gl.attachShader用于将相应的着色器挂载在着色器程序上,接下来gl.linkProgram用于链接着色器程序。通常我们应用可能会包括多个webglProgram对象,所以在使用前还需要启用之,即gl.useProgram 。

以上是创建webgl应用程序的基本工作流,基本上是调用webgl基础api的过程,虽然有点绕但不算难,后续也可以抽离出来。


接下来就是需要将javascript代码中的数据与program交互的过程了
那js应如何往着色器中传递数据呢?
首先我们可以在外部定义好要绘制的三角形的顶点,这里我们省略屏幕坐标向NDC坐标系归一化的过程,直接定义[-1, 1]范围内的点
const points = new Float32Array([-1, -1, 0, 1, 1, -1]);注意这里向着色器程序需要传入强类型的变量!

这里涉及一个缓冲区的概念,借助缓冲区我们可以向着色器传递多个顶点。
流程也是相对固定:
1.首先创建一个缓存区:const bufferId = gl.createBuffer();
2.将当前的缓存区绑定至WEBGL当前操作对象:gl.bindBuffer(gl.ARRAY_BUFFER, bufferId)
3.最后将向缓存区写入数据:gl.bufferData(gl.ARRAY_BUFFER, points, gl.STATIC_DRAW),最后一个参数主要是提示webgl根据不常改变的值做一些优化。

现在着色器的程序还不能读取这些数据,我们还需要将缓存区的的数据绑定至着色器,还记得上面定义的顶点着色器中定义的变量吗?
attribute vec2 position;
先将其取出来,const vPosition = gl.getAttribLocation(program, 'position');
接下来的步骤比较重要,关于给该变量设置长度与类型,gl.vertexAttribPointer(vPosition, 2, gl.FLOAT, false, 0, 0)
单独拎出来讲讲:用到的api是gl.vertexAttribPointer,我们看一下他的函数签名:
gl.vertexAttribPointer(target, size, type, normalize, stride, offset)
// targe较好理解,这里是目标属性vPosition
// size: 可以理解为一次性取几个值,因为这里position是vec2类型,所以size为2
// type: 这里为浮点型
// normalize: 是否要归一化到[-1, 1]的区间内
// stride: 步长默认为0,每个顶点所包含的字节数,这里一个节点包含2个Float变量,步长为 2* 4 = 8,如果多个属性共用同一个缓存区,需要定义该属性
// offset: 偏移量默认为0,指偏移多少字节开始读取目标属性
然后启用对应的属性gl.enableVertexAttribArray(vPosition);到这一步为止,我们可以将javascript中定义的变量,通过buffer方式让shader读取到了,最后一步就是绘制了。
gl.clear(gl.COLOR_BUFFER_BIT); // 先清除屏幕
gl.drawArrays(gl.TRIANGLES, 0, 3); // 绘制三角形图元

关于顶点着色器,还要补充的是,他除了可以设置顶点坐标等属性外,还可以向片元着色器传递数据。

举个例子,我们在顶点着色器与片元着色器中分别定义一个变量varying vec3 color,然后我们将color映射为与position相关,映射关系为x = 0.5 + position * 0.5,由于position落在[-1, 1]区间内,那么x落在[0, 1]区间内,维度与position一致,也是vec2,重新构造的color = vec3(0.5 + position * 0.5, 0.0),这样color与position之间就建立起了联系,经过映射三个顶点不再是同一个颜色,而是红绿黑三种颜色,片元着色器会根据像素的坐标进行线性插值,最后呈现的就是渐变的效果了。

总结
到这为止,我们简单介绍了webgl绘图的基本流程,顶点着色器的概念与用法,这里看上去有很多底层的api,但是流程相对固定,也不必望而却步,也有很多开源的库会帮我们简化这些基础流程。但是这些基础原理我觉得还是有必要接触的。
用户评论