cocos creator3d 三维多阶贝塞尔曲线的实现,可以做3d捕鱼了

转自订阅号

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

我们是如何让鱼自由自在的游动的呢,这里就用到了一个古老的技术,贝塞尔曲线,为什么说古老呢,因为这个技术不是我发明的而且存在了很长时间了,根据百度到的知识(“贝赛尔曲线”是由法国数学家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版本了,虽然仍然有一些小问题,但是已经完全可以作为生产工具来造游戏了。

感谢支持小游戏,


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

5赞

mark!!!

1赞

话说这个 ctlPosNodeList 怎么传递数值,应该定义怎么样子的 控制点?