Design Hub

    • 注册
    • 登录
    • 搜索
    • 版块
    • 最新

    Godot 2D光照 - 物体自身阴影遮挡设置

    游戏开发
    1
    1
    156
    正在加载更多帖子
    • 从旧到新
    • 从新到旧
    • 最多赞同
    回复
    • 在新帖中回复
    登录后回复
    此主题已被删除。只有拥有主题管理权限的用户可以查看。
    • Pamisu
      Pamisu 管理员 最后由 编辑

      记录一些在Godot中使用2D光照时遇到的问题和解决方法,本篇笔记包含以下内容:

      • 控制物体是否受自身阴影影响的不同设置方法
      • 理解光照相关的图层和各种Mask的作用

      在Unity中,用来投射阴影的Shadow Caster 2D组件有一个Self Shadows属性用来控制阴影是否影响物体自身。

      Pasted image 20250504183337.png

      Godot的光照遮挡器LightOccluder2D节点和TileSet里都没有类似的选项,默认都是影响自身。例如下图中方块物体和黑色墙体都被自身投射出的阴影遮挡了。

      Pasted image 20250504220112.png

      关于这个问题官方文档有这样一段说明:

      LightOccluder2D 遵循常规的 2D 绘图顺序。这对于 2D 灯光而言非常重要,因为可以用来控制遮挡器是否应该遮挡精灵本身。

      如果 LightOccluder2D 节点是精灵的同级节点,并且场景树中的遮挡器被放在精灵的下方,会遮挡住精灵本身。

      如果 LightOccluder2D 节点是一个精灵的子节点,如果在 LightOccluder2D 节点中禁用了 Show Behind Parent(显示在父级之后)这个遮挡器将遮挡住精灵本身(该选项默认禁用)。

      真的有用吗?以下是在4.4版本中测试的情况,分别是遮挡器位于精灵下方、上方、作为精灵的子节点并启用Show Behind Parent。

      Pasted image 20250520085233.png

      不难发现完全没有效果,就算有用这个方法也没法用于TileSet。

      所以这里总结一些比较普遍的解决方法,顺便介绍2D光照中各种Mask的作用与对应关系。前两种方法来自Catlike Coding的教程,不同的方法各有优缺点,在效果细节上也有差别。

      方法一 设置Cull Mode

      可以实现“物体不被自身阴影遮挡,可被其他物体的阴影遮挡”的效果。

      将LightOccluder2D节点的Cull Mode属性值设置为ClockWise或CounterClockWise,取决于顶点顺序,可以两个都试一下看哪个有效果。规律是如果顶点按逆时针排列则设置ClockWise,反之CounterClockWise,正好跟顶点顺序反过来。这个设置控制了遮挡形状是从内部还是外部投射阴影。

      Pasted image 20250504180335.png

      如果编辑器中Sprite被遮挡,可以调整Sprite2D和LightOccluder2D在场景树中的顺序,实际运行是没有区别的。

      Pasted image 20250504175706.png

      这样就做到了物体不被自身阴影遮挡,会被其他物体和墙体的阴影遮挡。

      Pasted image 20250504180238.png

      TileSet同样可以这样设置,选择TileMapLayer节点并打开编辑器底部的TileSet面板,按图中步骤操作即可。可以发现TileSet使用了类似的逻辑实现。

      Pasted image 20250504181841.png

      设置后墙体会变得和物体一样,不受自身投射出阴影的影响,会被其他墙体和物体的阴影遮挡。

      Pasted image 20250504182412.png

      但效果并不理想,视觉上墙体应该是一个整体,而实际上每一块墙体都是单独的遮挡器,互相之间会遮挡,导致看起来很奇怪,而且数量较多的情况下对性能也会有影响。

      教程里的目标效果是墙体不需要被照亮,即默认被自身阴影遮挡,但如果墙体需要照亮,这样设置满足不了要求,必须将墙体的遮挡器合并,类似这样:

      Pasted image 20250520113037.png

      很可惜目前Godot没有提供直接合并遮挡器的功能,上面是手动创建的,不适合大型复杂场景或者需要动态修改的情况,在这个提案里有提到可以使用2D导航/碰撞烘焙来实现,之后尝试如果可行再来补充。

      方法二 使用两个光源

      可以实现“物体不被自身阴影遮挡,不被其他物体的阴影遮挡,会被墙体的阴影遮挡,墙体被自身阴影遮挡”的效果。

      回退方法一中的所有修改,回到初始状态。把场景中的光源复制一份。

      Pasted image 20250504221607.png

      将光源2的Range Item Cull Mask属性改为2,同时把Shadow Item Cull Mask属性也改为2。

      Pasted image 20250504222054.png

      这一步意味着这个光源专门用来照亮Light Mask属性为2的物体,同时只会对Occluder Light Mask属性为2的物体投射出阴影。

      这一堆Mask看着有些头痛,总之先按步骤操作,之后会详细介绍每个Mask的作用和它们之间的关系,为什么这样设置就能有效果。

      然后将物体Sprite的Light Mask属性设置为2,LightOccluder2D节点保持原样不需要调整。

      Pasted image 20250504220330.png

      这一步让物体的Sprite将只会受到光源2的影响,呈现出的效果是物体不再被自身阴影遮挡,也不会被其他物体的阴影遮挡。

      Pasted image 20250504223727.png

      可以发现此时物体也不会被墙体投射出的阴影遮挡,如果需要则再做一项设置,在墙体TileMapLayer使用的TileSet中,将Rendering -> Occlusion Layers下墙体Tile使用的遮挡图层的Light Mask改为1与2同时点亮。

      Pasted image 20250504224117.png

      这一步让墙体在接受到光源1、光源2的光照时都投射出阴影,通过光源2投射出的阴影将会覆盖在物体上。

      Pasted image 20250504225120.png

      这种方法缺点很明显,需要双倍数量的光源,对性能有影响,适合低分辨率的像素游戏和光源较少的情况。

      如果只需要控制物体阴影自身遮挡,对物体和墙体之间的阴影遮挡没有要求,又不想使用两个光源,这种情况可以通过设置Mask来实现。

      理解各种Mask的作用

      官方文档中有时将Mask翻译为“遮罩”,有时不翻译。个人觉得“掩码”更贴切一些,类似计算机网络里的“子网掩码(Subnet Mask)”,都是用来做位运算。

      2D节点和UI控件节点都继承自CanvasItem节点,在CanvasItem中有Light Mask和Visibility Layer两个属性。

      Pasted image 20250513104628.png

      它们对应项目设置里2D Render的图层,共有20个,对应Mask中的20位。

      Pasted image 20250513104945.png

      是否渲染:Visibility Layer 与 Canvas Cull Mask

      Visibility Layer决定了物体位于哪个/哪些渲染图层,在视口Viewport中有一个与之对应的属性Canvas Cull Mask,用来控制这个视口渲染哪些图层,默认全部点亮,即渲染所有图层。

      Pasted image 20250513105709.png

      在运行时我们的场景会被放在一个名为root的窗口Window节点下,而Window正是继承自Viewport,也就是说默认有一个渲染所有图层的视口。

      Pasted image 20250513110220.png

      例如物体A的Visibility Layer点亮图层1,物体B的Visibility Layer点亮图层2、3、5,视口的Canvas Cull Mask点亮图层3、6。

      用视口的Canvas Cull Mask值分别与物体的Visibility Layer做与运算,结果不为0时表示需要渲染:

      物体A是否渲染 = 0000 0000 0000 0000 0001 & 0000 0000 0000 0010 0100 = 0000 0000 0000 0000 0000 = 不渲染

      物体B是否渲染 = 0000 0000 0000 0001 0110 & 0000 0000 0000 0010 0100 = 0000 0000 0000 0000 0100 = 渲染

      注意物体是否渲染还会受到其父物体的影响,如果父物体不渲染,子物体也不会渲染,但可以勾选CanvasItem的Top Level属性来取消这个限制。

      Godot原生支持多窗口,有基于这个特性开发独特玩法的游戏,例如《Windowkill》,这点是Unity做不到的。

      是否计算光照:Light Mask 与 Range Item Cull Mask

      类似地,CanvasItem中的Light Mask属性用来设置物体属于哪个/哪些光照图层,在2D光源Light2D中的Range组下有一个Item Cull Mask与之对应,用来控制这个光源在哪些图层上计算光照。

      Pasted image 20250513154420.png

      例如物体A、B都在光源范围内,物体A的Light Mask点亮图层1,物体B的Light Mask点亮图层2、3、5,光源的Range Item Cull Mask点亮图层3、6,那么只有物体B会被照亮。

      是否计算阴影:Occluder Light Mask 与 Light Mask 与 Shadow Item Cull Mask

      阴影有一些不同,它有三个属性参与控制,其实也很好理解,因为有投射出阴影的物体和接收阴影的物体,加上光源就是三个。

      对于投射阴影的物体,LightOccluder2D的Occluder Light Mask属性用来设置遮挡器属于哪个/哪些阴影图层;TileSet则是Rendering -> Occlusion Layers里图层项的Light Mask属性。

      Pasted image 20250513155829.png

      Pasted image 20250513160009.png

      对于接收阴影的物体,还是CanvasItem中的Light Mask属性,也就是说它既决定了物体的光照图层,也决定了阴影图层。

      在2D光源Light2D中的Shadow组下的Item Cull Mask与它们对应,用来控制这个光源在哪些图层上计算阴影。

      Pasted image 20250513160656.png

      例如物体A的Light Mask为1,物体B的Light Mask为2,遮挡器的Occluder Light Mask为3,光源的Range Item Cull Mask为1、2,Shadow Item Cull Mask为1、3,那么物体A、B会被照亮,遮挡器投射出的阴影会影响物体A,不影响物体B。

      方法三 划分图层

      可以实现“单独控制某个图层的物体是否被同图层物体的阴影遮挡”的效果,但对于不同图层物体之间的阴影遮挡不好控制。

      将场景中的物体和遮挡器划分到不同的图层,例如地板、墙体、墙体遮挡器、物体、物体遮挡器,可以在项目设置中给图层命名。

      Pasted image 20250513161405.png

      接着将各个物体的Light Mask属性都设置到对应的图层,遮挡器的Occluder Light Mask也一样。

      假设要实现物体和墙体都不被自身阴影遮挡,那么地板、墙体、物体图层都可以被照亮,即光源的Range Item Cull Mask点亮1、2、4图层;地板需要接收阴影,墙体、物体不需要接收阴影,墙体遮挡器、物体遮挡器需要投射阴影,即光源的Shadow Item Cull Mask点亮1、3、5图层。

      Pasted image 20250513162023.png

      Pasted image 20250513164014.png

      在此基础上,如果要做到方法二的最终效果,物体被墙体的阴影遮挡,这种情况还是得使用两个光源。

      将光源复制一份,光源1不照亮物体物体,阴影和之前一样;光源2只需要照亮物体,墙体遮挡器投射阴影,物体接收阴影。

      Pasted image 20250513165735.png

      Pasted image 20250513165910.png

      参考与素材来源:True Top-Down 2D by Catlike Coding

      1 条回复 最后回复 回复 引用
      • 1 / 1
      • First post
        Last post
      版权所有 ©2023 Design Hub 保留所有权利
      鄂ICP备2021004164号-2