臥槽,3D錯(cuò)覺!這腦洞簡(jiǎn)直了...
引言:去年,獨(dú)立游戲《籠中窺夢(mèng)》獨(dú)特的美術(shù)表現(xiàn)令無(wú)數(shù)玩家贊嘆。游戲利用「視錯(cuò)覺」來(lái)設(shè)計(jì)謎題關(guān)卡,創(chuàng)造了一個(gè)奇妙的立方體世界。本次,「Cocos Star Writer」Nowpaper 將在 v3.5 中實(shí)現(xiàn)同款視錯(cuò)效果,源碼和視頻教程見文末。

「視錯(cuò)覺」解密游戲《籠中窺夢(mèng)》
《籠中窺夢(mèng)》有很多值得考究的技術(shù)細(xì)節(jié)點(diǎn),那么今天,我們將在?Cocos Creator 3.5 中使用可渲染紋理和自定義著色器實(shí)現(xiàn)一下這個(gè)游戲的方盒視錯(cuò)功能,一窺其技術(shù)實(shí)現(xiàn)思路。

Demo 最終效果
這一次的技術(shù)實(shí)現(xiàn)靈感來(lái)自于我正在制作的另外一個(gè)技術(shù)視頻,那是探討如何在 v3.x 中實(shí)現(xiàn)瞄準(zhǔn)鏡和額外屏幕技術(shù)(PS.這個(gè)方案很快也會(huì)分享給大家)。偶然發(fā)現(xiàn)其中的部分,只要結(jié)合之前的傳送門技術(shù)實(shí)現(xiàn)方法,就能做出《籠中窺夢(mèng)》的方盒視錯(cuò),于是率先完成了這個(gè)方案,第一時(shí)間分享給大家。

實(shí)現(xiàn)瞄準(zhǔn)鏡和額外屏幕技術(shù)
場(chǎng)景準(zhǔn)備,實(shí)現(xiàn)分析
《籠中窺夢(mèng)》中的盒子,各個(gè)面展示了完全不同的場(chǎng)景,玩家通過(guò)旋轉(zhuǎn)攝像機(jī)找到合適的位置,組合達(dá)成過(guò)關(guān)條件。因此我們需要解決的是:如何讓盒子的各個(gè)面顯示不同的畫面。
正常來(lái)說(shuō),我們將使用5個(gè)攝像機(jī),渲染不同的畫面到方盒的5個(gè)面上,這里使用 RenderToTexture 可以很容易地完成。我準(zhǔn)備了一些素材搭建了幾個(gè)不同的場(chǎng)景,需要注意的是,需要注意的是,這里將場(chǎng)景按節(jié)點(diǎn)分割,方便管理。

首先搭建一個(gè)基礎(chǔ)場(chǎng)景,中間擺放一個(gè)盒子,并且建立對(duì)應(yīng)各方向的面片。但如果五個(gè)表面都制作,那么場(chǎng)景搭建將會(huì)異常復(fù)雜,為了降低復(fù)雜度,我們先做出兩個(gè)表面,對(duì)應(yīng)的也得做出來(lái)2個(gè)場(chǎng)景用來(lái)當(dāng)謎題。
這些場(chǎng)景搭建并不復(fù)雜,但是如果真的要實(shí)現(xiàn)完整的關(guān)卡,這就得需要精細(xì)設(shè)計(jì)了。本文主要探討技術(shù)實(shí)現(xiàn),就不做那么細(xì)致了。
在項(xiàng)目設(shè)置中,新增6個(gè) Layer 用來(lái)標(biāo)記渲染分組,因?yàn)閷?duì)應(yīng)的攝像機(jī),只需要渲染指定的渲染層即可,不用全部都渲染出來(lái)。
現(xiàn)在為它們每個(gè)組的根節(jié)點(diǎn)設(shè)置對(duì)應(yīng)的 Layer,以方便將來(lái)攝像機(jī)的設(shè)置。將基本場(chǎng)景的根節(jié)點(diǎn)設(shè)置成為 Base,而其他的兩個(gè)場(chǎng)景根節(jié)點(diǎn)分別設(shè)置為 Layer 1 和2。如此一來(lái)準(zhǔn)備工作就完成了。


