画一个三角形(上)

大家好,我是西瓜,你现在看到的是 2021 iThome 铁人赛『如何在网页中绘制 3D 场景?从 WebGL 的基础开始说起』系列文章的第 2 篇文章。本系列文章从 WebGL 之基础开始介绍,最後建构出绘制 3D、光影效果之网页。本章节讲述的是 WebGL 基本的运作机制以及如何使用其提供的功能

在让电脑绘制一个三维场景时,我们实际在做的事情把这三维场景中物体的『表面』画在画面上,而构成一个面最少需要三个点,三个点构成一个三角形,而所有更复杂的形状或是表面都可以用复数个三角形做出来,因此使用 3D 绘制相关的工具时基本的单位往往是三角形,我们就来使用 WebGL 画一个三角形吧!

WebGL 的绘制流程

在使用 WebGL 时,你写的主程序 (.js) 在 CPU 上跑,透过 WebGL API 对 GPU 『一个口令,一个动作』;不像是 HTML/CSS 那样,给系统一个结构,然後系统会根据这个结构直接生成画面。而且我们还要先告诉好 GPU 『怎麽画』、『画什麽』,讲好之後再叫 GPU 进行『画』这个动作

『怎麽画』

我们会把一种特定格式的程序(program)传送到 GPU 上,在『画』的动作时执行,这段程序称为 shader,而且分成 vertex(顶点)及 fragment(片段)两种 shader,vertex shader 负责计算每个形状(通常是三角形)的每个顶点在画布上的位置、fragment shader 负责计算填满形状时每个 pixel 使用的颜色,两者组成这个所谓特定格式的程序

『画什麽』

除了 shader 之外,还要传送给程序(主要是 vertex shader)使用的资料,在 shader 中这些资料叫做 attribute,并且透过 buffer 来传送到 GPU 上

『画』这个动作

首先执行 vertex shader,每执行一次产生一个顶点,且每次执行只会从 buffer 中拿出对应的片段作为 attribute,接着 GPU 会把每三个顶点组成三角形(模式是三角形的话),接着点阵化(rasterization)以对应萤幕的 pixel,最後为每个 pixel 分别执行 fragment shader

以接下来要画的三角形为例,笔者画了简易的示意图表示这个流程:

draw-flow

为什麽是这样的流程其实笔者也不得而知,或许就是维基百科 openGL 页面这边所说的:『它是为大部分或者全部使用硬体加速而设计的』,稍微想像一下,每个顶点位置以及每个 pixel 着色的计算工作可以高度平行化,而在显示卡硬体上可以针对这个特性使这些工作平行地在大量的 ALU / FPU 上同时计算以达到加速效果

建立 shader

当笔者第一次看到这个的时候,第一个反应是『原来可以在浏览器里面写 C 呀』,这个语言称为 OpenGL Shading Language,简称 GLSL,虽然看起来很像 C 语言,但是不能直接当成 C 来写,他有自己的资料格式,我们直接来看画三角形用的 vertex shader:

attribute vec2 a_position;
 
void main() {
  gl_Position = vec4(a_position, 0, 1);
}
  • 每次 shader 执行时跑 void main()
  • attribute vec2 a_position 是从 buffer 拿出对应的部份作为 attribute 变数 a_position,型别 vec2 表示有两个浮点数的 vector
    • 接下来要绘制的三角形在 2D 上,只需要 x, y 即可,因此使用 vec2
  • gl_Position 是 GLSL 规定用来输出在画布上位置的变数,其型别是 vec4
    • 这个变数的第一到第三个元素分别是 x, y, z,必须介於 -1+1 才会落在画布中,这个范围称为 clip space
    • vec4() 建构一个 vec4,理论上应该写成 vec4(x, y, z, w),因为 a_positionvec2,这边有语法糖自动展开,所以也可以写成 vec4(a_position[0], a_position[1], 0, 1)
    • 第四个元素我们先传 1,到後面的章节再讨论

假设有个 vec4 的变数叫做 var,不仅可以使用 var[i] 这样的写法取得第 i 个元素(当然,从 0 开始),还可以用 var.x / var.y / var.z / var.w 取得第一、第二、第三、第四个元素,甚至有种叫做 swizzling 的写法:var.xzz 等同於 vec3(var[0], var[2], var[2])

这个 shader 其实没做什麽事,只是直接把输入到 buffer 的位置资料放到 gl_Position,接着是 fragment shader,这次更简单了:

void main() {
  gl_FragColor = vec4(0.4745, 0.3333, 0.2823, 1);
}
  • 每个 pixel 都要跑一次 void main()
  • gl_FragColor 是 GLSL 规定用来输出在画布上颜色的变数,其型别是 vec4
    • 各个元素分别是介於 01 之间的 red, green, blue, alpha

为了不要让资讯量太爆炸,我们先不要介绍更多功能,这个 fragment shader 只会输出一种颜色,所以我们会得到的三角形是纯色的

编译、连结 shader 成为 program

由於 shader 建立的 WebGL API 实在太繁琐,这边直接建立两个 function:

function createShader(gl, type, source) {
  const shader = gl.createShader(type);
  gl.shaderSource(shader, source);
  gl.compileShader(shader);
  const ok = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
  if (ok) return shader;

  console.log(gl.getShaderInfoLog(shader));
  gl.deleteShader(shader);
}

function createProgram(gl, vertexShader, fragmentShader) {
  const program = gl.createProgram();
  gl.attachShader(program, vertexShader);
  gl.attachShader(program, fragmentShader);
  gl.linkProgram(program);

  const ok = gl.getProgramParameter(program, gl.LINK_STATUS);
  if (ok) return program;

  console.log(gl.getProgramInfoLog(program));
  gl.deleteProgram(program);
}

我们可以分别把 vertex shader, fragment shader 的 GLSL 原始码以 template literals (backtick 字串) 写在 .js 中,并传给 createShader(gl, type, source)source 进行『编译』:

const vertexShaderSource = `
attribute vec2 a_position;
 
void main() {
  gl_Position = vec4(a_position, 0, 1);
}
`;
 
const fragmentShaderSource = `
void main() {
  gl_FragColor = vec4(0.4745, 0.3333, 0.2823, 1);
}
`;

const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource);
const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource);

编译完成後,使用 createProgram(gl, vertexShader, fragmentShader) 把 GPU 内『怎麽画』的流程串起来:

const program = createProgram(gl, vertexShader, fragmentShader);

这样一来 program 就建立完成,下一篇我们再继续『画什麽』的资料部份

本篇的完整程序码可以在这边找到:


<<:  33岁转职者的前端笔记-DAY 2 如何处理 BUG 及遇到 BUG 的心态

>>:  DAY02 初探资料分析

iOS APP 开发 OC 第九天,UIWebView & WKWebView

tags: OC 30 day 我们来延续上一篇网路请求原理做出UIWebView吧 把网路请求做成...

[Day 15] Sass - Loop

Hi 终於来到第15天了(一半了!!!) 今天要写的是关於Sass-Loop回圈,回圈很常与前几天介...

Day 2:根基不牢,怎麽建高楼.来聊聊架构.

Keyword: MVC,MVP,MVVM 在使用KMM上,架构是重中之重.如果使用了好的架构并且遵...

Day12 跟着官方文件学习Laravel-Session

因为Http是无状态的,我们可以利用session让使用者表明自己的身份。 首先我们必须先建立一个s...

宝塔面板操作日志定时清理

这里删除的仅是面板的操作日志,与网站日志无关联 Linux宝塔操作记录日志路径: /www/serv...