2015 年 9 月 24 日
教程:使用 LiquidFun、快照和滤镜创建超赞的水效果
本教程由 Frozen Gun Studios 的 Andreas von Lepel 提供。在“Freeze! – The Escape”(iOS/Android) 全球范围内取得超过 1100 万 次免费下载的成功之后,Frozen Gun Studios 刚刚发布了续集“Freeze! 2 – Brothers”(iOS/Android),完全使用 Corona SDK 构建,并具有基于 LiquidFun 的水效果。
概述
在开发初期,我决定如果我能在旋转的关卡中拥有水和像火箭燃料这样的有毒液体四处飞溅,那将会非常有趣。请看预告片了解我们所做的
如您所见,我们的两位英雄都可以在水中游泳,但在后面的关卡中,玩家的任务是先将所有致命的火箭燃料收集到一个桶中,然后将其捡起以打开关卡的出口。
项目代码
使用 LiquidFun 设置具有基本水效果的场景非常容易,并且在 Corona SampleCode 代码库中包含了一些不错的示例。但是,创建具有可见“表面”的非常漂亮、透明的水有点困难,您需要使用快照和滤镜。幸运的是,像往常一样,对于 Corona SDK 来说,即使是复杂的东西也主要是由引擎为您完成的。
当我们逐步完成本教程时,我鼓励您下载 GitHub 代码库中的 LiquidFun-Transparency 项目,该项目也捆绑在最近的 Corona SDK 版本中
CoronaSDK-XXXX
→ SampleCode
→ Physics
→ LiquidFun-Transparency
在项目文件夹中,打开 main.lua
文件。我们将从基本的世界设置开始
液体“容器”
1 2 3 4 5 6 7 8 9 10 11 |
-- 添加三个物理对象作为模拟液体的边界,位于可见屏幕之外 local leftSide = display.newRect( worldGroup, -54-letterboxWidth, display.contentHeight-180, 600, 70 ) physics.addBody( leftSide, "static" ) leftSide.rotation = 86 local centerPiece = display.newRect( worldGroup, display.contentCenterX, display.contentHeight+60+letterboxHeight, 440, 120 ) physics.addBody( centerPiece, "static" ) local rightSide = display.newRect( worldGroup, display.contentWidth+54+letterboxWidth, display.contentHeight-180, 600, 70 ) physics.addBody( rightSide, "static" ) rightSide.rotation = -86 |
在此代码块中,我使用三个静态矩形在屏幕边界之外构建一个容器。容器顶部是开放的,它稍后将容纳屏幕内部的水。
滚动背景
我希望即使演示也看起来很漂亮,所以在下一个代码块中,我通过将两个相同的背景图像彼此相邻放置来添加一个无尽的滚动背景,一个完全可见在屏幕中间,另一个在其右侧。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
-- 创建一个无尽的滚动背景,使用来自“Freeze!”的背景图像 local background1 = display.newImageRect( worldGroup, "background.png", 320, 480 ) background1.x = 160 background1.y = 240 background1.xScale = 1.202 background1.yScale = 1.200 transition.to( background1, { time=12000, x=-224, iterations=0 } ) local background2 = display.newImageRect( worldGroup, "background.png", 320, 480 ) background2.x = 544 background2.y = 240 background2.xScale = 1.202 background2.yScale = 1.200 transition.to( background2, { time=12000, x=160, iterations=0 } ) |
请注意,两个图像都缓慢地向左移动,然后通过无尽的迭代,将它们设置回其原始位置并再次移动,所有这些都使用简单的 transition.to() 调用。
英雄
接下来,我将我们的眼睛英雄添加到场景中,作为一个动态物理对象,它可以在水面上游泳。它也可以被用户触摸和拖动。我不会深入探讨触摸拖动代码,关于这方面之前已经有很多文章和演示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
-- 创建我们的眼睛(“冻结!”的英雄) local hero = display.newImageRect( worldGroup, "hero.png", 64, 64 ) hero.x = 160 hero.y = -400 physics.addBody( hero, { density=0.7, friction=0.3, bounce=0.2, radius=30 } ) -- 通过触摸处理程序和物理触摸关节使英雄可拖动 local function dragBody( event ) local body = event.target local phase = event.phase if ( "began" == phase ) then display.getCurrentStage():setFocus( body, event.id ) body.isFocus = true body.tempJoint = physics.newJoint( "touch", body, event.x, event.y ) body.isFixedRotation = true elseif ( body.isFocus ) then if ( "moved" == phase ) then body.tempJoint:setTarget( event.x, event.y ) elseif ( "ended" == phase or "cancelled" == phase ) then display.getCurrentStage():setFocus( body, nil ) body.isFocus = false event.target:setLinearVelocity( 0,0 ) event.target.angularVelocity = 0 body.tempJoint:removeSelf() body.isFixedRotation = false end end return true end hero:addEventListener( "touch", dragBody ) |
LiquidFun 粒子系统和水
在接下来的代码块中,创建了用于水的粒子系统,并在场景中放置了一个大的水粒子矩形。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
-- 为水创建 LiquidFun 粒子系统 local particleSystem = physics.newParticleSystem{ filename = "liquidParticle.png", radius = 3, imageRadius = 5, gravityScale = 1.0, strictContactCheck = true } -- 创建一个水的“块”(LiquidFun 组) particleSystem:createGroup( { flags = { "tensile" }, x = 160, y = 0, color = { 0.1, 0.1, 0.1, 1 }, halfWidth = 128, halfHeight = 256 } ) |
如果你现在启动代码,这就是结果