攝像機(jī)的控制代碼
接著我們需要先實(shí)現(xiàn)一個(gè)攝像機(jī)的渲染控制代碼,讓攝像機(jī)圍繞著一個(gè)物體進(jìn)行旋轉(zhuǎn)。代碼相對(duì)來(lái)說(shuō)比較簡(jiǎn)單,通過(guò)監(jiān)聽 TouchMove 事件處理旋轉(zhuǎn),具體的請(qǐng)參看論壇中一些大佬的分享。
import?{?_decorator,?Component,?Node,??input,?Input,?EventTouch,?Vec3,?v3,?Quat,?math,?quat?}?from?'cc';
const?{?ccclass,?property?}?=?_decorator;
@ccclass('CameraControl')
export?class?CameraControl?extends?Component?{
????@property(Node)
????target:?Node?=?null;
????public?yMinLimit?=?-90;//相機(jī)向下最大角度
????public?yMaxLimit?=?0;//相機(jī)向上最大角度
????private?targetX?=?0;
????private?targetY?=?0;
????private?xSpeed?=?25;
????private?ySpeed?=?12;
????private?disance:?Vec3?=?v3();
????start()?{
????????let?angles?=?this.node.eulerAngles;
????????this.targetX?=?angles.y;
????????this.targetY?=?angles.x;
????????input.on(Input.EventType.TOUCH_MOVE,?this.onTouchMove,?this);
????????Vec3.subtract(this.disance,?this.node.worldPosition,?this.target.worldPosition);
????????this.disance?=?v3(0,0,this.disance.length());
????}
????private?onTouchMove(touch:?EventTouch)?{
????????this.targetX?-=?touch.getDeltaX()?*?this.xSpeed?*?0.02;
????????this.targetY?-=?touch.getDeltaY()?*?this.ySpeed?*?0.02;
????????this.targetY?=?this.ClampAngle(this.targetY,?this.yMinLimit,?this.yMaxLimit);
???????
????}
????private?_quat?=?quat();
????private?_vec3?=?v3();
????update?(deltaTime:?number)?{
????????Quat.fromEuler(this._quat,?this.targetY,?this.targetX,?0);
????????Vec3.transformQuat(this._vec3,?this.disance,?this._quat);
????????Vec3.add(this._vec3,?this.target.worldPosition,?this._vec3);
????????this.node.worldPosition?=?this._vec3.clone();
????????this.node.worldRotation?=?this._quat.clone();
????}
????ClampAngle(angle,?min,?max)?{
????????if?(angle?-360)?angle?+=?360;
????????if?(angle?>?360)?angle?-=?360;
????????return?math.clamp(angle,?min,?max);
????}
}
回到編輯器中,為主攝像機(jī)添加腳本,并且指定目標(biāo),測(cè)試旋轉(zhuǎn)效果。

新建一個(gè)攝像機(jī),命名為 Camera 1。然后復(fù)制主攝像機(jī)節(jié)點(diǎn)參數(shù),粘貼給新建的攝像機(jī),調(diào)整到合適的位置,用來(lái)拍攝第一個(gè)場(chǎng)景。設(shè)置這個(gè)攝像機(jī)的可見性掩碼,只勾選 Layer 1 這個(gè) Layer 層。

然后再實(shí)現(xiàn)一個(gè)攝像機(jī)同步的代碼,用來(lái)處理面攝像機(jī)和主攝像機(jī)的同步。實(shí)現(xiàn)同步只需要在開始的時(shí)候,計(jì)算兩個(gè)攝像機(jī)的偏移值,同步位置和旋轉(zhuǎn)時(shí)候修正即可。
import?{?_decorator,?Component,?Node,?Vec3,?v3?}?from?'cc';
const?{?ccclass,?property?}?=?_decorator;
//?同步攝像機(jī)腳本,事實(shí)上如果監(jiān)聽主攝像機(jī)坐標(biāo)、旋轉(zhuǎn)差值會(huì)更加合理
@ccclass('SyncCamera')
export?class?SyncCamera?extends?Component?{
????@property(Node)
????mainCamera:?Node?=?null;
????offset:?Vec3?=?v3();
????_vec3?=?v3();
????start()?{
????????Vec3.subtract(this.offset,?this.node.position,?this.mainCamera.position);
????}
????update(deltaTime:?number)?{
????????Vec3.add(this._vec3,this.mainCamera.position,this.offset);
????????this.node.position?=?this._vec3.clone();
????????this.node.rotation?=?this.mainCamera.rotation;
????}
}
先實(shí)現(xiàn)一個(gè)面
現(xiàn)在我們先實(shí)現(xiàn)一個(gè)面看看效果。
新建一個(gè)材質(zhì),選擇著色器為內(nèi)置 builtin-unlit,勾選 Use Texture,新建一個(gè)可渲染紋理,設(shè)置寬高為 512x512,把剛剛新建的可渲染紋理,拖到上面完成引用。


在場(chǎng)景中選擇 surface 1 的面片,將它材質(zhì)替換,就可以看到攝像機(jī)畫面,渲染到表面1上,現(xiàn)在我們運(yùn)行一下看看效果,由于有攝像機(jī)同步腳本,當(dāng)主攝像機(jī)調(diào)整的時(shí)候輔攝像機(jī)也會(huì)跟著變動(dòng)。

接著用同樣的方式建立第二個(gè)表面的渲染畫面。
但是很快我們會(huì)發(fā)現(xiàn)有個(gè)不對(duì)勁的地方:這個(gè)畫面看起來(lái)非常的平,根本就不像是透視過(guò)去的畫面。這是因?yàn)楝F(xiàn)在表面是直接將拍攝到的渲染畫面顯示出來(lái),這樣肯定是沒(méi)有透視效果的。

