我一直致力于为像素艺术打造理想的延迟渲染管线,灵感来源于《风来之国》和即将推出的《Kyora》等游戏。这不是一个插件或逐行教程,但我想分享所需的内容以及工作流程的样子。如果有人感兴趣,随着系统的改进,我会分享更多过程。提前说明,这不适合新手。
在我继续之前,这是最终产品的示例:
https://i.redd.it/d7iyofdaiqoh1.gif
贴图与纹理
无论你使用的是Unreal还是Gamemaker,如果你想要基于物理的渲染,你需要为你的精灵创建贴图。这个管线灵感来源于RE引擎,但也类似于Unity或Unreal。有许多工具可以创建贴图,有些人使用laigter或blender,但我更喜欢在Aseprite中手绘。
这个管线目前使用以下贴图,尽管还有"空间"容纳更多数据:
漫反射纹理 "spr_shrine_alb"
RGBA:漫反射贴图 - 艺术/颜色/基础纹理
法线纹理 "spr_shrine_nrm"
RGB:法线贴图 - 垂直于表面的方向
A:未使用
表面纹理 "spr_shrine.sur"(分3层绘制,高光和环境光遮蔽设置为"叠加"混合模式,输出为一张图像)
R:粗糙度 - 高光的扩散范围
G:高光 - 表面反射的光量
B:环境光遮蔽 - 环境光和太阳光被遮挡的区域
A:目前未使用
效果纹理 "spr_shrine_fxs"
R:亮度/发光 - 漫反射纹理的此区域会叠加到光照中
G:次表面散射
BA:目前未使用
扩展纹理 "spr_shrine_ext"
R:深度/高度图 - 红色值会从物体的Z位置中减去,以在光照计算中提供更准确的Z坐标
GBA:目前未使用
所以,是的,你必须在Aseprite中手绘8张贴图。如果这足以让你望而却步,我能理解。然而,这并不会让你的工作难度增加8倍。我认为一旦你做了10次左右,并设置好模板,其实也没那么糟。事实上,由于你的基础美术可能已经包含了高光、深阴影和发光部分,一半的贴图已经完成了。
既然所有在Gamemaker之外的工作都做完了,接下来呢?Gamemaker本身不支持任何延迟渲染。所以全靠我们自己。但一旦完成,就一劳永逸了。要导入新美术,我只需将5张图片拖入Gamemaker,全选它们并右键设置纹理组,就完成了。它就这么简单。
绕过Gamemaker的限制
以下是在Gamemaker中实现此功能需要做的事情。这是所需工作量的示例,我不能把所有代码都贴在这里,所以很抱歉这不是真正的教程。
- 为物体定义所有贴图精灵的自定义脚本
- 我们不想绘制五个不同的精灵。我们想绘制一个精灵,但包含五套不同的UV坐标
- 为了方便,每个精灵必须遵循严格的命名约定。你给物体分配漫反射精灵,然后脚本在创建时完成其余工作
​
//获取贴图精灵
var _name = sprite_get_name(sprite_index);
_name = string_copy(_name,0,string_length(_name) - 3);
alb = sprite_index;
nrm = asset_get_index(_name + "nrm");
sur = asset_get_index(_name + "sur");
fxs = asset_get_index(_name + "fxs");
ext = asset_get_index(_name + "ext");
- 只要我们的文件命名正确,就不需要额外工作
- 禁用应用表面
- 我们不再需要这个了
- 没有东西会自动绘制
- 忘记你关于内置绘制函数的知识,我不会再用它们了
- 创建obj_draw
- 这个物体管理所有表面:
- surf_alb
- surf_nrm
- surf_sur
- surf_fxs
- surf_lgt
- surf_shd
- 这个物体绘制所有瓦片、角色、装饰等。房间里的一切
- 是的,我们会绘制许多全分辨率表面。加载时,这个管线在一台500美元的笔记本电脑上以超过3000帧运行。然而,屏幕上最多只能同时有4个光源投射阴影。这是主要限制。
- 我们仍然可以添加非投影光源(如体积光),对性能影响很小或没有
- 构建顶点缓冲区
- 不再使用draw_sprite()之类的函数,我们将创建一个函数,将物体的精灵数据添加到全局顶点缓冲区中。
- 我为角色、瓦片、植被、视差背景等设置了不同的缓冲区和着色器。
- 例如,global.vb_actors会在每一步被管理。这使我们能够控制每个物体的Z位置,并添加贴图的纹理坐标。我们将不再使用深度排序。深度就是Z位置。
- 这对CPU来说是更多的工作,但静态物体/装饰/瓦片/植被/粒子/云/雨可以被冻结,使它们的渲染比以往更快。即使每个顶点存储了更多的UV数据。
- 在绘制阴影时,我不需要再次绘制角色/装饰。只需用不同的阴影着色器提交它们的顶点。所有这些数据已经在一个巨大的批次中坐等再次使用。非常高效。
- 瓦片顶点缓冲区
- 所有这一切中最糟糕的部分就是瓦片层。我花了数小时编写这些脚本。基本上,我们正常创建瓦片层,但也要遵循非常严格的命名约定。该瓦片层必须包含你的瓦片精灵的名称。然后我们构建一个瓦片集顶点缓冲区,在房间创建时冻结,然后销毁瓦片层。我们需要自己的缓冲区,以便瓦片数据可以包含法线贴图、表面纹理等。我们可以检查房间中的每个瓦片,获取其瓦片集索引,然后通过大量数学运算获取该索引在精灵纹理页上的位置。
- 说实话,我绝对讨厌这个。
- 使用MRT(多渲染目标)绘制
- 这被称为G-Buffer(几何缓冲区)。我们正常绘制美术到surf_alb。同时,为了最高效率,相同的着色器输出另外3个片段到其他3个表面:法线贴图表面、表面纹理表面和视觉效果表面。顶点着色器无需做额外工作。然后我们的光照计算会采样所有这些"几何体"来计算其强度和反射(高光)。
- 这是obj_draw在其draw_g_buffer()函数中所做的一个示例:
​
//启用Z
gpu_set_ztestenable(1);
gpu_set_zwriteenable(1);
gpu_set_alphatestenable(1);
surface_set_target_ext(0,surf_alb);
surface_set_target_ext(1,surf_nrm);
surface_set_target_ext(2,surf_sur);
surface_set_target_ext(3,surf_fxs);
//清除表面
draw_clear_alpha(c_black,0.0);
//透明度不改变颜色值
gpu_set_blendmode_ext_sepalpha(bm_one,bm_zero,bm_one,bm_zero);
//应用相机
camera_apply(global.view);
//绘制角色
shader_set(shd_actors);
vertex_submit(global.vb_actors,pr_trianglelist,tex_actors);
shader_reset();
//然后我会绘制植被、静态物体、绳索/藤蔓、瓦片、背景、天空等
//结束G-Buffer
surface_reset_target();
gpu_set_blendmode(bm_normal);
//禁用深度
gpu_set_ztestenable(0);
gpu_set_zwriteenable(0);
gpu_set_alphatestenable(0);
//引用G-Buffer
tex_alb = surface_get_texture(surf_alb);
tex_nrm = surface_get_texture(surf_nrm);
tex_sur = surface_get_texture(surf_sur);
tex_fxs = surface_get_texture(surf_fxs);
- 我们总是从前往后绘制,因为我们不想浪费时间绘制背景或天空,如果它们有70%会被物体、瓦片和装饰遮挡。Gamemaker会在背景的片段着色器因Z测试失败(被瓦片等遮挡)时将其丢弃。
- 光照计算
- 我有3个点光源、太阳/月亮光和环境光。
- 它们都以加法模式绘制到光照表面上。
- 首先,点光源和太阳将阴影绘制到阴影表面,每个光源使用不同的颜色通道。这减少了每帧的表面切换,是巨大的性能提升。
- 光源采样阴影表面和G-Buffer表面来计算其强度/高光。
- 这意味着你必须自己编写所有法线贴图/Blinn-Phong/SSS/AO着色器计算。
- 我的环境光很简单,根据一天中的时间变化,但也可以尽可能复杂。
- 后绘制事件
- 我绘制我的surf_alb表面,采样光照表面。
- 光照颜色的亮度决定强度 - 将漫反射颜色乘以光照颜色。
- 光照的Alpha存储高光值 - 高光将光照颜色加到漫反射上。
- 最终我会在这个阶段添加雾/薄雾。注意,部分透明度在G-Buffer中无法工作,必须在之后添加。
- 渲染完成。
结论
这就是大概情况。如果你读到这里,你可能会觉得我这样做很蠢,而不是直接改用Unity或Unreal等。你可能说得对,但听我说。我绝对讨厌Unity和Unreal。没有不尊重使用这些引擎的人的意思,但我宁愿做这项工作1000次也不会切换到它们。Gamemaker是我的引擎。
那么,在Gamemaker中实现基于物理的渲染有多难?嗯,我觉得有点难,但非常有趣。编写代码花了一些时间,但学习着色器和PBR需要数月时间。如果有人想了解更多,我会尽力回答任何问题。Acerola也有很多关于计算机图形的精彩视频,启发了我自己在Gamemaker中尝试。
评论 (0)