水在流动,但它更像油,因为现在还不透明。
添加透明度(首次尝试)
现在事情变得稍微复杂了。我希望将水渲染成半透明的,例如,使用 0.3
的 alpha 值(30%),以便背景能够透过水可见。
如果我更改第 111
行 color
属性的最后一个值(alpha),会产生一些透明度,但并非我想要的效果。

现在,每个粒子的 alpha 值均为 30%。
0.3
,由于所有粒子都略有重叠,因此水的渲染效果很混乱。虽然这在某些情况下可能是一种不错的效果,但绝对不是我想要实现的效果。
使用快照添加透明度
解决方案是将所有粒子渲染到每个帧的 快照 纹理中,然后将透明度应用于整个纹理。以下是相关的代码行
1 2 3 4 5 6 7 |
-- 初始化全屏快照 local snapshot = display.newSnapshot( worldGroup, 320+letterboxWidth+letterboxWidth, 480+letterboxHeight+letterboxHeight ) local snapshotGroup = snapshot.group snapshot.x = 160 snapshot.y = 240 snapshot.canvasMode = "discard" snapshot.alpha = 0.3 |
1 2 3 4 |
-- 将粒子系统插入到快照中 snapshotGroup:insert( particleSystem ) snapshotGroup.x = -160 snapshotGroup.y = -240 |
1 2 3 4 5 |
-- 每帧更新(使失效)快照 local function onEnterFrame( event ) snapshot:invalidate() end Runtime:addEventListener( "enterFrame", onEnterFrame ) |
本质上,创建了一个快照及其组,并将其放置在内容区域的中心。请注意,快照的宽度和高度通过之前在 main.lua
中计算的 letterboxWidth
和 letterboxHeight
变量进行调整——这确保了在 "letterbox"
缩放模式下运行时,快照在不同宽高比的设备上占据整个屏幕。
在第 123
行,我将整个快照的 alpha 值设置为 0.3
,有效地将整个快照纹理设置为 30% 的不透明度。在此之后,粒子系统被插入到快照组中,最后,借助 "enterFrame"
监听器,快照每帧都会失效并重新渲染。
通过添加这个,您可以看到水现在具有真实的透明度。

透明度!终于!
追求美观并添加滤镜
当我达到这一步时,我感到非常高兴。但是 Corona Labs 的酷工程师添加了滤镜支持,所以我开始大量尝试所有滤镜和滤镜选项,以寻求为水添加一个漂亮可见的表面。花了一些时间才达到正确的外观,但最终它真的很容易。
1 2 |
-- 应用“sobel”滤镜来描绘水的可见表面 snapshot.fill.effect = "filter.sobel" |
就是这样,我们现在有了具有透明度和漂亮水面的水!

