转自订阅号

我们首先来看一看贝塞尔曲线是如何实现的。

我们是如何让鱼自由自在的游动的呢,这里就用到了一个古老的技术,贝塞尔曲线,为什么说古老呢,因为这个技术不是我发明的而且存在了很长时间了,根据百度到的知识(“贝赛尔曲线”是由法国数学家Pierre Bézier所发明,由此为计算机矢量图形学奠定了基础。它的主要意义在于无论是直线或曲线都能在数学上予以描述。),是由法国的数学家发明的,而且网上有好多关于贝塞尔曲线的详细解释,我就不在这里赘述了,我就是参考了这篇文章,https://blog.csdn.net/aimeimeits/article/details/72809382
大家可以拷贝链接到浏览器打开,如果对贝塞尔曲线不甚了解的同学,可以仔细研读这篇文章。
然后我们只需要从这篇文章里面把重点拿出来,翻译成ts语言放到cocos creator里面即可,那么他的重点是什么呢!
他的重点就是贝塞尔曲线的计算公式
是不是看不懂,哈哈,不用着急,我们一点点来分析这个公式。其实这个公式很简单,p0-pn 里面的p代表的就是控制点,t代表的是时间,这里暂且理解为时间,这个公式的意思就是,在时间为t的位置,求出多个表达式的和,这个表达式可以简写为
这个表达式主要分为4个部分
1.由n-i的常数部分
2.下标为i控制点p
3.(1-t)的n-i次方
4. t的i次方
-------------------下面这段为拷贝内容---------------------
如果直接从上面的公式上找规律比较抽象,那就从具体的例子中找规律吧:
设 Bt 为要计算的贝塞尔曲线上的坐标,N 为控制点个数,P0,P1,P2…Pn 为贝塞尔曲线控制点的坐标,当 N 值不同时有如下计算公式:
如 N 为 3 表示贝塞尔曲线的控制点有 3 个点,这时 n 为 2 ,这三个点分别用 P0,P1,P2 表示。
N = 3: P = (1-t)^2P0 + 2(1-t)tP1 + t^2*P2
N = 4: P = (1-t)^3P0 + 3(1-t)^2tP1 + 3(1-t)t^2P2 + t^3*P3
N = 5: P = (1-t)^4P0 + 4(1-t)^3tP1 + 6(1-t)^2t^2P2 + 4*(1-t)t^3P3 + t^4*P4
将贝塞尔曲线一般参数公式中的表达式用如下方式表示:
设有常数 a,b 和 c,则该表达式可统一表示为如下形式:
a * (1 - t)^b * t^c * Pn;
分析当 N 分别为3,4,5 时对应 a,b,c 的值:
如 N = 3 时,公式有三个表达式,第一个表达式为 (1-t)^2*P0,其对应 a,b,c 值分别为:1,2,0
N = 3: 1,2,0 2,1,1 1,0,2
a: 1 2 1
b: 2 1 0
c: 0 1 2
N = 4: 1,3,0 3,2,1 3,1,2 1,0,3
a: 1 3 3 1
b: 3 2 1 0
c: 0 1 2 3
N = 5: 1,4,0 4,3,1 6,2,2 4,1,3 1,0,4
a: 1 4 6 4 1
b: 4 3 2 1 0
c: 0 1 2 3 4
根据上面的分析就可以总结出 a,b,c 对应的取值规则:
b: (N - 1) 递减到 0 (b 为 1-t 的幂)
c: 0 递增到 (N - 1) (c 为 t 的幂)
a: 在 N 分别为 1,2,3,4,5 时将其值用如下形式表示:
N=1:———1
N=2:——–1 1
N=3:——1 2 1
N=4:—–1 3 3 1
N=5:—1 4 6 4 1
a 值的改变规则为: 杨辉三角
-------------------上面这段为拷贝内容---------------------
看到这里我想大家已经对贝塞尔曲线的公式有了一个大体的了解了,那么下面我们来把他翻译成ts语言
首先创建一个cocos creator3d的工程项目,把资源放进去。然后创建几个脚本,场景里面加一些节点,创建几个脚本,把遇到模型拖进去,如图所示,FishCtl脚本拖到GameCtl节点上,CtlPos到CtlPos-008是控制点的节点,这里用了9个控制点,也就是我们得到的贝塞尔曲线是9阶的,Camera的坐标的z轴位置改成100,这样我们的摄像机就可以看到鱼节点, 控制点可以随便摆放,只要在摄像机的视野范围之内就行。
然后我们来是实现FishCtl的脚本代码
export class FinshCtl extends Component {
@property({ type: Node })
public fishNode: Node = null;
@property({ type: Node })
public ctlPosNodeList: Node[] = [];
}
红字部分为新增内容,这里声明了两个变量,一个是fishNode,为了方便绑定鱼的节点,另一个是ctlPosNodeList控制点的节点列表,这里为了方便绑定控制点。代码写好之后,打开creator,点一下GameCtl节点,即可看到声明的变量,把节点依次绑定好。如图所示。
然后我们来实现BezierN脚本的代码
import { _decorator, Vec3, v3 } from ‘cc’;
const { ccclass, property } = _decorator;
export class BezierN {
public controllerPointList: Vec3[] = [];
constructor(ctlPL: Vec3[]) {
this.controllerPointList = ctlPL;
}
public getPointList(segmentNum: number): Vec3[] {
//参数为细分值,细分值越大,得到的曲线越平滑
let n = this.controllerPointList.length;
//n为当前的曲线是n阶的
//---------计算杨辉三角
let aList = [1, 1];
for (let i = 3; i < n + 1; i++) {
let tList = [];
for (let j = 0; j < aList.length; j++) {
tList.push(aList[j])
}
aList[0] = 1;
aList[i - 1] = 1;
for (let t = 0; t < tList.length - 1; t++) {
aList[t + 1] = tList[t] + tList[t + 1]
}
}
//--------计算杨辉三角
//--------计算表达式的和
let pointList: Vec3[] = [];
for (let j = 0; j < segmentNum; j++) {
let t = j / segmentNum;
//t为当前的时间,这里暂时理解为时间。
let endPos: Vec3 = v3(0, 0, 0);
for (let i = 0; i < n; i++) {
//计算 a,b,c的值
let a = aList[i];
let b = n - 1 - i;
let c = i;
//根据a,b,c的结果计算单个表达式的结果
let value = a * Math.pow((1 - t), b) * Math.pow(t, c);
//根据单个表达式的结果求所有表达式的和
endPos.add(v3(this.controllerPointList[i]).multiplyScalar(value));
}
pointList.push(endPos);
}
//--------计算表达式的和
return pointList;
}
}
是的,没有错,这就是实现贝塞尔曲线的所有代码,就是这么简单,一个构造函数,一个取出点的列表的函数,构造函数接收控制点的列表,取出点的函数,接收的参数是曲线的细分程度,比如输入100 ,那么我们将得到长度为100的坐标点的列表。
那么下面我们来继续实现鱼移动,
start() {
// Your initialization goes here.
let v3List = [];
for (let i = 0; i < this.ctlPosNodeList.length; i++) {
let node = this.ctlPosNodeList[i];
v3List.push(node.position);
}
let bezier = new BezierN(v3List);
let pathList = bezier.getPointList(30);
this.fishFly(pathList);
}
在start函数里面,我们取出控制点的坐标值,然后把他放到一个列表里面,然后new 一个我们创建的 BezierN的类,并且将控制点列表放进去,并且把曲线的点的列表取出来,这里我们的细分参数是30,也就是说我们得到的坐标点的列表长度是30,下一步我们来实现fishFly这个函数,他接收一个参数,坐标点的列表。
fishFly(pathList: Vec3[]) {
let tw = new Tween(this.fishNode);
this.fishNode.position = pathList[0];
const moveToPoint = (pos) => {
tw.to(0.2, {
position: pos
})
}
for (let i = 1; i < pathList.length; i++) {
let point = pathList[i];
moveToPoint(this.fishNode, pathList[i - 1], point);
}
tw.call(() => {
this.fishFly(pathList);
})
tw.start();
}
熟悉cocos creator3d的小伙伴看这几行代码应该没有难度。用tween来实现节点的移动,遍历坐标点列表,并且将坐标点依次加入到tween.to里面,这样节点就沿着我们取到的坐标点的坐标来移动了。
看效果鱼确实移动了,但是感觉有点奇怪,鱼头总是朝着一个方向,那么最后我们再来修饰一下,让鱼头随时指向下一个点,
fishFly(pathList: Vec3[]) {
let tw = new Tween(this.fishNode);
this.fishNode.position = pathList[0];
const moveToPoint = (node: Node, beforPos, pos) => {
let dir = v3(beforPos).subtract(pos).normalize();
let quat = new Quat();
Quat.fromViewUp(quat, dir, Vec3.UP);
tw.to(0.2, {
position: pos,
worldRotation: quat
})
}
for (let i = 1; i < pathList.length; i++) {
let point = pathList[i];
moveToPoint(this.fishNode, pathList[i - 1], point);
}
tw.call(() => {
this.fishFly(pathList);
})
tw.start();
}
这里我们用到了creator提供的api 四元数,目前大多数3d游戏引擎都提供了四元数功能,具体细节我就不在这里赘述了,有兴趣的小伙伴可以去自行百度。
鱼虽然可以旋转了,但是好像角度不太对,很简单只需要把模型的y轴旋转90度即可。
最终效果
是不是很完美,终于写完了。
这里需要感谢csdn上写那篇文章的大佬,虽然不认识。也要感谢cocos引擎组,在各种成熟的优秀的引擎层出不穷的时候,仍然顶着压力来造轮子,为我们的国产之光添砖加瓦。现在cocos creator已经1.1.2版本了,虽然仍然有一些小问题,但是已经完全可以作为生产工具来造游戏了。
感谢支持小游戏,


欢迎关注订阅号 发现更多有趣内容。
