两年前,我已经在Phaser 2D中尝试了阴影
物质 。 在最后的Ludum Dare中,我们突然决定制造恐怖,这是没有阴影和灯光的恐怖! 我弄开了指关节...
……对于LD而言,这并不是一件及时的事情。 在游戏中,当然会有一些光影,但这真是本来就很悲惨的表象。
在将游戏发送到比赛后回到家中,我决定“关闭格式塔”并完成这些不幸的阴影。 发生了什么-您可以
在游戏中感觉 ,
在演示中玩 ,看图片并在文章中阅读。
像在这种情况下一样,尝试编写一个通用的解决方案是没有意义的,您需要关注特定的情况。 游戏世界可以以片段的形式表示-至少是那些投射阴影的实体。 墙壁是矩形,人是矩形,仅旋转,地狱扰流板是圆形,但是在截断模型中,可以简化为始终垂直于光线的直径长度。
有几个光源(20-30),它们都是圆形的(聚光灯),有条件地位于比被照明物体低的位置(以便阴影可以是无限的)。
我头脑中看到以下解决问题的方法:
- 对于每个光源,我们构建一个屏幕大小的纹理(好,或者小2-4倍)。 在此纹理上,我们仅绘制梯形BCC'D',其中A是光源,BC是线段,B'C'是线段到纹理边缘的投影。 之后,将这些纹理发送到着色器,在此处将它们混合为一张图片。
Celeste平台游戏的作者做了这样的事情,在其关于media的文章中写得很好: medium.com/@NoelFB/remaking-celestes-lighting-3478d6f10bf
问题:几乎每帧都需要重新绘制20-30个屏幕大小的纹理并将其加载到GPU中。 我记得这是一个非常非常不快的过程。
- 在habr-habr.com/post/272233上的帖子中描述了该方法。 对于每个光源,我们都会建立一个“深度图”,即 这样的纹理,其中x =“光束”与光源的角度,y =光源的数量,颜色==从光源到最近障碍物的距离。 如果我们以0.7度(360/512)为步长并使用32个光源,则会得到一个512x32纹理,该纹理尚未更新很长时间。
(以45度为步长的示例纹理)
- 最后我将描述的秘密方式
最后,我决定使用方法2。但是,文章中的描述并不适合我。 在那里,纹理还使用rakecast在着色器中构建-循环中的着色器从光源沿光束方向移出并寻找障碍物。 在过去的实验中,我还在着色器中进行了rakecast,尽管通用,但它非常昂贵。
“我认为模型中只有细分,而10-20个细分落入任何光源的半径内。 我不能基于此快速计算距离图吗?”
因此,我决定这样做。
首先,我只是在屏幕上显示墙壁,有条件的“主角”和光源。 在光源周围,一圈纯净的透明光在黑暗中切出。 为了得到这个:
( 演示 )我立即开始使用着色器,以免放松。 对于每个光源,必须将其坐标和作用半径(超出该范围光线无法到达)传递给它,只需通过均匀的阵列即可完成。 然后在着色器中(该着色器是片段性的,将对屏幕上的每个像素执行该着色器),以了解当前像素是否在亮圆圈中。
class SimpleLightShader extends Phaser.Filter { constructor(game) { super(game); let lightsArray = new Array(MAX_LIGHTS*4); lightsArray.fill(0, 0, lightsArray.length); this.uniforms.lightsCount = {type: '1i', value: 0}; this.uniforms.lights = {type: '4fv', value: lightsArray}; this.fragmentSrc = ` precision highp float; uniform int lightsCount; uniform vec4 lights[${MAX_LIGHTS}]; void main() { float lightness = 0.; for (int i = 0; i < ${MAX_LIGHTS}; i++) { if (i >= lightsCount) break; vec4 light = lights[i]; lightness += step(length(light.xy - gl_FragCoord.xy), light.z); } lightness = clamp(0., 1., lightness); gl_FragColor = mix(vec4(0,0,0,0.5), vec4(0,0,0,0), lightness); } `; } updateLights(lightSources) { this.uniforms.lightsCount.value = lightSources.length; let i = 0; let array = this.uniforms.lights.value; for (let light of lightSources) { array[i++] = light.x; array[i++] = game.world.height - light.y; array[i++] = light.radius; i++; } } }
现在,我们需要了解每个光源将投射阴影的部分。 相反,细分的哪些部分-在下图中,我们对该细分的“红色”部分不感兴趣,因为 光仍然没有到达他们。
注意:路口定义是一种初步优化。 为了减少进一步处理的时间,消除超过光源半径的大段片段,这是需要的。 当我们有许多段的长度远大于“辉光”的半径时,这是有道理的。 如果不是这种情况,并且我们有很多短的路段,可能不浪费时间确定交叉点并处理整个路段是正确的,因为 节省时间仍然行不通。为此,我使用了众所周知的公式来查找直线和圆的交点,每个人都从几何学的某个课程中学到了每个人的内心记忆……在某人的想象世界中。 我只是不记得她,所以我不得不用
Google搜索它 。
我们编码,看看发生了什么。
( 演示 )这似乎是常态。 现在我们知道哪些段可以投射阴影并可以进行rakecast。
在这里,我们还有选择:
- 我们只是绕了一圈,抛出光线并寻找相交点。 到最近的交叉点的距离是我们需要的值
- 您只能进入那些细分的角落。 毕竟,我们已经知道了点,计算角度并不困难。
- 此外,如果我们沿线段行驶,则无需投射光线并计算交点-我们可以按所需的步长沿线段移动。 运作方式如下:
在这里
-段(墙),
是光源的中心,
-垂直于线段。
让
-与法线的角度,您需要为此确定从源到线段的距离,
-在路段上
光束落在哪里。 三角形
-矩形
-一条腿,其长度在该段中是已知的且恒定,
-所需的长度。
。 如果您提前知道了这一步(并且我们知道),那么您可以预先计算反余弦表并非常快速地寻找距离。
我将给出此类表的代码示例。 几乎所有带角的工作都被带索引的工作所取代,即 从0到N的整数,其中N =圆的步数(即,步距=
)
class HypTable { constructor(steps = 512, stepAngle = 2*Math.PI/steps) { this.perAngleStep = [1]; for (let i = 1; i < steps/4; i++) {
当然,对于初始角度ACD不是步长的倍数的情况,此方法会引入错误。 但是对于512步,我在视觉上看不出任何区别。
所以我们已经知道该怎么做:
- 在可以投射阴影的光源范围内查找片段
- 对于步骤t,创建一个dist(角度)表,遍历每个线段并计算距离。
如果您用光线绘制这张桌子,它的外观如下。
( 演示 )如果是用纹理写的,这就是它寻找10个光源的样子。
在此,每个水平像素对应于一个角度,颜色对应于以像素为单位的距离。
像这样使用
imageData用js编写
fillBitmap(data, index) { let total = index + this.steps*4; let d1, d2; let i = 0;
现在,将纹理传递到我们的着色器,该着色器已经具有光源的坐标和半径。 然后像这样处理它:
结果:
( 演示 )现在,您可以带来一点美丽。 让光线随着距离逐渐消失,阴影将变得模糊。
对于模糊,我看一下相邻的角,+-步骤,如下所示:
thisLightness = (1. - getShadow(i, angle, distance)) * 0.4 + (1. - getShadow(i, angle-SMOOTH_STEP, distance)) * 0.2 + (1. - getShadow(i, angle+SMOOTH_STEP, distance)) * 0.2 + (1. - getShadow(i, angle-SMOOTH_STEP*2., distance)) * 0.1 + (1. - getShadow(i, angle+SMOOTH_STEP*2., distance)) * 0.1;
如果将所有内容放在一起并测量FPS,结果如下:
- 在内置视频卡上-一切都很糟糕(<30-40),即使是简单的示例
- 只要光源不是很强,其他一切都很好。 也就是说,每个像素的光源数量很重要,而不是总数。
这个结果很适合我。 您仍然可以使用照明的颜色,但是我没有。 稍微扭曲并添加一些法线贴图后,我上传了NOPE的更新版本。 她现在看起来像这样:
然后他开始准备一篇文章。 我看着这样的gif和想法。
我大声喊道:“所以它几乎是伪3D外观,就像在《德军总部》中一样。”(是的,我有很好的想象力)。 实际上-如果我们假设所有墙壁的高度都相同,则距离图就足以构建场景。 为什么不尝试呢?
场景应该看起来像这样。
所以我们的任务是:
- 在屏幕上的某个点上,获取没有墙的情况下的世界坐标。
我们将考虑:
- 首先,我们对屏幕上一个点的坐标进行归一化,以便在屏幕中心以及角(-1,-1)和(1,1)分别有一个点(0,0)
- x坐标成为从视线角度来看的角度,您只需将其乘以A / 2,其中A是视角
- y坐标确定从观察者到点的距离,通常情况下为d〜1 / y。 对于屏幕下边缘上的点,距离= 1,对于屏幕中心上的点,距离=无穷大。
- 因此,如果您不考虑墙壁,那么对于世界上的每个可见点,屏幕上都会有2个点-一个在中间上方(在“天花板”上),另一个在下方(在“地板”上)
- 现在我们可以看一下距离表。 如果有一堵墙比我们的要近,那么您需要绘制一堵墙。 如果不是,则表示地板或天花板
我们得到命令:
( 演示 )添加照明-以相同的方式,遍历光源并检查世界坐标。 以及-最后的修饰-添加纹理。 为此,在有距离的纹理中,您还需要在此时为墙壁纹理写偏移量u。 这是频道b派上用场的地方。
( 演示 )太好了
开玩笑的
当然不完美。 但是,地狱,我仍然读到大约15年前如何通过racast制作我的Wolfenstein的书,我想做到这一点,这是一个机会!
而不是结论
在本文的开头,我提到了另一个秘密方法。 这是:
只需采用已经知道如何使用的引擎即可。
实际上,如果您需要制作游戏,那么这将是最正确,最快的方法。 为什么您需要围护自行车并解决长期存在的问题?
但是为什么。
在10年级时,我搬到另一所学校,遇到数学问题。 我不记得确切的例子,但它是一个带有度数的方程式,在各个方面都需要简化,但只是没有成功。 绝望的是,我咨询了姐姐,她说:“所以在两边加
2 ,一切都会分解。” 那就是解决方案:添加不存在的内容。
后来,当我帮助我的朋友盖房时,我需要在门槛上放一个障碍-填补壁a。 在这里,我站起来整理一下吧台的装饰。 似乎很合适,但并不完全合适。 其他的则小得多。 我正在考虑如何在这里收集“幸福”一词,一位朋友说:“因此,他们在一个会干扰的圆形位置喝了凹槽”。 现在,大酒吧已经停滞不前。
这些故事通过这样的效果结合在一起,我称之为“库存效果”。 当您尝试从现有零件中做出决定时,却没有看到可以在这些零件中进行处理和改进的材料。 数字是木头,金钱或代码。
在编程方面,我多次观察到与同事相同的效果。 他们对材料没有信心,有时会在需要进行非标准控制时屈服。 或将单元测试添加到没有的地方。 或者他们在设计类时尝试提供所有内容,然后我们得到如下对话:
-现在没有必要了
-如果有必要怎么办?
-然后我们将添加。 离开扩展点,仅此而已。 代码不是花岗岩,而是橡皮泥。
为了学习看到和感受与我们合作的材料,我们还需要自行车。
这不仅是锻炼头脑或训练。 这是在代码上达到质的不同工作水平的一种方式。
谢谢大家的阅读。
链接,以防您忘记单击某处: