模擬經(jīng)典流體解謎游戲!Cocos Creator 三步實(shí)現(xiàn)動(dòng)態(tài) 2D 液體

游戲中可交互的液體是一種頗為吸引人的元素,比如經(jīng)典人氣解謎游戲《鱷魚小頑皮愛洗澡》就通過簡(jiǎn)單的「引水入缸」玩法收獲了一大票玩家。
在 Cocos Creator 3.x 中,若想實(shí)現(xiàn) 2D 液體、同時(shí)兼顧運(yùn)行效率,可以選擇使用 Box2D 的物理粒子效果來進(jìn)行模擬。物理粒子系統(tǒng)除了適合用于模擬液體以外,我們也可以用于模擬任何可變形的物體。這里將解析由引擎技術(shù)支持中心帶來的 Cocos Creator 的動(dòng)態(tài) 2D 液體解決方案。
點(diǎn)擊文末【閱讀原文】下載 DEMO:
https://store.cocos.com/app/detail/
PART 1
使用方式
場(chǎng)景搭建
在 Cocos Creator 中新建一個(gè)空?qǐng)鼍?,并?chuàng)建一個(gè)?UICanvas:

創(chuàng)建一個(gè)用于液體渲染的相機(jī) Camera-001 :

對(duì)于這個(gè)相機(jī),使其 ClearFlag = SOLID_COLOR :

這個(gè)相機(jī)的作用是將液體繪制到一張 RT 之后將這個(gè)動(dòng)態(tài)紋理投射到 UI 內(nèi)的某個(gè) sprite 上面。
之后就在場(chǎng)景內(nèi)布置碰撞體,既然是 Box2D,碰撞體記得選 2D,不然碰撞體就會(huì)使用 Bullet 和 Box2d 無關(guān)了:

Cocos Creator 里封裝了兩種物理引擎 Bullet 和 Box2D,兩者處于單獨(dú)的世界。
在 Box2d 里面如果希望碰撞體之間的碰撞有效,那么至少有一方需要持有 Rigidbody2D 組件。因此需要給碰撞體添加一個(gè) RigidBody2D,其類型選擇為 static 。這樣物理引擎不會(huì)去模擬他的速度和受力情況。

添加液體
新建一個(gè)空的 UINode,也就是只有 UITransform 組件的一個(gè) Node:

為其添加 WaterRender 這個(gè)組件:

之后指定好他的一些值:
自定義材質(zhì);

- FixError :FixError 指向的是一張1個(gè) 2x2 的純色小紋理;

- 水管:水管由多個(gè)碰撞體構(gòu)成,這樣可以約束液體,使其往我們想要的地方流動(dòng);


效果預(yù)覽
播放預(yù)覽一下效果(這里開啟了物理調(diào)試,可以更清晰的觀察到粒子的運(yùn)行狀態(tài)):

PART 2
前置知識(shí)
物理引擎
Box2D 是一款輕量級(jí)的 2D 游戲物理引擎。主流游戲引擎的 2D 物理部分大都使用 Box2D 來完成的。在物理引擎模擬中,通過質(zhì)心的受力,計(jì)算出其速度和加速度等最終得到物體所在的位置,之后渲染引擎會(huì)讀取物理引擎的計(jì)算結(jié)果并將其應(yīng)用到渲染上。
LiquidFun

