大家好,我是西瓜,你现在看到的是 2021 iThome 铁人赛『如何在网页中绘制 3D 场景?从 WebGL 的基础开始说起』系列文章的第 2 篇文章。本系列文章从 WebGL 之基础开始介绍,最後建构出绘制 3D、光影效果之网页。本章节讲述的是 WebGL 基本的运作机制以及如何使用其提供的功能
在让电脑绘制一个三维场景时,我们实际在做的事情把这三维场景中物体的『表面』画在画面上,而构成一个面最少需要三个点,三个点构成一个三角形,而所有更复杂的形状或是表面都可以用复数个三角形做出来,因此使用 3D 绘制相关的工具时基本的单位往往是三角形,我们就来使用 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
以接下来要画的三角形为例,笔者画了简易的示意图表示这个流程:
为什麽是这样的流程其实笔者也不得而知,或许就是维基百科 openGL 页面这边所说的:『它是为大部分或者全部使用硬体加速而设计的』,稍微想像一下,每个顶点位置以及每个 pixel 着色的计算工作可以高度平行化,而在显示卡硬体上可以针对这个特性使这些工作平行地在大量的 ALU / FPU 上同时计算以达到加速效果
当笔者第一次看到这个的时候,第一个反应是『原来可以在浏览器里面写 C 呀』,这个语言称为 OpenGL Shading Language,简称 GLSL,虽然看起来很像 C 语言,但是不能直接当成 C 来写,他有自己的资料格式,我们直接来看画三角形用的 vertex shader:
attribute vec2 a_position;
void main() {
gl_Position = vec4(a_position, 0, 1);
}
void main()
attribute vec2 a_position
是从 buffer 拿出对应的部份作为 attribute 变数 a_position
,型别 vec2
表示有两个浮点数的 vector
vec2
gl_Position
是 GLSL 规定用来输出在画布上位置的变数,其型别是 vec4
-1
至 +1
才会落在画布中,这个范围称为 clip spacevec4()
建构一个 vec4
,理论上应该写成 vec4(x, y, z, w)
,因为 a_position
是 vec2
,这边有语法糖自动展开,所以也可以写成 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);
}
void main()
gl_FragColor
是 GLSL 规定用来输出在画布上颜色的变数,其型别是 vec4
0
到 1
之间的 red, green, blue, alpha为了不要让资讯量太爆炸,我们先不要介绍更多功能,这个 fragment shader 只会输出一种颜色,所以我们会得到的三角形是纯色的
由於 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 的心态
tags: OC 30 day 我们来延续上一篇网路请求原理做出UIWebView吧 把网路请求做成...
Hi 终於来到第15天了(一半了!!!) 今天要写的是关於Sass-Loop回圈,回圈很常与前几天介...
Keyword: MVC,MVP,MVVM 在使用KMM上,架构是重中之重.如果使用了好的架构并且遵...
因为Http是无状态的,我们可以利用session让使用者表明自己的身份。 首先我们必须先建立一个s...
这里删除的仅是面板的操作日志,与网站日志无关联 Linux宝塔操作记录日志路径: /www/serv...