Cocos Creator 3.0 3D 之相機(jī)跟隨與旋轉(zhuǎn)
Cocos Creator 3.0 正式版更新后,許多同學(xué)開始嘗試著在相機(jī)上做些簡單的事情。現(xiàn)在吃雞或者 FPS 類的游戲挺火,相機(jī)跟隨方面的技術(shù)必不可少,但目前 Cocos Creator 3D 這方面的代碼文章尚不多。
所以今天想跟大家分享下此篇文章,通過學(xué)習(xí)四元數(shù),理解相機(jī)的旋轉(zhuǎn),實(shí)現(xiàn)游戲中常見的第一人稱視角及第三人稱視角。
本文轉(zhuǎn)載自 Cocos 中文社區(qū),作者:JoeyHuang312,歡迎閱讀。
01
初探
基礎(chǔ)我就不說了,下面我就直接來點(diǎn)代碼。
Cocos 3D 的四元數(shù)類 Qaut.ts 幫我們封裝了基本上常用的四元數(shù)計算、算法和相關(guān)公式。
雖然我們這篇文章主題是跟隨與旋轉(zhuǎn),但其實(shí)絕大部分是針對四元數(shù)旋轉(zhuǎn)來說的。對于初學(xué)四元數(shù)的人來說可能會有些生疏,要運(yùn)用起來沒那么容易,下面我們針對幾個方法來講下 Quat 類如何使用:
//static rotateY<Out extends IQuatLike>(out: Out, a: Out, rad: number): Out;//Quat里面很多方法第一個參數(shù)都帶一個輸出的參數(shù),表示的是當(dāng)前經(jīng)過計算要輸出的四元數(shù),當(dāng)然,他的返回值也是計算后的四元數(shù)。//下面這句話的意思是繞Y軸旋轉(zhuǎn)指定角度,方法里的單位是以弧度來算的,所以需要轉(zhuǎn)化this.node.rotation=Quat.rotateY(new Quat(),this.node.rotation,this.currAngle*Math.PI/180);//該方法是根據(jù)歐拉角獲取四元數(shù),歐拉角單位是角度this.node.rotation=Quat.fromEuler(new Quat(),this.angleY,this.angleX,0);//繞世界空間下指定軸旋轉(zhuǎn)四元數(shù),弧度為單位,繞UP軸Quat.rotateAround(_quat,this.node.rotation,Vec3.UP,rad);
說下 rotateAround 這個方法,通過配圖看我們可以很清楚的看到,給定原四元數(shù),還有旋轉(zhuǎn)軸和角度(該方法以弧度為單位),就能確定繞著這個軸旋轉(zhuǎn)后的四元數(shù),也就是旋轉(zhuǎn)后的位置。
至于具體的計算原理,這些計算四元數(shù)的公式其實(shí)百度就有一大把,想要更加深入的了解可以看下我上面發(fā)的四元數(shù)的知識鏈接。本文主要還是針對相機(jī)跟隨旋轉(zhuǎn)視角來講,就不深入來講四元數(shù)了。
我們還可以看下這三個方法的源碼:
public static rotateY<Out extends IQuatLike> (out: Out, a: Out, rad: number) {rad *= 0.5;const by = Math.sin(rad);const bw = Math.cos(rad);const { x, y, z, w } = a;out.x = x * bw - z * by;out.y = y * bw + w * by;out.z = z * bw + x * by;out.w = w * bw - y * by;return out;}public static rotateAround<Out extends IQuatLike, VecLike extends IVec3Like> (out: Out, rot: Out, axis: VecLike, rad: number) {// get inv-axis (local to rot)Quat.invert(qt_1, rot);Vec3.transformQuat(v3_1, axis, qt_1);// rotate by inv-axisQuat.fromAxisAngle(qt_1, v3_1, rad);Quat.multiply(out, rot, qt_1);return out;}public static fromEuler<Out extends IQuatLike> (out: Out, x: number, y: number, z: number) {x *= halfToRad;y *= halfToRad;z *= halfToRad;const sx = Math.sin(x);const cx = Math.cos(x);const sy = Math.sin(y);const cy = Math.cos(y);const sz = Math.sin(z);const cz = Math.cos(z);out.x = sx * cy * cz + cx * sy * sz;out.y = cx * sy * cz + sx * cy * sz;out.z = cx * cy * sz - sx * sy * cz;out.w = cx * cy * cz - sx * sy * sz;return out;}
其實(shí)這第一個參數(shù)我是覺得是不是有點(diǎn)多余,因為每次都要把多一個參數(shù)進(jìn)去。為了更方便使用,我自己又寫了個四元數(shù)類,去掉了第一個參數(shù)。
PS:文章有個 Quaternion 類都是我自己封裝的,和 Quat 是差不多的,在原文的源碼有。
02
環(huán)繞物體
在 Cocos 3D 環(huán)繞物體旋轉(zhuǎn),沒什么好說的,就幾句代碼,相關(guān)說明也有注釋了。
update(dt:number){//圍繞旋轉(zhuǎn)Quaternion.RotationAroundNode(this.node,this.target.position,Vec3.UP,0.5);this.node.lookAt(this.target.position);}
這個方法和上面的 Quat.rotateAround 方法是差不多的,只是這里多封裝了一個修改變換的位置,使得 Node 按照這個旋轉(zhuǎn)弧度去獲取位置更改位置。(PS:后面我會持續(xù)改進(jìn),還請繼續(xù)關(guān)注哈)
這里說下 RotationAroundNode 這個方法,因為后面還會弄得到,這里 RotationAroundNode 我直接封裝在一個類里面了(源碼在原文后面),里面的具體實(shí)現(xiàn)方法是:
/*** 將變換圍繞穿過世界坐標(biāo)中的 point 的 axis 旋轉(zhuǎn) angle 度。* 這會修改變換的位置和旋轉(zhuǎn)。* @param self 要變換旋轉(zhuǎn)的目標(biāo)* @param pos 指定圍繞的point* @param axis 旋轉(zhuǎn)軸* @param angle 旋轉(zhuǎn)角度*/public static RotationAroundNode(self:Node,pos:Vec3,axis:Vec3,angle:number):Quat{let _quat=new Quat();let v1=new Vec3();let v2=new Vec3();let pos2:Vec3=self.position;let rad=angle* this.Deg2Rad;//根據(jù)旋轉(zhuǎn)軸和旋轉(zhuǎn)弧度計算四元數(shù)Quat.fromAxisAngle(_quat,axis,rad);//相減,目標(biāo)點(diǎn)與相機(jī)點(diǎn)之間的向量Vec3.subtract(v1,pos2,pos);//把向量dir根據(jù)計算到的四元數(shù)旋轉(zhuǎn),然后計算出旋轉(zhuǎn)后的距離Vec3.transformQuat(v2,v1,_quat);self.position=Vec3.add(v2,pos,v2);//根據(jù)軸和弧度繞世界空間下指定軸旋轉(zhuǎn)四元數(shù)Quat.rotateAround(_quat,self.rotation,axis,rad);return _quat;}
這段代碼如果不是很理解四元數(shù)的原理的話較難消化,當(dāng)然你也可以直接用,并不會有什么問題。不過這里還是建議大家理解四元數(shù)這個概念,會對 3D 旋轉(zhuǎn)有個很深的理解。
03
FPS必備-第一人稱跟隨
第一人稱視角,例如我們在玩賽車的時候,都會有兩個視角,一個是從賽車外面看向前方,還有個就是以自己為中心從車?yán)锩嫱蜍囃饬恕?/p>
很簡單的兩句代碼,先根據(jù)鼠標(biāo)的偏移量來設(shè)置歐拉角,再把歐拉角轉(zhuǎn)換為四元數(shù)賦給相機(jī)
注意這里鼠標(biāo)的X方向表示的是繞Y軸旋轉(zhuǎn),鼠標(biāo)Y方向表示的是繞X軸旋轉(zhuǎn)。所有在賦值轉(zhuǎn)換的時候是倒過來的。
相機(jī)抬頭低頭我還做了個限制角度
private MouseMove(e: EventMouse) {this.angleX+=-e.movementX;this.angleY+=-e.movementY;console.log(this.angleY);this.angleY=this.Clamp(this.angleY,this.xAxisMin,this.xAxisMax);//this.node.rotation=Quat.fromEuler(new Quat(),this.angleY,this.angleX,0);//歐拉角轉(zhuǎn)換為四元數(shù)this.node.rotation=Quaternion.GetQuatFromAngle(new Vec3(this.angleY,this.angleX,0));return ;}
04
上帝視角-第三人稱跟隨
上帝視角的第三人稱跟隨,好像不同游戲還有不同的跟隨方法,這里我先列舉出三個,以后如果還遇到更多的,我再補(bǔ)充。
05
簡單的
這就是一個很簡單的跟隨,設(shè)置好距離目標(biāo)的高度和距離,獲取到相機(jī)要走到的目標(biāo)位置,再對相機(jī)進(jìn)行插值運(yùn)算。
let temp: Vec3 = new Vec3();Vec3.add(temp, this.lookAt.worldPosition, new Vec3(0, this.positionOffset.y, this.positionOffset.z));this.node.position = this.node.position.lerp(temp, this.moveSmooth);
06
尾隨屁股后面
哈哈,尾隨這個詞,是不是有點(diǎn)不好聽。
好像很多 RPG 游戲都是這樣的視角,一直跟在后方,相機(jī)是不能隨意旋轉(zhuǎn)的,他會自動根據(jù)人物的正前方向去旋轉(zhuǎn)。
具體說明下面的注釋里也有,好像也沒什么好說的。還有注釋的兩句代碼是 Cocos 3D 原方法的,我用的是我自己略微修改了的封裝的方法。
//這里計算出相機(jī)距離目標(biāo)的位置的所在坐標(biāo)先,距離多高Y,距離多遠(yuǎn)Z//下面四句代碼等同于:targetPosition+Up*updistance-forwardView*backDistancelet u = Vec3.multiplyScalar(new Vec3(), Vec3.UP, this.positionOffset.y);let f = Vec3.multiplyScalar(new Vec3(), this.target.forward, this.positionOffset.z);let pos = Vec3.add(new Vec3(), this.target.position, u);//本來這里應(yīng)該是減的,可是下面的lookat默認(rèn)前方是-z,所有這里倒轉(zhuǎn)過來變?yōu)榧?/span>Vec3.add(pos, pos, f);//球形差值移動,我發(fā)現(xiàn)cocos只有Lerp差值移動,而我看unity是有SmoothDampV3平滑緩沖移動的,所有我這里照搬過來了一個球形差值this.node.position = VectorTool.SmoothDampV3(this.node.position, pos, this.velocity, this.moveSmooth, 100000, 0.02);//cocos的差值移動//this.node.position=this.node.position.lerp(pos,this.moveSmooth);//計算前方向this.forwardView = Vec3.subtract(this.forwardView, this.node.position, this.target.getWorldPosition());//this.node.lookAt(this.target.worldPosition);this.node.rotation=Quaternion.LookRotation(this.forwardView);
我們?yōu)榱嗽谟螒蛑懈玫膶?shí)現(xiàn)某一緩動效果,都要利用到插值。如果只是單純的綁定相互關(guān)系,實(shí)現(xiàn)出來的效果肯定很生硬,但我們加入插值計算之后,就能很好地實(shí)現(xiàn)鏡頭的緩沖效果。
Lerp 是線性插值,在兩點(diǎn)之間進(jìn)行插值計算,進(jìn)行移動。
SmoothDampV3 平滑緩沖,東西不是僵硬的移動而是做減速緩沖運(yùn)動到指定位置,Lerp 更像是線性衰減,而 SmoothDamp 像是弧形衰減,兩者都是由快而慢。
最后兩句代碼是相機(jī)方向?qū)?zhǔn)目標(biāo),兩句代碼原理是一樣的都是這樣的:
let _quat = new Quat();Vec3.normalize(_forward,_forward);//根據(jù)視口的前方向和上方向計算四元數(shù)Quat.fromViewUp(_quat,_forward,_upwards);
如果大家還要看更加深入點(diǎn)的代碼原理,可以查看 Cocos 源碼,具體源碼位置在:
Window 位置:F:\CocosDashboard\resources.editors\Creator\3.0.0\resources\resources\3d\engine\cocos\core\math\quat.ts
Mac 位置:/Applications/CocosCreator/Creator/3.0.0/CocosCreator.app/Contents/Resources/resources/3d/engine/bin/.cache/dev/editor/transform-cache/fs/cocos/core/math/quat.js Window 位置:
07
我要自由旋轉(zhuǎn)的
這個可以說是上面那個的升級版,因為這個跟隨著的時候相機(jī)是可以根據(jù)鼠標(biāo)來上下左右旋轉(zhuǎn)的,而且目標(biāo)會根據(jù)相機(jī)的正方向去行走。
PS:需要配套目標(biāo)人物相關(guān)的旋轉(zhuǎn)代碼去使用,源碼戳文末原文。
/*** 實(shí)時設(shè)置相機(jī)距離目標(biāo)的位置position*/public SetMove() {this._forward = new Vec3();this._right = new Vec3();this._up = new Vec3();Vec3.transformQuat(this._forward, Vec3.FORWARD, this.node.rotation);//Vec3.transformQuat(this._right, Vec3.RIGHT, this.node.rotation);//Vec3.transformQuat(this._up, Vec3.UP, this.node.rotation);this._forward.multiplyScalar(this.positionOffset.z);//this._right.multiplyScalar(this.positionOffset.x);//this._up.multiplyScalar(this.positionOffset.y);let desiredPos = new Vec3();desiredPos = desiredPos.add(this.lookAt.worldPosition).subtract(this._forward).add(this._right).add(this._up);this.node.position = this.node.position.lerp(desiredPos, this.moveSmooth);}/*** 計算根據(jù)鼠標(biāo)X,Y偏移量來圍繞X軸和Y軸的旋轉(zhuǎn)四元數(shù)* @param e*/private SetIndependentRotation(e: EventMouse) {let radX: number = -e.movementX;let radY: number = -e.movementY;let _quat: Quat = new Quat();//計算繞X軸旋轉(zhuǎn)的四元數(shù)并應(yīng)用到node,這里用的是鼠標(biāo)上下Y偏移量let _right = Vec3.transformQuat(this._right, Vec3.RIGHT, this.node.rotation);_quat = Quaternion.RotationAroundNode(this.node, this.target.position, _right, radY);//獲取歐拉角,限制相機(jī)抬頭低頭的范圍this.angle = Quaternion.GetEulerFromQuat(_quat);this.angle.x = this.angle.x > 0 ? this.Clamp(this.angle.x, 120, 180) : this.Clamp(this.angle.x, -180, -170);Quat.fromEuler(_quat, this.angle.x, this.angle.y, this.angle.z);this.node.setWorldRotation(_quat);//計算繞Y軸旋轉(zhuǎn)的四元數(shù)并應(yīng)用到node,這里用的是鼠標(biāo)上下X偏移量_quat = Quaternion.RotationAroundNode(this.node, this.target.position, Vec3.UP, radX);this.node.setWorldRotation(_quat);this.angle = Quaternion.GetEulerFromQuat(_quat);this.MouseX = this.angle.y;this.MouseY = this.angle.x;//console.log(this.MouseX.toFixed(2),this.MouseY.toFixed(2));}
人物目標(biāo)跟隨旋轉(zhuǎn):
if (data.keyCode == macro.KEY.a || data.keyCode == macro.KEY.d|| data.keyCode == macro.KEY.w || data.keyCode == macro.KEY.s) {if (this.camera.GetType()== ThirdPersonCameraType.FollowIndependentRotation) {let fq = Quat.fromEuler(new Quat(), 0, this.camera.MouseX, 0);this.node.rotation = Quat.slerp(new Quat(), this.node.rotation, fq, 0.1);}else {//this.node.rotation=Quat.rotateY(new Quat(),this.node.rotation,this.currAngle*Math.PI/180);Quaternion.RotateY(this.node, this.currAngle);}this.node.translate(this.movemenet, Node.NodeSpace.LOCAL);}
上面有句限制繞 X 軸旋轉(zhuǎn)的代碼,我限制了相機(jī)的抬頭和低頭的范圍是,角度在120-180 和 -180->-170。
之所以我會設(shè)很奇葩的這兩個范圍是因為,如果相機(jī)正對著物體的時候是 0 度,從正對到抬頭是 0-180,從正對到低頭是 -180-0,所有才會分開判斷。
這里要分開設(shè)置 X 軸和 Y 軸旋轉(zhuǎn)的代碼,可以看到我分開了好多,可能是我方法不對,我目前還沒找到簡化代碼的辦法,如果有大神知道懇請指點(diǎn)。
其實(shí)通讀全文,我們可以看到,用得最多的就是 rotationAround、lookAt、transformQuat、transformQuat 這幾個,只要熟悉了對于旋轉(zhuǎn)就沒什么好怕的了。
以上就是今天的全部分享,歡迎廣大開發(fā)者繼續(xù)挖掘 Cocos Creator 3.0 更多的可能性,點(diǎn)擊【閱讀原文】前往社區(qū)獲取完整源碼,跟原作者快樂交流