水“表面”在水面上显示为可见的线
(通过 sobel 滤镜增强的渲染水的边缘)。
接下来该做什么
从这里开始,您可以进行很多实验,例如,尝试英雄眼睛的密度(通过这个您可以影响它是否应该很轻并始终漂浮在水面上,或者它是否应该很重并沉入水下)。
另一个选择是为水使用不同的滤镜并调整它们各自的滤镜设置。
snapshot.fill.effect = "filter.emboss"
snapshot.fill.effect = "filter.frostedGlass"
snapshot.fill.effect = "filter.crystallize"
snapshot.fill.effect = "filter.scatter"
最后,不要忘记您可以用于 LiquidFun 组的许多不同的“标志”——例如,添加 "staticPressure"
,容器底部的水粒子将不会被压缩。
性能问题
并非所有设备都有足够的性能来显示具有这些额外特效的水,同时保持可接受的帧速率。因此,我采取了以下预防措施
- 在 iOS 上,我在所有支持的设备上使用快照(iPhone 4S 及更高版本;iPad 2 及更高版本)。但是,仅在更强大的设备(如 iPad Air 或 iPad mini 2 (iPad4,*)、iPhone 5S (iPhone6,1) 或 iPod Touch 5G (iPod5,1) 及更高版本)上添加额外的 sobel 滤镜。
- 对于 Android,我将
minSdkVersion
设置为"16"
(Android 4.1),以排除许多较旧的设备。此外,我仅在支持高精度着色器的设备上使用 sobel 滤镜,这是通过以下方式确定的
system.getInfo( "gpuSupportsHighPrecisionFragmentShaders" )
- 最后,我测试
system.getInfo( "androidDisplayDensityName" )
是否为"xhdpi"
、"xxhdpi"
和"xxxhdpi"
——只有这些设备才会获得 sobel 滤镜,因为它们相当现代,应该足够快。
结论
如您所见,创建美观的水和其他液体非常容易,而且最终该技术足够好,可以在现代移动设备上使用所有这些功能。我期待看到更多游戏利用这一点,因为液体对于玩家和开发人员来说都非常有趣。
如果您想了解更多关于“Freeze! 2 – Brothers”的信息,请访问 www.freeze2.com,或在 iOS 或 Android 上下载它。我希望您喜欢这款游戏!
Dave Haynes
发布于 11:34,9 月 25 日我一直在考虑向 Puzzlewood Quests 添加某种基于水的谜题,所以这篇文章太完美了。现在我只需要找到时间实际去做它…
Andreas
发布于 02:35,9 月 27 日嗨,Dave,
请花时间——很乐意看到更多使用 LiquidFun 的游戏。 🙂
我正在考虑向“Freeze! 2 – Brothers”添加另一个世界,在那里您实际上可以冻结水并将其变成冰,对冰冻的水进行操作,然后再将其解冻。
我会在接下来的几周内抽出时间测试一些东西,我猜只要我还能把一些代码输入到我的键盘中,我就不会休息。如果我不能再那样做了,我就会开始像“Cortana,请帮助 Siri 添加一个循环,其中遍历我所有的 LiquidFun 粒子 …”
无论如何。请做吧。然后别忘了告诉我们。
来自德国慕尼黑,啤酒节的故乡,
Andreas
Steven Johnson (Star Crunch)
发布于 13:57,9 月 28 日这看起来(和听起来)真的很不错!
如果这很有趣,我有一个冰效果,或多或少就像我 这里 所做的那样,但在 Corona proper 中。我最近考虑组装某种“材料”包,包括不同的纹理效果及其变体。当然,我想知道在实际使用中会出现哪些需求…
“我会在接下来的几周内抽出时间测试一些东西…”/“……啤酒节的故乡”啊,真是紧张!🙂
Andreas
发布于 13:03,9 月 29 日嗨,Steven,
看起来很棒!我很乐意窃取这个优秀的代码示例并将其用于我自己的目的。邪恶的我!
但我猜老问题仍然存在,我不能在自定义着色器中使用 LiquidFun 粒子的快照?或者这已经可以实现了,我已经几个月没有关注关于着色器的情况了。
最好的
Andreas
Steven Johnson (Star Crunch)
发布于 15:50,9 月 29 日听起来不错。
我应该指出,这实际上只是 ShaderX6 中描述的效果的一个非常简化的版本,它具有透明度并将背面几何形状融入反射和折射中,因此还有很大的改进空间!(它还包括裂缝和气体之类的东西,除了它们带来的成本和复杂性之外,在移动设备上可能也太微妙了。)
我的 Lua 实现可能看起来有点吓人,因为它引入了额外的实用程序,以便将四个以上的输入塞入着色器。 但是,在实践中,您可以硬编码几个着色器,每个着色器都具有特定情况的值,例如“厚冰”或“光线昏暗的冰”,这将释放一些输入。 这很容易通过 Shader Playground 版本来完成。
(如果您仍然可以使用完全的通用性,如果需要任何帮助,请给我发邮件或私信。)
我在快照上使用着色器没有任何问题。 不过,快照还不能通过复合绘制传递。 这意味着您必须在着色器中合成凹凸贴图,但对于动态粒子来说,这可能是不可避免的。
Carlos Montesino
发布于 23:23,9月26日感谢各位的教程!😀
Andreas
发布于 01:07,9月30日嗨,Steven,
感谢大家的所有建议!
遗憾的是,在我处理实际的着色器(毕竟只是美化效果)之前,我还有很多其他东西要研究,比如将 LiquidFun 粒子冻结在原位(已经有了——几乎——一个解决方案),等等。
但真的很高兴知道我可以使用快照作为着色器的来源。
如果我可以使用快照作为 graphics.newOutline 的来源就好了...
但我想,用快照水面生成的所有复杂形状,newOutline 会产生太多问题。
有很多东西需要考虑。
谢谢 & 祝好
Andreas
顺便说一下
我一直在想“回复”列会变得多窄,现在我知道了——在您上次回答的末尾,不再提供“回复”按钮了。 🙂
Ed Maurina
发布于 23:35,10月1日非常感谢您撰写这篇文章。 我们很多人很难开始尝试更高级的视觉元素。 这给了我们一个优势和尝试的动力。