GAMEMAKER STUDIO 2中实现2D动态光影效果(PART 2)
教程原文:Realtime 2D Lighting in GameMaker Studio 2 - Part 2
翻译:顺子
在上一篇文章中,我想您展示了如何创建 2D 光影系统的基础知识,本文将继续沿用上一篇中的示例,我们将进一步完善它使其更像真实的点光源。我们还将介绍一些着色器的使用以便于实现这一效果——没错,使用着色器会更方便!——然后让光源也具备颜色,让我们先来看看上一次我们做到哪儿了。
在上一篇文章最后,我们说接下来的任务是让光线半径以外所有的区域都变黑,因为这更像一个“点光源”,因此我们先来实现这个效果。在我们真正了解这个机制之前,我们先把之前绘制的阴影都放到一个表面(surface)上,因为这么做更方便后期使用着色器进行处理。请注意,以下的变化将穿插于不同的脚本之间,因此可能会有一点跳——让我们开始吧!在光源对象的创建事件中我们要添加一个新的变量:
surf = -1 ;
然后在绘制事件的顶部添加以下内容,这将会在首次运行绘制事件时创建一个新的表面——如果这个表面不见了也会重新创建。
if( !surface_exists(surf) ){
surf = surface_create(room_width,room_height);
}
接下来我们找到检测 tile 的循环,并且在vertex_begin()之前,我们将该表面设置为当前表面,这样我们的阴影就会被渲染到这个表面上而不是屏幕上。我们将修改我们的循环代码,它现在看起来像这样......
surface_set_target(surf);
draw_clear_alpha(0,0);
vertex_begin(VBuffer, VertexFormat);
for(var yy=starty;yy<=endy;yy++)
{
for(var xx=startx;xx<=endx;xx++)
{
// Shadow volume creation.
}
}
vertex_end(VBuffer);
vertex_submit(VBuffer,pr_trianglelist,-1);
surface_reset_target();
draw_surface(surf,0,0);
现在,除了我们修改了阴影渲染的表面以外应该跟之前没有任何变化,你可能已经注意到了在代码中我们把这个表面设置为黑色并且 alpha 为 0。这一点非常重要,这样做我们才能把这个表面混合显示,然后使得没有阴影的区域正确显示出地面来。
OK,现在进入真正有趣的部分!我们现在要创建一个着色器(shader),并让它来处理这个新的表面。同样,目前来看这一切都没有任何的不同。我们在资源树中着色器一栏单击右键并选择“创建”,这将会自动添加一个新的着色器并打开编辑窗口。这将为我们自动生成一个简单的默认着色器,我们要做的就是在绘制先前那个表面之前先设置这个着色器,然后在绘制完表面后进行重置。这个操作(假设你调用了你的着色器 shader0)将只影响中间调用 draw_surface() 的部分。
shader_set(shader0);
draw_surface(surf,0,0);
shader_reset();
照旧,目前运行起来还是跟之前没有任何区别,但是我们现在可以做一些有趣的事情了!首先我们需要传递几个用户自定义的值,直接传给我们绘制灯光的事件。我们把这段代码添加到 Fragment Shader 中 void main() 的上方。
uniform vec4 u_fLightPositionRadius; // x=lightx, y=lighty, z=light radius, w=unused
接下来,我们需要把场景的坐标从 Vertex Shader 传递到 Fragment Shader 中。这很容易,因为之前我们在场景中哪些坐标绘制阴影都已经保存下来了,只需要把这些坐标传递过去就行了。为此我们需要另外为 Vertex shader添加一个新的值,如下所示:
varying vec2 v_vTexcoord;
varying vec4 v_vColour;
varying vec2 v_vScreenPos;
然后在 Vertex Shader 脚本的底部“}”前添加
v_vScreenPos = vec2( in_Position.x, in_Position.y );
你还需要把“varying vec2 v_vScreenPos;”这一行放进 Fragment shader 里。现在 Fragment shader 已经有了一切要素,只需要把光源的坐标传到绘制事件中即可。为此我们需要获取光源对象的创建事件中 u_fLightPositionRadius 变量的值。我们要在最开始就去处理,因为这个过程实在不够快,因此如果我们在一开始就去处理,在后面真正要用的时候就会更高效一些。让我们在光源的创建事件底部添加以下内容:
LightPosRadius = shader_get_uniform(shader0,"u_fLightPositionRadius");
现在我们可以直接在光源的绘制事件中使用它了,应当在我们设置 shader 以及绘制阴影表面之间
shader_set(shader0);
shader_set_uniform_f( LightPosRadius, lx,ly,rad,0.0 );
draw_surface(surf,0,0);
shader_reset();
现在,我们运行一下程序——没错,看上去还是没有任何变化——但至少要确保能正常运行!
到此为止,我们已经可以在 fragment shader 中使用这些额外的信息。包括获取场景坐标(v_vScreenPos)并计算其到光源的位置(u_fLightPositionRadius.xy)的距离,然后检查这个距离是否大于光照半径,如果不是则输出黑色像素。你还可以继续增大光照半径(绘制事件中的 rad变量 256 左右)。让我们来看看我们如何在 Fragment Shader 中使用它,以下是 shader 全文:
varying vec2 v_vTexcoord;
varying vec4 v_vColour;
varying vec2 v_vScreenPos;
uniform vec4 u_fLightPositionRadius; // x=lightx, y=lighty, z=light radius, w=unused
void main()
{
// Work out vector from room location to the light
vec2 vect = vec2( v_vScreenPos.x-u_fLightPositionRadius.x, v_vScreenPos.y-u_fLightPositionRadius.y );
// work out the length of this vector
float dist = sqrt(vect.x*vect.x + vect.y*vect.y);
// if in range use the shadow texture, if not it's black.
if( dist< u_fLightPositionRadius.z ){
gl_FragColor = v_vColour * texture2D( gm_BaseTexture, v_vTexcoord );
}else{
gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0);
}
}
如你所见,这是一个非常简单的计算,但是会给我们一个很棒的效果,可以很好的区分出点光源,并将光照半径之外的所有区域都变成黑色。
看这效果多酷!移动鼠标时我们可以发现所有的投影都会跟随我们鼠标而移动位置。我们还可以进一步提升效果,只需要略微降低ALPHA 值使画面更透明一些即可,只需要在 Fragment shader 最后的“}”之前添加以下内容
gl_FragColor.a *= 0.5 ;
这么做是为了获取最终的 alpha 值(在我们的示例中是在影子区域为 1.0,其它区域则为 0.0),降低透明度就会得到以下画面
在上面你可能会注意到,当我计算出 vect 变量时我调用了 * vec2 (xx,yy)* 。我这么做是为了让它更清晰,但实际上这并不是必须的。着色器作用于向量,这意味着我们可以同时执行多个操作。这条线应该是这样的:
vec2 vect = v_vScreenPos.xy-u_fLightPositionRadius.xy;
这么做是为了能在一个操作中获取屏幕上某个点的 xy 坐标并减去光源的坐标 xy 然后直接存储到 vec2 中。如果你能明白这一点,就使用这个新的语句,如果不明白就还是用原来的写法,这对这个着色器而言没什么区别但在其它某些着色器中可能区别就大了,你可以试着去尝试一些新的东西,这么做可以让你的代码更精炼,启动更快。
接下来我们要添加一些颜色,但在此之前,我们想要让光线的边缘变得不那么硬,而是一个渐渐削弱的效果。毕竟在现实中除非你非常靠近光源本身,否则你看不到这么强烈的光线边缘,事实上光线会逐渐消失,所以我们在这里添加这样的效果会使得整体效果更好。我们要让光线随着光照半径线性削弱,如果你足够聪明,可以在这里使用曲线或类似的方法来实现。这个衰减的计算非常简单,实际上我们已经拥有了所有需要的值。跟光源的距离以及光照半径就是我们所需要的所有数值。目前我们已经有一些简单的代码是在光照半径内去调节 alpha 值在 0.0 到 1.0 之间的缩放 (光照半径内外),因此现在我们用 LERP(线性插值)来处理阴影的值来完成整个阴影的渲染绘制,以下是 Fragment shader 中的内容:
//在范围内使用阴影纹理,否则全黑。
if( dist< u_fLightPositionRadius.z ){
// 从光源中央往半径将值从 0 变为 1
float falloff = dist/u_fLightPositionRadius.z;
// 获得阴影纹理
gl_FragColor = v_vColour * texture2D( gm_BaseTexture, v_vTexcoord );
// 阴影线性变为全黑
gl_FragColor = mix( gl_FragColor, vec4(0.0,0.0,0.0,0.7), falloff);
}else{
// 超出半径的直接全黑
gl_FragColor = vec4(0.0, 0.0, 0.0, 0.7);
}
着色器中的 LERP 方法名为 mix(很奇怪),但事实上就是一个东西。这个值会根据衰减值逐渐从 gl_FragColor 混合(个人猜测)处理成 0,0,0,0.7 (全黑)。当你从脚本尾部删除 gl_FragColor.a *= 0.5;以后,这就会呈现出一个很棒的衰减效果,更像真实的明暗处理。
接下来我们要添加颜色。首先,我们双击场景中的实例,并将颜色设置为绿色,如下所示:
完成这个操作后,再修改我们绘制表面的光线绘制事件,这样我们可以把颜色传进去,比如下面这样:
draw_surface_ext(surf,0,0,1,1,0,image_blend,0.5);
这将会把绿色(以及 0.5 的 alpha 值)传递给着色器,然后我们就可以用来设置光线颜色。接下来我们需要修改 Fragment shader 的代码让着色器也处理颜色。所以我们之前写着**gl_FragColor = v_vColour * texture2D()**的地方现在要改成:
vec4 col = texture2D( gm_BaseTexture, v_vTexcoord );
if( col.a<=0.01 ){
gl_FragColor = v_vColour;
}else{
gl_FragColor = col;
}
我们检测 alpha 值(col.a 的值),当它为 1.0 时则为全黑阴影区域,0.0 则为非阴影区,所以这是个非常容易理解的数字。我们也没有必要拿这个值跟 0.0 进行精确比较,因为我们永远也不能确定这个值到底是多少,所以我们需要预留一点误差空间。现在我们运行程序的话,你会看到一道可爱的绿光!(译者:要想生活过得去,嗯~)
要想点亮多个光源,你只要重复以上操作即可,混合区域的处理会稍微有些复杂(即如何让黑色区域和着色区域保持本色),但这才是核心关键。如果你找不到正确的混合模式,那就把所有的内容传递给着色器并让它来帮你处理一切,记住默认的 Application Surface 也只是一个表面,同样可以用着色器来渲染。另外要记住你没法同时读取并写入同一个表面,因此你可能会需要一个临时表面。
终于……你做到了,你已经完成了一个属于自己的实时光影系统!在下面的图里你可以看到看到我把一个光源穿越多个动态光源的效果。点击此处可以查看相关效果的视频,其中使用的就是与我介绍的类似的系统,尽管我还做了一些更复杂的处理(比如在单个表面上绘制多个阴影),你当然没有必要去尝试并把这些东西复制到你自己的光影系统里,你的系统已经能够实现各种基础的功能了。
译者:最后这个图的效果,光照着本教程是无法完成的,算是原作者留的课后思考题吧,另外自己按照教程实现了一个小示例——网盘下载:密码:bymz,仅供参考