第0讲 写在前面的话
关于shader
本帖主要介绍shader的原理以及在gms2中的应用方式。因此看完此贴,你很有可能还是写不出好的shader特效,这好比我告诉你每种画笔的用法但是还是画不出好看的画一样。shader的原理其实不难,语法也不难,难是难在各种特效的算法上。本帖仅仅介绍原理,讲述shader是如何在gms2中工作的。由于shader本身的特性,本帖会基于3d绘制进行讲解。
关于3d
本来gm8中的3D是用d3d函数实现的,但是到了gms2中yoyo把d3d废除了,于是就有人以为gms2做不了3d了,其实不是这样的。d3d函数被废除了,但是其功能还是以其它形式保留着的,有很多更基础更高效的函数代替了。如果你导入使用了d3d函数的gm8工程,gms2会给你自动添加d3d兼容性脚本,你可以查看里面的内容来学习如何在gms2中书写d3d的功能,但是我建议你最好还是在看懂原理之后自己造轮子,因为这些兼容性脚本为了兼容,效率会比较低。顺便一说,代码运行效率低是gms2制作3d游戏的巨大障碍。如果你想用gms2制作华丽的3d游戏请谨慎,到后期,你将不得不对代码进行各种优化。另外提一嘴,能绘制3d图形跟能做3d游戏还是差别蛮大的。目前来说gms2最多只能做到前者。
先抛出几个重要结论
想要用gms2做3d,请务必先学好gms2中的以下内容,它们是制作3d的基础:相机(camera)、矩阵(matrix)、图元绘制(vertex_submit)、着色器(shader)、表面(surface)、缓冲区(vertex_buffer)、GPU和贴图(texture)相关函数。俗称7颗知识碎片。集齐7颗碎片,你就会做3d了(雾)。这些知识都不是互相独立的而是相互交叉,想全部学会还是挺不容易的,本人也是经过啃原版f1多年才逐渐领会到这些门道的。
与unity一样,gms2使用的也是3d相机。也就是说gms2跟unity一样,相机天生就是3d的。通过修改相机矩阵就能发现这个结论。也就是说yoyo是使用的3d的相机给我们制作了一个2d的轮子然后再让我们制作2d的游戏的。里面的各种绘制函数也统统是封装成2d的格式给咱们用的,它们的内核其实还是3d。
绘制的本质是图元(primitive)绘制(或者说顶点(vertex)绘制),gms2中所有的绘制函数比如draw_sprite、draw_surface之类的,其实底层都是使用的图元绘制,而这也是使用shader绘制的唯一方法。也就是说,如果想要学会shader那首先就得先学会图元绘制。而且shader的绘制,本质上也是基于3d绘制的。因此shader和3d密不可分。
看到这里你会发现,想学shader和3d不是一件易事,里面后很多的基础知识需要学习。其中最重要的就是数学知识,它主要包括:线性代数、空间向量、解析几何。想要写出好看的特效你还需要掌握:颜色与光照模型,还有各种特效的算法。没有这些,想写出一个漂亮的shader几乎不可能。(除非白嫖别人的代码,白嫖网站:https://www.shadertoy.com)
也就是说,这东西确实是有门槛的,看不懂,学不会很正常。如果这些都还没劝退你的话,那么咱们就正式开始吧。
第1讲 绘制流程概述
由于内容过于庞大,我甚至不知道从何开始讲起,为了尽快给大家展示一个大致的框架,我们从一个简单的精灵绘制开始讲起吧,讲讲在底层上gms2是如何绘制图形的。里面涉及的概念会非常非常多,顺便劝退一波人(雾),看不懂没关系,以后会细讲的(笑)
首先,随便建立一个sprite然后随便画点内容(其实不是随便画的,你画的内容最好能明显区分出画面方向以便后续的测试)比方说这样(经典上下左右,这都是血的教训呀,后期要是搞不清图形方向很麻烦的)。

那么要draw这个sprite其实很简单,在绘制事件里写
draw_sprite(spr_wsad,0,0,0)
你的spr就出现在房间左上角。但是这掩盖了很多细节。接下来我们按照最底层的写法给大家展示这个精灵是怎么绘制出来的。
§1.1 图元绘制概述
上一讲说道,图元绘制是最基本的绘制方法。基本图形就是点、线、面了。点和线一般不常用,最重要的就是面了。而最简单的面就是三角形。众所周知,所有的面都可以转化成三角形来绘制,一切物体都要三角形化并贴上贴图。只要掌握了如何绘制三角形就大功告成了。

此图取自我的视频:【简明微积分】第五章 矢量代数与空间解析几何——推广微积分到高维的几何基础
看一看了解一下“矢量代数和空间解析几何”,或许对理解以后的内容有很大帮助哦(笑)
在本例中我们的 spr_wsad
是一个正方形,把它拆成两个三角形进行绘制即可。每个三角形都是由三个顶点连接而成的。只要知道了三个顶点的位置、颜色和贴图坐标就可以把三角形的内部区域给填充好。于是我们需要给要绘制的内容建立顶点缓冲区(vertex buffer),然后用vertex_submit进行绘制。
创建顶点格式
根据f1的讲述,建立vertex buffer之前要先创建一个顶点格式(vertex format),gms2中默认的顶点格式是
vertex_format_begin()
vertex_format_add_position_3d()
vertex_format_add_color()
vertex_format_add_texcoord()
my_format=vertex_format_end()
最后一行把生成的顶点格式存在了变量my_format里。这个顶点格式只用声明一次就行,因此最好把my_format弄成全局变量(教程这里就先这样,懒得改了),并在游戏开始前就运行此代码。顶点格式让我们知道了一个顶点所存储的信息,它一般包括顶点的位置、颜色、贴图坐标、法线。当然这里默认的没有法线。
建立顶点缓冲区
有了格式就可以根据格式创建vertex buffer了。这里假设大家都会gm8里的draw_primitive功能,其实原理都一样,只是代码略微有所不同,这里就简短介绍:
创建事件里写
vbuff=vertex_create_buffer()
vertex_begin(vbuff,my_format)
vertex_position_3d(vbuff, 0, 0, 0);vertex_color(vbuff,c_white,1);vertex_texcoord(vbuff,0,0);
vertex_position_3d(vbuff,200, 0, 0);vertex_color(vbuff,c_white,1);vertex_texcoord(vbuff,1,0);
vertex_position_3d(vbuff, 0,200, 0);vertex_color(vbuff,c_white,1);vertex_texcoord(vbuff,0,1);
vertex_position_3d(vbuff,200,200, 0);vertex_color(vbuff,c_white,1);vertex_texcoord(vbuff,1,1);
vertex_end(vbuff)
vertex_freeze(vbuff)
tex = sprite_get_texture(spr_wsad,0)
代码创建了一个vertex buffer保存在变量vbuff里,接着vertex begin(字面意思),然后开始添加顶点信息,注意添加的信息一定要跟顶点格式相匹配,这里添加了4个顶点,最后vertex end(字面意思),最后一行的冻结freeze是提高运行效率用的,加了这一句后顶点的信息就不能再次修改了。
提交顶点缓冲区(绘制)
在绘制事件写
vertex_submit(vbuff,pr_trianglestrip,tex)
第一个参数就是你要绘制的顶点buffer,第二个是图元类型,第三个是贴图(-1就是无贴图,其实yoyo偷偷的创建了一个一像素的白色贴图给没贴图的图元绘制)(手动滑稽)
注:
- 可以发现在本例中,顶点颜色似乎没有任何用处。确实如此,因此可以可以把此项剔除。但是为啥yoyo会默认把这项加上呢,这是因为如果你想绘制纯色的形状的话就可以只用顶点颜色来规定,就不需要贴图了。而且类似
draw_sprite_ext
中的image_blend
实际就是规定了顶点颜色。这是yoyo自己封装好的轮子。本质上就是图元绘制,你完全可以另起一套。
- 图元绘制是最基础最底层的绘制,gms2中其它的绘制都是用图元绘制封装好给你用的,但是你就不知道它底层的顶点是怎么建立的了,而顶点的信息对shader来说是非常重要的。所以说为了防止yoyosb,如果你想用shader,你要么使用图元绘制,要么就先提前测试清楚yoyo的轮子搞清它内部顶点定义的格式和内容后再使用。
- 本例中顶点的位置
vertex_position_3d
是三维的,这也是唯一能添加3d坐标的方法。想做3d游戏必定会用到此函数。而shader也只接收3d位置坐标,所以说2d只是个假象,认为gms2只能做2d的,你只是在用yoyo造好的轮子罢了。底层还是3d的。(当然,能绘制3d图形跟能做3d游戏还是差的很远的。说gms2只能做2d游戏好像也没啥毛病)
- 如果顶点buffer没有被freeze那么,它可以转换成普通buffer进行修改,然后再转换成vertex buffer进行绘制。所以说这里要求开发者要对普通的buffer使用有要求。这个功能在gms2里操作的意义不大,因为gml效率对于数量大的顶点无能为力,只能借助插件拯救gm。也就是说这个buffer功能主要还是为插件服务的,至少我是这么认为的。当然你可以把顶点buffer转成普通buffer然后保存成文件,方便下次读取。
§1.2 shader工作原理概述
其实上面的所有准备工作都是给shader服务的,shader接收vertex_submit函数所传进来的内容然后进行一系列处理最后把图像绘制在表面(surface)上。所以说gms2中所有的绘制事件都是被包在shader和surface里的。默认的shader模板就是你新创建的shader,默认的surface就是application_surface。因此,表面上只写了个
vertex_submit(vbuff,pr_trianglestrip,tex)
实际上是这样的
surface_set_target(application_surface)
shader_set(sh_default)
vertex_submit(vbuff,pr_trianglestrip,tex)
shader_reset()
surface_reset_target()
其中sh_default的内容是默认的shader模板

gms2提供给我们的shader就两个部分:顶点着色器(vertex shader)和片段(fragment)着色器(或者叫像素(pixel)着色器)
顶点着色器
shader接过vertex_submit传进来的内容后开始在顶点着色器(vsh)中计算每个顶点应该绘制在表面上的哪个位置。以sh_default为例,三个attribute
声明的变量就是你传进来的顶点格式所对应的变量,由于是并行计算,每个顶点都是运行的相同的代码,在主函数void main()
中object_space_pos
是一个四维向量,前三个是你传进来的顶点位置,最后一个是1(具体为什么要这么做,以后再讲)。接着,这个向量左边乘了一个矩阵gm_Matrices[MATRIX_WORLD_VIEW_PROJECTION]
这是gms2自带的矩阵它携带了物体的空间变换属性、视角观察方向、投影方式。乘完之后就得到这个顶点变换后的位置gl_Position
,经过内部的齐次化处理,就得到了该点在surface上的位置。与此同时,我们把顶点颜色和贴图坐标赋值给含有varying
属性的变量。这种变量会传递给片元着色器(fsh)处理。
确定好每个顶点在surface上的位置、颜色、贴图坐标后,shader会根据提交的图元类型将各个顶点链接成对应的图形。

片段着色器
本例提交的是有四个顶点的pr_trianglestrip,于是会绘制两个三角形。三角形中的每个像素就是片段着色器来处理的。varying
属性的变量会被线性插值传给片段着色器。第9行根据插值的坐标采集到对应贴图的颜色,然后乘以顶点插值后的颜色得到最终的颜色gl_FragColor
。
这样这个图就画好了。

可以发现它的实际宽高有200像素,而不是编辑器里的64像素,那是因为创建vertex buffer的时候给的200。
注:
- 简单来说,顶点着色器确定图形的位置形状,片段着色器决定形状内部的颜色。有一种先确定骨架后织网的感觉。(类比伞,顶点着色器决定伞骨的形状,片段着色器决定伞面的内容)
- 物体的空间变换主要发生在顶点着色器,也就是说,3d投影发生在这里。而3d投影本身是个数学问题,因此需要用到向量、解析几何和线性代数。在gms2中涉及到的内容包括矩阵(matrix)、相机(camera)和视图(view)。等会儿会在后面概述一下。
- 片段着色器主要是在图形的内部填充颜色,可以根据贴图来确定颜色,也可以根据数学公式算个颜色出来。这也是shader算法中,变数最多的地方。在网站shadertoy中有大量的shader代码,它们都仅仅使用了片段着色器。这些图像都是用数学公式算出来的。可见数学的魅力。(笑)
- 细心的小伙伴可能发现了一个问题,shader是如何识别物体遮挡关系的呢?这需要开启两个gpu函数才能实现
gpu_set_ztestenable,gpu_set_zwriteenable
。因此gpu相关函数也是一个重点。开启这两个功能后,surface上会记录下每个像素点的深度值,它们组成了z-buffer。通过判断当前像素与要绘制像素的深度大小判定是否遮挡。
§1.3 相机和矩阵概述
shader中的矩阵gm_Matrices[MATRIX_WORLD_VIEW_PROJECTION]
是三个矩阵的乘积,它们可以分开写gm_Matrices[MATRIX_WORLD],gm_Matrices[MATRIX_VIEW],gm_Matrices[MATRIX_PROJECTION]
分别叫作世界矩阵(world_matrix)、视角矩阵(view_matrix)、投影矩阵(projection_matrix)。世界矩阵可以将物体进行旋转、缩放、平移以及剪切变形,也就是gm8中的d3d_transform
函数。视角矩阵规定了相机的观察视角,投影矩阵规定了相机的投影方式(有正交投影和透视投影两种)。因此设置相机视角和投影矩阵camera_set_view_mat,camera_set_proj_mat
成了相机最基础的函数。这也是实现3d视角的重要方法。gml中其它关于相机属性的函数都是这两个函数的衍生物(即yoyo造的轮子)。
关于3d投影的算法网上有很多,我也写了两篇,大家可以参考学习一下:
物体的投影与3D图形的绘制
3D绘图中的透视原理
矩阵当然就是线性代数中的矩阵了。然而在gms2中矩阵特指4*4的矩阵,专门用来存储世界、视角、投影矩阵。其它类型的矩阵yoyo并没有实现(yoyo又偷懒了)。特别要注意的是在gml中矩阵是以大小为16的一维数组按行储存的,向量要考虑成行向量,向量应该乘在矩阵的左边。而shader中矩阵是按列存储的,向量要考虑成列向量,向量应该乘在矩阵的右边。
还有一个小点是,gms2的视口(view)其实就是个鸡肋,注意每一个视口都会触发一次绘制事件,建议大家只用一个视口。想实现多视口的效果请灵活使用surface。
§1.4 表面(surface)概述
surface非常重要,任何物体都是绘制在表面上的,最终用来显示的surface是application_surface
干脆简称app surface得了。surface是shader输出结果的地方。 surface就是一个画布,你可以拥有多个surface,方便用来图像的合成、图层的拼接、图像的缓存。surface也可以跟buffer互相转换,这样方便跟插件互通。表面会在切换窗口的时候丢失,如果想永久保存请转换成贴图或者精灵或者buffer。
在任何表面上绘制的时候都是存在相机的,因此可以在绘制任何表面时设置相机的视角矩阵和投影矩阵,注意设置完后一定要camere_apply
一下让设置生效。
欲知后事如何,请听下回分解
第1讲完结,下一讲我直接开新帖得了,更新时间不定。