LiquidFun 是基于 Box2D 的擴(kuò)展庫,作用就是給 Box2D 添加了模擬液體的粒子系統(tǒng)。該庫由谷歌高級(jí)程序員 Kentaro Suto 開發(fā),源代碼由 C++ 編寫,并翻譯為 JavaScript。
組裝器
在游戲引擎中,繪制精靈或者模型時(shí),都需要通過生成特定的頂點(diǎn),并調(diào)用驅(qū)動(dòng)方法(OpenGL,DirectX ...等)繪制到屏幕上。在 Cocos Creator?里面,如果我們要繪制一系列頂點(diǎn)到屏幕上,需要使用到?Assembler 組裝器。
組裝器顧名思義就是將頂點(diǎn)組裝起來,以供渲染組件使用。通過這個(gè) Assembler,可以自定義頂點(diǎn)的位置、顏色、紋理坐標(biāo)、索引。
Cocos Creator 里有多種 Assembler:
/**
?*?simple?組裝器
?*?可通過?`UI.simple`?獲取該組裝器。
?*/+
export?const?simple:?IAssembler?
/**
?Tiled組裝器
*/
export?const?tiled:?IAssembler?=?
...
DEMO 中通過讀取物理引擎內(nèi)粒子的位置,計(jì)算出了頂點(diǎn)緩存內(nèi)所有頂點(diǎn)的相關(guān)信息。
PART 3
原理解析
render.ts 里面有兩個(gè)類 WaterRender 和 WaterAssembler 。
WaterRender 解析
WaterRender 是整個(gè) DEMO 的核心類,負(fù)責(zé)粒子的創(chuàng)建和渲染。
Renderable2D
WaterRender 繼承自 Renderable2D 。在 Cocos Creator 中,任何需要渲染的 Node 對(duì)象都會(huì)持有一個(gè) RenderableComponent,其中 Renderable2D 是 Cocos Creator 中渲染 2D 組件的基類。
通過重寫?_render 方法,自定義自己的渲染方案。這里通過使用自定義的 _assembler 來組裝需要繪制的幾何體。
/**
*commitComp會(huì)提交當(dāng)前的渲染數(shù)據(jù)給渲染管線
*/
protected?_render(render:?any)?{
????render.commitComp(this,?this.fixError,?this._assembler!,?null);
}
創(chuàng)建粒子系統(tǒng)
我們可以把液體理解為由很多個(gè)小的水滴組成。這樣對(duì)于物理引擎來說,就可以選擇使用粒子系統(tǒng),以一種高效的方式,來模擬大量水滴運(yùn)動(dòng)的行為。
創(chuàng)建粒子系統(tǒng):
??var?psd_def?=?{
????????????strictContactCheck:?false,
????????????density:?1.0,
????????????gravityScale:?1.0,
????????????radius:?0.35,??//這里指定了粒子的半徑
?????????
????????????...
???}
this._particles?=?this._world.physicsWorld.impl.CreateParticleSystem(psd);
創(chuàng)建粒子組:
var?particleGroupDef?=?{
???...
????shape:?null,
????position:?{
????????x:?this.particleBox.node.getWorldPosition().x?/?PHYSICS_2D_PTM_RATIO,
????????y:?this.particleBox.node.getWorldPosition().y?/?PHYSICS_2D_PTM_RATIO
????},
????//?@ts-ignore
????shape:?this.particleBox._shape._createShapes(1.0,?1.0)[0]
};
this._particleGroup?=?this._particles.CreateParticleGroup(particleGroupDef);
this.SetParticles(this._particles);
粒子組為粒子發(fā)射器定義了一組粒子,這些粒子擁有自定義的形狀:
//創(chuàng)建BoxCollider2D的幾何形狀
shape:?this.particleBox._shape._createShapes(1.0,?1.0)[0]
通過對(duì)液體的觀察,可以發(fā)現(xiàn)液體有一些常見的特性:
水往低處流,水滴會(huì)沿著碰撞體的表面進(jìn)行移動(dòng)
gravityScale: 1.0,定義了粒子受重力影響的系數(shù);黏連性,可觀察到兩個(gè)水滴靠近時(shí),會(huì)在液體的作用力下相互吸引,通過定義
viscousStrength來定義粒子的黏連;壓縮,液體粒子間會(huì)進(jìn)行壓縮,由下面的值來定義粒子允許進(jìn)行的壓縮:
??pressureStrength?
??staticPressureStrength
??staticPressureRelaxation?
??staticPressureIterations表面張力, 我們都知道在水面上放硬幣,硬幣不會(huì)沉底的實(shí)驗(yàn)。這個(gè)就是液體的表面張力。通過下面兩個(gè)屬性,可以調(diào)整液體的表面張力:
????surfaceTensionPressureStrength:?0.2,
????surfaceTensionNormalStrength:?0.2,
WaterAssembler 解析
WaterAssembler 為 RenderableComponent 提供頂點(diǎn)緩存的定制。
在這個(gè)類里面,通過訪問粒子系統(tǒng)的每一個(gè)粒子的位置,生成4個(gè)單獨(dú)的頂點(diǎn):
let?posBuff?=?particles.GetPositionBuffer();
let?r?=?particles.GetRadius()?*?PHYSICS_2D_PTM_RATIO?*?3;
?
for?(let?i?=?0;?i?????let?x?=?posBuff[i].x?*?PHYSICS_2D_PTM_RATIO;
????let?y?=?posBuff[i].y?*?PHYSICS_2D_PTM_RATIO;
????//?left-bottom
????vbuf[vertexOffset++]?=?x?-?r;?//x?
????vbuf[vertexOffset++]?=?y?-?r;?//y
????vbuf[vertexOffset++]?=?0;?//?z?
????vbuf[vertexOffset++]?=?x;?//?u
????vbuf[vertexOffset++]?=?y;?//?v
???...
}
最后計(jì)算索引緩存:
//?fill?indices
const?ibuf?=?buffer.iData!;
for?(let?i?=?0;?i?????ibuf[indicesOffset++]?=?vertexId;
????ibuf[indicesOffset++]?=?vertexId?+?1;
????ibuf[indicesOffset++]?=?vertexId?+?2;
????ibuf[indicesOffset++]?=?vertexId?+?1;
????ibuf[indicesOffset++]?=?vertexId?+?3;
????ibuf[indicesOffset++]?=?vertexId?+?2;
????vertexId?+=?4;
}
頂點(diǎn)緩存描述了頂點(diǎn)的數(shù)據(jù),索引緩存指定了頂點(diǎn)的繪制順序。
這樣就生成了一個(gè)基于粒子中心點(diǎn)的矩形。但是我們最終看到的是圓形,這里的魔法就是通過材質(zhì)和 Effect 系統(tǒng)來解決的。
材質(zhì)和 Shader 解析

