Intro

不论是WebGL、OpenGL、OpenGL ES、Vulkan、DirectX,这些听起来就十分“底层”、“高性能”、“难写”的东西似乎是和我一个后端开发都没什么关系。(远处传来声音:别tm擅自改台词!)

咳,回归正题。

我看了好多的 OpenGL 入门书,固定管线的比较好懂,但过时已久。新的可编程管线则十分晦涩——没有人试图解释一个绘制三角形的简单 OpenGL 程序有哪些细节,红宝书也是,上手便是一个"Framework",面对大片几十上百行代码,试图理解的人总会遇到各种“这个常量是什么意思?glBind是在干嘛?三角形是怎么画出来的?”

实在太魔术了。

所以本博文试图从一个更全局的角度,去解释一个最简单的 画三角形 WebGL 程序都做了什么。

零、纵观全局

一个OpenGL的Hello World无非是做了这些事情。

  1. 创建shader
  2. 编译shader
  3. 链接shader为program
  4. 创建/初始化顶点buffer
  5. 将顶点buffer传递给顶点着色器,完成渲染

一、着色器

着色器是一段在GPU上运行的程序,它不是脚本,有使用过编译语言的同学应该明白,C要编译成一个exe需要经过编译、链接这两个步骤,同样的,着色器程序也需要这两个步骤。

对于单个着色器我们需要先使用gl.compileShader(shader)来编译,这个api不会直接返回错误,而是设置了一个全局的错误代码,通过gl.getError来取得。十分老派的posix/unix/c风格错误处理api,不是吗。

着色器编译不会带来任何可见的改变,我们持有的shader对象本质上是一个指向黑箱的索引,编译好shader之后我们使用gl.createProgramgl.attachShadergl.linkProgram来创造一个可用的着色器程序,就像是我们把一段c代码编译成了可以在gpu上跑的exe。

看一个示例

const vs = gl.createShader(gl.VERTEX_SHADER); // 顶点着色器
const fs = gl.createShader(gl.FRAGMENT_SHADER); // 片元着色器

gl.shaderSource(vs, `... 顶点着色器代码略`); // 指定 shader 源码
gl.shaderSource(fs, `... 片元着色器代码略`); // 指定 shader 源码

gl.compileShader(vs); // 编译顶点着色器
if(!gl.getShaderParameter(vs, gl.COMPILE_STATUS))
    console.log(`顶点着色器编译错误:${gl.getShaderInfoLog(vs)}`);

gl.compileShader(fs); // 编译片元着色器
if(!gl.getShaderParameter(fs, gl.COMPILE_STATUS))
    console.log(`片元着色器编译错误:${gl.getShaderInfoLog(fs)}`);

const program = gl.createProgram(); // 创建一个着色器程序
gl.attachShader(program, vs); // 指定要链接的 顶点着色器
gl.attachShader(program, fs); // 指定要链接的 片元着色器
gl.linkProgram(program); // 链接着色器程序

if (!gl.getProgramParameter(program, gl.LINK_STATUS))
    console.log(`着色器程序链接失败:${gl.getProgramInfoLog(program)}`);

大多教程都封装了这个加载、创建着色器程序的细节,通常来说像是叫做createShaderloadShader的做的事情和以上代码做的差不多。

GLSL

glsl是OpenGL的着色器语言,可以理解为和C语言类似的编译型语言,不过因为是在GPU上跑的缘故,所以有诸多限制,比如说不能递归。

从一个简单的顶点着色器说起。

#version 120
attribute vec4 position;

void main() {
    gl_Position = position;
}

上面这个顶点着色器和一个普通C程序类似,但有三个差异点。

差异一:#version

#version 120指定GLSL的版本,GLSL版本和OpenGL版本之间有个对照表。因为OpenGL细节是由驱动实现的,驱动支持到哪个版本的OpenGLGLSL最多也就是跟进到那个版本而已。

差异二:attribute

attribute只能在顶点着色器里,去声明一个可以被GLSL外的环境修改的变量,由于现在说的是WebGL,所以这个_GLSL外的环境_指的就是js了。

attribute赋值的方式是通过两个api:gl.getAttribLocationgl.vertexAttribPointer

其中gl.getAttribLocation可以获得指定attribute名字的位置——用函数参数打比方的话,就是这个attribute是第几个参数。

const posAttribLocation = gl.getAttribLocation(program, "position");

有了这个posAttribLocation,我们就能用gl.vertexAttribPointer来赋值数据了。

const vertexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Flaot32Buffer([
    0.75, 0.75, 0, 1.0,
    0.75, -0.75, 0, 1.0,
    -0.75, -0.75, 0, 1.0,
]), gl.STATIC_DRAW);
gl.vertexAttribPointer(posAttribLocation, 4, gl.FLOAT, false, 0, 0);

还记得OpenGL是状态机模型吧?

gl.vertexAttribPointer之所以不需要传给他要用哪个buffer,是因为我们前面先做了一个gl.bindBuffer

差异三:gl_Position

按照网上解释,gl_Position是当前在处理的那个顶点在处理结束之后的位置,是一个GLSL内置的变量,它的存在可以用C里面extern vec4 gl_Position的声明来类比。不过在GLSL里,gl_Position是不需要声明的。

11-11 02:26