查看原文
其他

Cocos Creator 3.0 3D 之相机跟随与旋转

JoeyHuang312 COCOS 2022-06-10

Cocos Creator 3.0 正式版更新后,许多同学开始尝试着在相机上做些简单的事情。现在吃鸡或者 FPS 类的游戏挺火,相机跟随方面的技术必不可少,但目前 Cocos Creator 3D 这方面的代码文章尚不多

所以今天想跟大家分享下此篇文章,通过学习四元数,理解相机的旋转,实现游戏中常见的第一人称视角及第三人称视角

本文转载自 Cocos 中文社区,作者:JoeyHuang312,欢迎阅读。



01

初探


基础我就不说了,下面我就直接来点代码。

Cocos 3D 的四元数类 Qaut.ts 帮我们封装了基本上常用的四元数计算、算法和相关公式。

虽然我们这篇文章主题是跟随与旋转,但其实绝大部分是针对四元数旋转来说的。对于初学四元数的人来说可能会有些生疏,要运用起来没那么容易,下面我们针对几个方法来讲下 Quat 类如何使用:

//static rotateY<Out extends IQuatLike>(out: Out, a: Out, rad: number): Out; //Quat里面很多方法第一个参数都带一个输出的参数,表示的是当前经过计算要输出的四元数,当然,他的返回值也是计算后的四元数。//下面这句话的意思是绕Y轴旋转指定角度,方法里的单位是以弧度来算的,所以需要转化this.node.rotation=Quat.rotateY(new Quat(),this.node.rotation,this.currAngle*Math.PI/180);//该方法是根据欧拉角获取四元数,欧拉角单位是角度this.node.rotation=Quat.fromEuler(new Quat(),this.angleY,this.angleX,0);//绕世界空间下指定轴旋转四元数,弧度为单位,绕UP轴Quat.rotateAround(_quat,this.node.rotation,Vec3.UP,rad);

说下 rotateAround 这个方法,通过配图看我们可以很清楚的看到,给定原四元数,还有旋转轴和角度(该方法以弧度为单位),就能确定绕着这个轴旋转后的四元数,也就是旋转后的位置。

至于具体的计算原理,这些计算四元数的公式其实百度就有一大把,想要更加深入的了解可以看下我上面发的四元数的知识链接。本文主要还是针对相机跟随旋转视角来讲,就不深入来讲四元数了。

我们还可以看下这三个方法的源码:

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-axis Quat.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;    }

其实这第一个参数我是觉得是不是有点多余,因为每次都要把多一个参数进去。为了更方便使用,我自己又写了个四元数类,去掉了第一个参数。

PS:文章有个 Quaternion 类都是我自己封装的,和 Quat 是差不多的,在原文的源码有。


02

环绕物体


在 Cocos 3D 环绕物体旋转,没什么好说的,就几句代码,相关说明也有注释了。

update(dt:number) { //围绕旋转 Quaternion.RotationAroundNode(this.node,this.target.position,Vec3.UP,0.5); this.node.lookAt(this.target.position);    }

这个方法和上面的 Quat.rotateAround 方法是差不多的,只是这里多封装了一个修改变换的位置,使得 Node 按照这个旋转弧度去获取位置更改位置。(PS:后面我会持续改进,还请继续关注哈)

这里说下 RotationAroundNode 这个方法,因为后面还会弄得到,这里 RotationAroundNode 我直接封装在一个类里面了(源码在原文后面),里面的具体实现方法是:

/** * 将变换围绕穿过世界坐标中的 point 的 axis 旋转 angle 度。 * 这会修改变换的位置和旋转。 * @param self 要变换旋转的目标 * @param pos 指定围绕的point * @param axis 旋转轴 * @param angle 旋转角度 */ 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; //根据旋转轴和旋转弧度计算四元数 Quat.fromAxisAngle(_quat,axis,rad); //相减,目标点与相机点之间的向量 Vec3.subtract(v1,pos2,pos); //把向量dir根据计算到的四元数旋转,然后计算出旋转后的距离 Vec3.transformQuat(v2,v1,_quat); self.position=Vec3.add(v2,pos,v2); //根据轴和弧度绕世界空间下指定轴旋转四元数 Quat.rotateAround(_quat,self.rotation,axis,rad); return _quat;    }

这段代码如果不是很理解四元数的原理的话较难消化,当然你也可以直接用,并不会有什么问题。不过这里还是建议大家理解四元数这个概念,会对 3D 旋转有个很深的理解。


03

FPS必备-第一人称跟随


第一人称视角,例如我们在玩赛车的时候,都会有两个视角,一个是从赛车外面看向前方,还有个就是以自己为中心从车里面望向车外了。

  1. 很简单的两句代码,先根据鼠标的偏移量来设置欧拉角,再把欧拉角转换为四元数赋给相机 

  2. 注意这里鼠标的X方向表示的是绕Y轴旋转,鼠标Y方向表示的是绕X轴旋转。所有在赋值转换的时候是倒过来的。

  3. 相机抬头低头我还做了个限制角度

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); //欧拉角转换为四元数 this.node.rotation=Quaternion.GetQuatFromAngle(new Vec3(this.angleY,this.angleX,0));
return ;     }