這個(gè)時(shí)候就需要一些特殊的處理。在我之前有關(guān)傳送門的分享中,這個(gè)問(wèn)題也沒(méi)有解決得很完美,后來(lái)在 Cocos 的 Panda 大佬的幫助下,通過(guò)修改著色器 Shader 達(dá)成了完美的傳送門效果,本方案就將用到這個(gè)著色器文件,大家也可以到 Cocos Store 中免費(fèi)下載。
「?jìng)魉烷T實(shí)現(xiàn)」源碼免費(fèi)下載:
https://store.cocos.com/app/detail/3275
也可以參考 youtube 大佬 Sebastian Lague 的有關(guān)視頻,他對(duì)透視計(jì)算 Shader 和原理的講解非常詳細(xì),本文中的實(shí)現(xiàn)只是他的簡(jiǎn)化版本。
https://www.youtube.com/watch?v=cWpFZbjtSQg
核心代碼如下:
float?flip?=?cc_cameraPos.w?==?0.0???-1.0?:?1.0;
v_screenPos?=?pos?*?0.5;
v_screenPos.xy?=?vec2(v_screenPos.x,?v_screenPos.y?*?flip)?+?v_screenPos.w;
v_screenPos.zw?=?pos.zw;
將修改過(guò)后的著色器添加到工程,然后選擇 surface 的材質(zhì),修改 Effect 點(diǎn)擊保存,運(yùn)行就可以看到效果,現(xiàn)在透視基本上 OK 了。

不過(guò)好像還有一些小細(xì)節(jié)可以調(diào)整,比如畫質(zhì)發(fā)生了變化、顯示的內(nèi)容更近了,這個(gè)需要我們自行對(duì)攝像機(jī)進(jìn)行調(diào)整。但是為了更好的設(shè)計(jì)關(guān)卡,兩個(gè)攝像機(jī)的有關(guān)參數(shù)應(yīng)該保持一致,否則可能會(huì)出現(xiàn)奇怪的視覺錯(cuò)誤。
視錯(cuò)謎題
經(jīng)過(guò)參數(shù)調(diào)整,效果已經(jīng)非常不錯(cuò)了,那么我們也設(shè)計(jì)一個(gè)簡(jiǎn)單的謎題到其中。
這里有一幅畫,放進(jìn)第一個(gè)場(chǎng)景中,然后再往第二個(gè)場(chǎng)景中添加一個(gè),此時(shí)需要一些空間視覺感,來(lái)調(diào)整對(duì)應(yīng)的位置和角度。這里可能需要我們反復(fù)對(duì)比攝像機(jī)呈現(xiàn)的畫面和最終運(yùn)行結(jié)果,才能調(diào)出滿意的效果。
運(yùn)行一下看看效果。一般情況下,我們透過(guò)兩個(gè)表面看到的畫并不能完全重合,經(jīng)過(guò)拖動(dòng)旋轉(zhuǎn)可以讓其完全重合。

結(jié)語(yǔ)
好了,剩下的就是關(guān)卡設(shè)計(jì)的問(wèn)題了,您可以利用本篇中的技巧,使用 Cocos Creator 完成一個(gè)自己的《籠中窺夢(mèng)》游戲。今天這個(gè)技術(shù)實(shí)現(xiàn)本質(zhì)上是將 RenderToTexture 給用活了,只要稍微調(diào)整一下,你也能做出令人嘆為觀止的創(chuàng)意。
我一直認(rèn)為,技術(shù)工具都是為創(chuàng)意服務(wù),人類的創(chuàng)意總是沒(méi)有盡頭,如何實(shí)現(xiàn)心中所想,除了對(duì)它的熱愛,工具的熟練度和技術(shù)的沉淀,才是創(chuàng)意發(fā)揮的基礎(chǔ)。
資源鏈接
點(diǎn)擊文末【閱讀原文】前往 Cocos Store 下載本項(xiàng)目源碼(含三個(gè)基礎(chǔ)場(chǎng)景資源、三個(gè)面的實(shí)現(xiàn)場(chǎng)景,可以很方便地進(jìn)行擴(kuò)展):
https://store.cocos.com/app/detail/3791
視頻教程(UP 主:Nowpaper)
https://www.bilibili.com/video/BV1D3411G7jy
論壇討論帖:
https://forum.cocos.org/t/topic/135819
今天的文章就到這里了,我是 Nowpaper,一個(gè)混跡游戲行業(yè)的老爸,如果您喜歡我的分享,不妨多多點(diǎn)贊留言,也歡迎關(guān)注我的 B 站,您的支持是我更新的動(dòng)力,下次再見!
Nowpaper 往期分享
用 RenderTexture 實(shí)現(xiàn)小地圖與傳送門