模擬時(shí),需要使用 effect.effect 特效倆來模擬。
注意這里選擇的是 transparent 的 technique:

在 effect.effect 的 vert 函數(shù)內(nèi),計(jì)算了兩個(gè)傳輸?shù)?frag 的變量:v_corner 和 v_center ,這兩個(gè)變量代表的是粒子位置的中心點(diǎn)和角落的位置:
??out?vec2?v_corner;
??out?vec2?v_center;
??vec4?vert?()?{
????vec4?pos?=?vec4(a_position.xy,?0,?1);????
????//?no?a_corner?in?web?version
????//?use?a_position?instead?of?a_corner
????v_corner?=?a_position.xy?*?reverseRes;
????//?由于粒子是純色的,texCoord?里面記錄的是粒子的中心點(diǎn)位置
????v_center?=?a_texCoord.xy?*?reverseRes;
????v_corner.y?*=?yratio;
????v_center.y?*=?yratio;
????return?cc_matViewProj?*?pos;
??}
這兩個(gè)變量在 frag 里面通過 smoothstep 進(jìn)行插值的計(jì)算:
smoothstep(edge0,?edge1,?x)?
這個(gè)函數(shù)會(huì)根據(jù)x計(jì)算赫爾米特插值:
t?=?clamp((x?-?edge0)?/?(edge1?-?edge0),?0.0,?1.0);
return?t?*?t?*?(3.0?-?2.0?*?t);

在?frag()?函數(shù)內(nèi)通過計(jì)算像素位置和粒子中心的距離,使用 smoothstep 進(jìn)行插值,粒子的半徑就會(huì)被控制在3倍到1倍半徑之間。同時(shí)由于是根據(jù)中心和半徑計(jì)算,粒子也會(huì)從矩形變成圓形:
??in?vec2?v_corner;
??in?vec2?v_center;
??vec4?frag?()?{
????float?mask?=?smoothstep(radius?*?3.,?radius,?distance(v_corner,?v_center));
????return?vec4(1.0,?1.0,?1.0,?mask);
??}
此時(shí)繪制出來的粒子顏色是白色:

最后通過 display.effect 配合 render texture 將其渲染為藍(lán)色:

在 display.effect 使用了屬性查看器內(nèi)傳入的顏色 color :
??in?vec4?color;
??#if?USE_TEXTURE
????in?vec2?uv0;
????#pragma?builtin(local)
????layout(set?=?2,?binding?=?10)?uniform?sampler2D?cc_spriteTexture;
??#endif
??vec4?frag?()?{
????vec4?o?=?vec4(1,?1,?1,?1);
????#if?USE_TEXTURE
??????o?*=?CCSampleWithAlphaSeparated(cc_spriteTexture,?uv0);
??????#if?IS_GRAY
????????float?gray??=?0.2126?*?o.r?+?0.7152?*?o.g?+?0.0722?*?o.b;
????????o.r?=?o.g?=?o.b?=?gray;
??????#endif
????#endif
????o.a?=?smoothstep(0.95,?1.0,?o.a);
????o?*=?color;
????ALPHA_TEST(o);
????return?o;
??}
??
這個(gè)時(shí)候由于 alpha 的問題會(huì)出現(xiàn)一些毛邊:

因此通過 smoothstep(0.95, 1.0, o.a) ,將像素的 alpha 值都控制在0.95到1之間。
通過這個(gè)渲染我們可以看到,其實(shí)做游戲不一定非要真實(shí)的去模擬,我們只要騙過眼睛就能做出很好的效果了!
本期分享就到這里,DEMO 地址見評(píng)論。在之前我們也和大家分享過 Cocos Creator 3.x 的?2D 動(dòng)態(tài)光照、2D 實(shí)時(shí)陰影的技術(shù)實(shí)現(xiàn)方案,更多方案與 DEMO 請(qǐng)移步論壇集中貼,如果有想了解的技術(shù)或效果實(shí)現(xiàn),歡迎在評(píng)論區(qū)留言,后續(xù)我們會(huì)更新更多關(guān)于游戲引擎的技術(shù)分享。
論壇集中貼
https://forum.cocos.org/t/topic/124637
參考鏈接&擴(kuò)展閱讀
Box2D 開源:
https://github.com/erincatto/box2d
LiquidFun 官網(wǎng):
https://github.com/google/liquidfun
LiquidFun 參考文檔:
https://google.github.io/liquidfun/Programmers-Guide/html/index.html
SmoothStep:
https://en.wikipedia.org/wiki/Smoothstep
往期精彩