04

上帝视角-第三人称跟随 


上帝视角的第三人称跟随,好像不同游戏还有不同的跟随方法,这里我先列举出三个,以后如果还遇到更多的,我再补充。


05

简单的


这就是一个很简单的跟随,设置好距离目标的高度和距离,获取到相机要走到的目标位置,再对相机进行插值运算。

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

尾随屁股后面


哈哈,尾随这个词,是不是有点不好听。

好像很多 RPG 游戏都是这样的视角,一直跟在后方,相机是不能随意旋转的,他会自动根据人物的正前方向去旋转。

具体说明下面的注释里也有,好像也没什么好说的。还有注释的两句代码是 Cocos 3D 原方法的,我用的是我自己略微修改了的封装的方法。

//这里计算出相机距离目标的位置的所在坐标先,距离多高Y,距离多远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);//本来这里应该是减的,可是下面的lookat默认前方是-z,所有这里倒转过来变为加Vec3.add(pos, pos, f);//球形差值移动,我发现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);
  1. 我们为了在游戏中更好的实现某一缓动效果,都要利用到插值。如果只是单纯的绑定相互关系,实现出来的效果肯定很生硬,但我们加入插值计算之后,就能很好地实现镜头的缓冲效果。

  2. Lerp 是线性插值,在两点之间进行插值计算,进行移动。

  3. SmoothDampV3 平滑缓冲,东西不是僵硬的移动而是做减速缓冲运动到指定位置,Lerp 更像是线性衰减,而 SmoothDamp 像是弧形衰减,两者都是由快而慢。

最后两句代码是相机方向对准目标,两句代码原理是一样的都是这样的:

let _quat = new Quat();Vec3.normalize(_forward,_forward);//根据视口的前方向和上方向计算四元数Quat.fromViewUp(_quat,_forward,_upwards);

如果大家还要看更加深入点的代码原理,可以查看 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

我要自由旋转的


这个可以说是上面那个的升级版,因为这个跟随着的时候相机是可以根据鼠标来上下左右旋转的,而且目标会根据相机的正方向去行走。

PS:需要配套目标人物相关的旋转代码去使用,源码戳文末原文

/** * 实时设置相机距离目标的位置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); }
/** * 计算根据鼠标X,Y偏移量来围绕X轴和Y轴的旋转四元数 * @param e */ private SetIndependentRotation(e: EventMouse) {
let radX: number = -e.movementX; let radY: number = -e.movementY; let _quat: Quat = new Quat();
//计算绕X轴旋转的四元数并应用到node,这里用的是鼠标上下Y偏移量 let _right = Vec3.transformQuat(this._right, Vec3.RIGHT, this.node.rotation); _quat = Quaternion.RotationAroundNode(this.node, this.target.position, _right, radY); //获取欧拉角,限制相机抬头低头的范围 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轴旋转的四元数并应用到node,这里用的是鼠标上下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));    }

人物目标跟随旋转:

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 轴旋转的代码,我限制了相机的抬头和低头的范围是,角度在120-180 和 -180->-170。

之所以我会设很奇葩的这两个范围是因为,如果相机正对着物体的时候是 0 度,从正对到抬头是 0-180,从正对到低头是 -180-0,所有才会分开判断。

这里要分开设置 X 轴和 Y 轴旋转的代码,可以看到我分开了好多,可能是我方法不对,我目前还没找到简化代码的办法,如果有大神知道恳请指点。

其实通读全文,我们可以看到,用得最多的就是 rotationAround、lookAt、transformQuat、transformQuat 这几个,只要熟悉了对于旋转就没什么好怕的了。



以上就是今天的全部分享,欢迎广大开发者继续挖掘 Cocos Creator 3.0 更多的可能性,点击【阅读原文】前往社区获取完整源码,跟原作者快乐交流

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存