【前端er入门Shader系列】01—从渲染管线了解Shader
Write By CS逍遥剑仙
我的主页: www.csxiaoyao.com
GitHub: github.com/csxiaoyaojianxian
Email: sunjianfeng@csxiaoyao.com
1. 经历分享:前端er如何入门web游戏开发
对于深耕web前端多年的开发者来说,大多有过开发web轻游戏应用的梦想。毕竟前端的本质是为用户提供令人愉悦的交互体验,而游戏作为第九艺术,更是交互艺术的巅峰,二者有异曲同工之妙。
这些年web浏览器的飞速发展让前端er群体不断壮大,而 HTML5中的 canvas 更是为交互式应用提供了无限可能!web浏览器跨平台和统一技术栈的特性也逐渐影响到了现代游戏开发,涌现了大量的 web 游戏(包括微信、抖音小游戏等),这也为前端er开发跨端的轻游戏应用提供了契机,前端开发者熟悉的web技术可以无缝地应用于图形渲染框架和游戏引擎,如WebGL、Three.js、cocos、laya等等。注意,这里的 web 小游戏指的是基于 HTML5 的 canvas 画布逐帧绘制的 2D/3D 应用程序,虽然基于 HTML 的 DOM 也能制作一些交互体验很棒的小游戏,但想要高效实现以及高性能地渲染更复杂的图形和动画,canvas 才是不二之选。
作为过来人,因为自身兴趣推动业务需求,亲身经历过几个 web 小游戏的独立开发后,回顾学习经历,从学习 C4D / Blender 建模起步进入3D世界,到使用 Three.js / A-Frame / Cannon.js 等库绘制3D场景,再到应用 cocos creator / LAYABOX 游戏引擎实现稍复杂的游戏场景和交互。
在这几个模块的学习中,对个人影响最大的是 Three.js,跟随着《Three.js Journey》课程学习完成后,会从宏观上对整个 3D 世界有较为清晰的认识。事实上大多数前端er的 web 3D 学习路径也都是从 Three.js 开始的,可以轻松在浏览器中渲染一些3D场景并实现一些简单交互,如看房网页中的3D全景图、车企网页中的3D车模展示等等。本人的第一个 web 小游戏需求就是使用 Three.js 实现的 3D 坦克驾驶和射击,用于为 CFM 载具版本预热,可以 B 站搜索《战垒驾照考试》,整体场景渲染很粗糙,功能也很简陋,在部分机型上还存在性能问题。
相信我,当你打开 3D 世界的大门后,一定会迫不及待地学习 3D 建模(甚至整一台 3D 打印机)
随着开发的 3D 游戏场景复杂度不断提升,会发现资源、逻辑管理难度加大、出现各类性能问题,这时候游戏引擎就可以大显身手了,基于 ECS 架构的 cocos creator 可以轻松应对,还能够打包为微信/抖音小游戏等各类小游戏平台包。下面是本人开发的第一个基于 cocos 游戏引擎的微信/QQ小游戏,可以 B 站搜索《钻石推推乐》。
游戏引擎的学习,对于 web 游戏新手入门,若不是业务需求 push,建议浅尝辄止,还是从 WebGL 开始探索。笔者一直认为,框架的目的在于提升开发效率,而不是为了回避基础知识的学习,游戏领域的概念和知识点繁杂,直接上手框架和引擎会事倍功半。前端er打好前端三件套 html / css / js 的基础后,各类框架的学习自然手到擒来。同样的,web小游戏的开发也可以先从 WebGL 的渲染起步,在打好渲染的基础后,游戏引擎也能够随心所欲的应用起来。
回归文本的主题,而 WebGL 的学习重点和难点体现在 Shader 的编写。个人认为 web 小游戏和 web 前端最大的区别在于引入了较多计算机图形学(Computer Graphics,简称CG)的概念,尤其是渲
本系列文章分10个章节,前几节讲述 OpenGL 的渲染管线和 Shader 语法,然后讲述在 cocos 引擎中的应用,最后几节会讲述一些常用的特效渲染案例,旨在为前端开发者开发 Web 小游戏输出相关技术扫盲,希望看完本文后,能消除读者碰到 Shader 代码时的恐惧心理。
2. Shader可以做什么
前面提到的 Shader 是一种运行在 GPU 上的图形程序,也叫可编程着色器,可创建出逼真和吸引人的画面效果。虽说 Shader 本身的程序 api 并不算复杂,但对于初入游戏开发领域的新人而言涉及到的知识面较广,涉及到数学、图形学、图形 API、GPU、显卡等基础知识,还对观察力和想象力有着较高的经验要求,往往是入门即劝退。
但也不用担心,在实际的开发中,大多数的场景渲染都可以直接复用现成的 Shader 处理顶点的位置、光照、阴影、材质和纹理等,甚至是序列帧、蒙皮、骨骼等动效,开发者只需要调节一些参数即可。
开发者往往通过编写 Shader 程序来生成各种视觉特效,如描边、消融、光效,叠加烟雾、火焰、粒子,或是实现物理模拟,甚至是一些逻辑处理,如小兵的位置移动、动态渲染小兵的布局排列、修改敌人的转角朝向等等。
相对于使用贴图、帧动画实现各类效果,用 Shader 做纹理动画不仅灵活,还可以减轻CPU负担,还能够多动画同时绘制,实现特殊效果。
shadertoy 上分享了许多 Shader 大神的作品,推荐观看,打开眼界
总而言之、言而总之,Shader 是一门神奇的计算机图形学编程艺术,它可以天马行空地操纵渲染,它无所不能!
3. 概念扫盲:OpenGL / WebGL / canvas / Shader / GLSL
OpenGL 全称 "Open Graphics Library",中文名为开放图形库,定义了一套跨平台的图形渲染 API 协议,用于创建 2D 和 3D 图形,能够在多种操作系统和硬件平台上运行,类似的协议还有微软的 DirectX。
WebGL(Web Graphics Library) 则是基于 OpenGL ES 2.0 的 JavaScript API,用于在 Web 浏览器中调用 GPU 能力呈现交互式的 2D 和 3D 图形。
canvas 是一种 HTML5 标签,提供了 JavaScript 操作 WebGL API 来绘制图形的画布,支持 2D 和 3D 两种模式。
Shader 指的是 OpenGL 整个渲染管线中存在可编程节点,大多数场景下开发者只需要处理 Vertex Shader 和 Fragment Shader 这对编程节点即可,一个顶点着色器和一个片断着色器合起来称作一个 program (着色程序)。
GLSL(OpenGL Shading Language) 是 OpenGL 中编写 Shader 的一门 GPU 编程语言,Shader 编程的核心就是使用 GLSL 编写顶点着色器和片断着色器,优秀的 GLSL 代码能显著提升运行性能。
4. 渲染管线总览:屏幕像素的渲染流程
先说明一个常识:两点确定一条直线,三点确定一个三角形,n边形可由(n-2)个三角形组成,任何复杂的场景都能用点线三角面实现。
因此在 OpenGL 中,可以通过拆解为 点(point)、线(Line Segment)、三角形(Triangle) 完成对各类复杂图形的绘制,虽然物体存在于 3D 空间中,但最终是以显示器 2D 像素呈现给用户,这个过程即为渲染管线,渲染引擎底层的大部分工作是根据 Shader 程序在 GPU 中将 3D 坐标转换成屏幕 2D 像素,包含两个核心流程,对应两个可编程节点:
-
3D坐标 => 2D坐标,将顶点数据转换到齐次裁剪空间坐标,对应 Vertex Shader
-
2D坐标 => 颜色像素,基于转换后的坐标绘制屏幕像素点,对应 Fragment Shader
这个过程能够充分发挥 GPU 的特性,因为 GPU 由大量的小型处理单元构成,虽然单个处理能力远不及 CPU,但胜在数量多且能够并行处理。
渲染管线整体流程如上图所示,几个阶段的核心功能分别为:
- 顶点数据:在数据准备阶段,JavaScript等外部程序可以从 CPU 向 GPU 缓存区传入数据,以便后面的顶点着色器和片段着色器读取并处理,送入到渲染管线的数据包括顶点的坐标、法线、切线、纹理坐标、颜色等属性。
- 顶点着色器:顶点着色器主要实现顶点坐标从本地空间到屏幕空间的转换,如下图所示:
- 图元装配:该阶段将顶点着色器输出的所有顶点作为输入,根据绘制方式将所有的点装配成指定的图元形状,以及执行 Face Culling 面剔除。OpenGL提供了七种基本形状的绘制方式,如下图所示,注意关注点的连接方式。
-
光栅化:该阶段将图元处理为像素,通过遍历所有像素,判断像素中心是否落入三角形图元内,来决定是否进行下一步着色操作,如下图所示。
-
片段着色器:该阶段对图形内的片元进行着色处理,能够实现一些炫酷的高级效果。片段着色器通常包含 3D 场景数据(如光照、阴影、光的颜色等),用于计算最终像素的颜色。
-
测试与混合:虽然片元着色器输出了颜色缓冲,但现实 3D 场景还需要考虑远近遮挡、半透明物体透视等因素,所以必须经过模板、深度和混合测试后才能最终输出颜色缓冲。
(1) 模板测试(Stencil Test) 通过每个像素/片段的8位模板掩码值确定片段的丢弃或保留,用于裁剪出特定的形状;
(2) 深度测试(Depth test) 在颜色被写入帧缓冲区之前会进行深度测试,通过检测片段的深度来判断像素的前后关系和透明度来丢弃较后绘制但被"遮挡"的像素,从而保证渲染结果的正确性。深度测试提供了
>/>=/==/</<=/!=/always/never比较运算符,默认为<=表示通过测试。开启后会在深度缓冲中存储每个片段的z深度值(16/24/32位float,一般默认精度为24),用当前渲染的每个片段的深度值与深度缓冲值对比测试,若测试通过则更新深度缓冲中的深度值,若测试失败则丢弃片段。深度测试是在屏幕空间中运行的,深度值在 0.0 ~ 1.0 之间(0近处/1远处),实体对象一般都会开启深度测试和深度写入,但像技能特效中的半透明效果,只进行深度测试不进行深度写入。总结如下:非透明物体的深度写入开启,深度测试开启,绘制顺序可以与远近无关。
半透明物体的深度写入关闭,深度测试开启,需要按从远及近的顺序进行绘制。(3) 混合测试(Blend test) 对于半透明的物体,颜色值中会使用 a 分量表示透明度,例如 0.2 表示保留 20% 本体颜色,剩下 80% 使用物体背后的颜色,无法简单用片段的保留/丢弃的方式完成半透明物体渲染,因此需要混合测试,它会将当前像素的值按照设定的混合方式,与目标颜色缓冲区的值进行融合。开启后的渲染顺序为:优先从近向远渲染不透明物体,渲染过的像素视为遮挡跳过,然后从远向近混合渲染透明的物体且不开启深度测试。颜色缓冲区的最终值会被渲染到屏幕上。
5. WebGL程序的基本结构
使用 GLSL 语言编写的 Shader 可以在浏览器中通过 WebGL 运行,基本结构如下所示:
<!DOCTYPE html>
<html lang="en">
<body>
<canvas id="webgl" width="400" height="400">浏览器不支持HTML5</canvas>
<script>
// 顶点着色器
const vertexcsxiaoyao.com Source = `
void main() {
gl_Position = vec4(0.0, 0.0, 0.0, 1.0); // xyzw
gl_PointSize = 10.0; // 点大小
}`;
// 片段着色器
const fragmentSource = `
void main() {
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); // rgba 红色
}`;
// 【1】初始化编译shader
const canvas = document.getElementById("webgl");
const gl = canvas.getContext("webgl");
const vertexShader = gl.createShader(gl.VERTEX_SHADER);
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(vertexShader, vertexSource);
gl.shaderSource(fragmentShader, fragmentSource);
gl.compileShader(vertexShader);
gl.compileShader(fragmentShader);
const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
gl.useProgram(program);
// 【2】清空画布
gl.clearColor(0.5, 0.5, 0.5, 1.0); // rgba 灰色
gl.clear(gl.COLOR_BUFFER_BIT); // 确认清空
// 【3】执行绘制图元
/**
* @param mode 图元类型,符号常数 GL_POINTS、GL_LINE_STRIP、GL_LLINE_LOOP、GL_LONES、GL_LINE_STRIP_ADJACENCY、GL_LANES_ADJACCENCY、GL_TRIANGLE_STRI、GL_TROANGLE_FAN、GL_TRAANGLES、GL_TTRINGLE_STIP_ADJACENCY、GL_PATCHES
* @param first 数组起始索引
* @param count 索引数量
*/
gl.drawArrays(gl.POINTS, 0, 1);
</script>
</body>
</html>
WebGL 依赖 canvas 载体获取绘图上下文,调用绘图 API 执行绘制。一个 program 必须同时包含一对着色器:gl.VERTEX_SHADER 和 gl.FRAGMENT_SHADER。
在 Web 浏览器中的 Shader 程序以js模板字符串传入,通过调用 gl.drawArrays 或 gl.drawElements 运行一个着色方法对在 GPU 中绘制图元。在图形渲染管线中提到过,WebGL 可以绘制点、线、三角形。上面是最基础的程序结构,在灰色的画布中心绘制了一个大小为 10 的红色点,对于点的位置使用了 vec4 向量来描述,其实 [x,y,z,w] 前三个分量 xyz 已经能描述三维坐标位置,第四维是为了方便做矩阵运算,默认 1.0,需要注意 GLSL 是类似 C / C++ 的强类型语言,1 和 1.0 不同。










