THREEJS——高科技感的魔法阵

我正在参加 #金石计划征文活动# ,希望各位点个赞,谢谢

效果图

绘制静态图

在threejs的场景中使用lineLine2绘制出外围由线段组成的部分,

外围虚线圆

下面的代码是其中一条圆形虚线的代码,其中材质用到的是LineMaterial,属性包含color颜色、linewidth 宽度、dashed是否虚线、dashSize虚线尺寸、gapSize间隔尺寸、opacity透明度、transparent是否支持透明。

ts 复制代码
const createRoundDashed1 = () => {
    const geometry = new LineGeometry();
    geometry.setPositions(getRoundPoints(50));

    const matLine = new LineMaterial({
        color: lineColor,
        linewidth: 20,
        dashed: true,
        dashSize: 360 / 16,
        gapSize: 360 / 16,
        opacity: 0.1,
        transparent: true
    });
    lineMaterials.push(matLine)

    roundDashed1 = new Line2(geometry, matLine);

    roundDashed1.computeLineDistances();
    roundDashed1.scale.set(1, 1, 1);
    scene.add(roundDashed1)
}

计算圆形的点位

getRoundPoints方法是通过一个半径值生成一个圆弧并得到圆弧上的所有点位,供给LineGeometry使用,作为position属性。内部通过三角函数中Math.sin正弦和Math.cos余弦计算得出。在之后所有的圆形绘制都使用这个方法获取圆

ts 复制代码
const getRoundPoints = (R: number, N = 361): number[] => {
    // 批量生成圆弧上的顶点数据
    const vertices: number[] = []
    for (var i = -N / 2; i < N / 2; i++) {
        var angle = 2 * Math.PI / N * i;
        var x = R * Math.sin(angle);
        var y = R * Math.cos(angle);
        vertices.push(x, y, 0)
    }
    return vertices
}

上面的虚线作为将来动图的第一组,也就是z坐标最接近0的位置,所以将上面创建的3个元素变成一个组,并在组的usedata里设置一个layer属性为1

围绕式字母

字母这里相对比较复杂一些,用到FontLoader加载文字和InstancedFlow流的实例化,InstancedFlow主要作用就是将多个模型通过重复的方式展现,其提供的updateCurve接受一个曲线路径,那么重复展现的物体将以这个曲线路径去排列,咱们这里使用到的就是加载一段文字并创建TextGeometry,字体用的是threejs提供的字体json文件。

ts 复制代码
const loader = new FontLoader();
    loader.load('../node_modules/three/examples/fonts/droid/droid_sans_mono_regular.typeface.json', function (font) {

        const letterSize = 2, offset = 1.5, letterRoundD = 28
        const letterGeometry = new TextGeometry(' BLACKCATI XY ', {
            font: font,
            size: letterSize,
            height: 0.05,
            bevelThickness: 0.02,
            bevelSize: 0.01,
        });

        letterGeometry.rotateX(Math.PI * 0.5);

        const material = new THREE.MeshStandardMaterial({
            color: lineColor
        });

        const numberOfInstances = 8;

        let flow = new InstancedFlow(numberOfInstances, 1, letterGeometry, material);
        const points: THREE.Vector3[] = []
        const n = getRoundPoints(letterRoundD)
        for (let i = 0; i < n.length / 3; i++) {
            points.push(new THREE.Vector3().fromArray(n, i * 3))
        }
        const curve = new THREE.CatmullRomCurve3(points, true, 'centripetal');

        flow.updateCurve(0, curve);
        letterGroup.add(flow.object3D);

        flow.setCurve(0, 0);

        for (let i = 0; i < numberOfInstances; i++) {

            const curveIndex = i % 1;
            flow.setCurve(i, curveIndex);
            flow.moveIndividualAlongCurve(i, i * 1 / numberOfInstances);
            // flow.object3D.setColorAt( i, new THREE.Color( 0xffffff * Math.random() ) );

        }

        const createRound = (d: number) => {
            const roundGeometry1 = new LineGeometry();
            roundGeometry1.setPositions(getRoundPoints(d));

            const matLine = new LineMaterial({
                color: lineColor,
                linewidth: 4,
            });
            lineMaterials.push(matLine)

            let round1 = new Line2(roundGeometry1, matLine);

            round1.computeLineDistances();
            round1.scale.set(1, 1, 1);
            letterGroup.add(round1)
        }

        createRound(letterRoundD - offset)
        createRound(letterRoundD + letterSize + offset)

效果如下:

后面的矩形部分这里就不展示代码了,和前面绘制虚线的方法一样。

三角形的点位坐标也是通过getRoundPoints方法得到的,由三个点位组成的,第二个参数传3,意味着取得组成这个半径的圆的三个点,但是得到的图形并不是闭合的

那么就需要第四个点,第四个点和第一个点是相同的,所以将getRoundPoints方法改造一下

ts 复制代码
const getRoundPoints = (R: number, N = 361): number[] => {
    // 批量生成圆弧上的顶点数据
    const vertices: number[] = []
    const start: number[] = []
    for (var i = -N / 2; i < N / 2; i++) {
        var angle = 2 * Math.PI / N * i;
        var x = R * Math.sin(angle);
        var y = R * Math.cos(angle);
        if (i === -N / 2) {
            // 将第一个点存下
            start.push(x, y, 0)
        }
        vertices.push(x, y, 0)
    }
    // 将第一个点放在点位数组的最后一位
    return [...vertices, ...start]
}

获取到三角形的定点信息后,利用创建圆形的方法创建由三个点位信息组成的Line2

logo用的是上一篇文章中所用到的logo,具体的点位信息获取这里就不赘述,感兴趣的同学可以看一下# THREE.JS------让你的logo切割出高级感。同样在获取点位信息后,创建Line2

至此所有静态图形都绘制完成了,接下来要制作动效。

动效

图形结构

目前图形分为6层,每一层都单独制作效果,并根据层级的不同,改变每个组的z值,从而实现立体的效果

ts 复制代码
// 最底层虚线的组
roundGroup.userData.layer = 1
scene.add(roundGroup)
// 上一层矩形的组
rectGroup.userData.layer = 2
scene.add(rectGroup)
// 字母和字母周围圆圈的组
letterGroup.userData.layer = 3
scene.add(letterGroup)
// 三组相同三角形的组
triangleGroup1.userData.layer = 4
scene.add(triangleGroup1)
// 独立三角形的组
triangleGroup2.userData.layer = 5
scene.add(triangleGroup2)
// logo 的组
girlGroup.userData.layer = 6
scene.add(girlGroup)

动效

tween动画

动效的实现方法使用的是tween,通过缩放各个元素的比例来实现显示的过程,在创建好元素后,将动画绑定到一个动画合集中,在所有元素加载完以后,调用tween的start方法来执行动画,以某个圆圈为例,比例从0放大到1的代码

ts 复制代码
const createRoundDashed1 = () => {
   ...
    roundDashed1 = new Line2(geometry, matLine);
    roundDashed1.computeLineDistances();
    roundDashed1.scale.set(0, 0, 0);
    roundDashed1.name = 'roundDashed1'
    animatList.push(new TWEEN.Tween(roundDashed1.scale)
        .delay(300)
        .to(new THREE.Vector3(1, 1, 1), 1000)
        .onUpdate((value, a) => {
            if (roundDashed1) {
                roundDashed1.scale.copy(value)
            }
        }))
    roundGroup.add(roundDashed1)
}

这个动画延迟300毫秒,并在1秒内执行完由new THREE.Vector3(0,0,0)new THREE.Vector3(1, 1, 1)之间的变化,再将这个动画收集到animatList中,我在画面中添加了一个按钮,在点击按钮的时候,循环遍历animatList的所有动画,并调用动画的start方法。

ts 复制代码
const btn = document.querySelector('button')
if (btn) {
    btn.onclick = () => {
        console.log('gres');

        animatList.forEach((animate: any) => {
            animate.start()
        })
    }
}

这里tween就举着一个例子,其他基本都是一样的,下面代码是元素的旋转,还是以roundDashed1为例,在render中修改他的routation.z

ts 复制代码
if (roundDashed1) {
        // 速度 * 旋转范围
    roundDashed1.rotation.z = Math.cos(Date.now() * 0.0005) * 0.5
}

以以上方法继续将其他的元素做好动画,控制好延迟执行和执行持续的时间,就会出现有层次的动画效果

除了缩放动画的部分,其他部分是根据路径更新来显示的

路径增加动画

之前我们做的三角形只有四个顶点(起点两个)。那么在做路径增长动画显然不是很合适,所以需要将newTriangle方法改一下,使用LineCurve3将每两个点之间添加一些顶点,pointCounts这个就是原有顶点的数量,将第i个和第i+1个顶点作为线段的两段,通过getPoints

ts 复制代码
const linePoints: THREE.Vector3[] = []
    let pointArr:number[] = []
    for (let i = 0; i < pointCounts; i++) {
        const line = new THREE.LineCurve3(new THREE.Vector3().fromArray(points, i * 3), new THREE.Vector3().fromArray(points, (i + 1) * 3))
        const ps = line.getPoints(10)
        ps.forEach((v3: THREE.Vector3) => {
            pointArr.push(...v3.toArray())
            linePoints.push(v3)
        })
    }

这样就获得到每条线段11个点,每个三角形一共33个点位

有了这些点位就可以通过tweenjs继续做补间动画,对于LineGeometry的更新,一般都是使用geometry.setPositions(),不过有一点问题,就是顶点缓冲区数量要相同,不然会报错

[.WebGL-0x100016da900] GL_INVALID_OPERATION: Vertex buffer is not big enough for the draw call

那么比如一个三角形有33个点,在补间动画的过程中也要保持有33个点,但是又不能让这些点显示出来,就需要一个方法,将补间动画未经过的点设置为最后一个点的坐标,这样既能保证动画的完整性,也可以保证不报错

ts 复制代码
/**
 * 
 * @param arr 所有点位信息
 * @param r 当前经过的百分比 0-1浮点数
 * @returns 修正过的点位信息
 */
const filterPoint = (arr: THREE.Vector3[], r: number): Array<number> => {
    let l = Math.ceil(arr.length * r)
    if (l > arr.length) l -= arr.length
    let v3: number[] = []
    arr.forEach((n, i) => {
        if (i <= l) v3.push(...n.toArray())
        else v3.push(...arr[l].toArray())
    })
    return v3
}

具体的使用方法,就是用0-1做过渡,代表当前路径经过的进度。

ts 复制代码
animatList.push(new TWEEN.Tween({ index: 0 })
    .delay(delay)
    .to({ index: 1 }, 1000)
    .onUpdate(({ index }) => {
        triangle.geometry.setPositions(filterPoint(linePoints, index))
    })
)

路径动画效果

后面再用同样的方法把前面的图标也做成路径动画即可,将所有元素和动画都展示出来,大概就是这个效果,接下来要做的就是旋转并进行立体化

立体化

记得之前给每个元素都编组了吧而且每个组都加了layer属性,那么就可以在动画中根据每个组的layer修改对应的z轴的值,并且将摄像头沿着x轴向右移动

ts 复制代码
 const scale = 0.65
        animatList.push(new TWEEN.Tween({
            cameraPosition: camera.position,
            layerZIndex: 0
        })
            .delay(1000)
            .to({
                cameraPosition: new THREE.Vector3(90, 0, 140),
                layerZIndex: 3.5
            }, 2000)
            .onUpdate(({ cameraPosition, layerZIndex }) => {
                scene.traverse((child: any) => {
                    if(child.name === 'flow') {
                        if(child) {
                            child.scale.set(scale,scale,scale)
                        }
                    }
                    if (child.userData.layer && child.isGroup) {
                        const layer = child.userData.layer;
                        console.log(layer);
                        child.position.z = layer * layerZIndex - layerZIndex * 0.5
                    }
                })
            })
        )

gif图比较大,可以下载下来后查看

代码地址

历史文章

# THREE.JS------让你的logo切割出高级感

# threejs------多重场景渲染

相关推荐
everyStudy22 分钟前
JS中判断字符串中是否包含指定字符
开发语言·前端·javascript
城南云小白23 分钟前
web基础+http协议+httpd详细配置
前端·网络协议·http
前端小趴菜、23 分钟前
Web Worker 简单使用
前端
web_learning_32126 分钟前
信息收集常用指令
前端·搜索引擎
tabzzz33 分钟前
Webpack 概念速通:从入门到掌握构建工具的精髓
前端·webpack
200不是二百1 小时前
Vuex详解
前端·javascript·vue.js
滔滔不绝tao1 小时前
自动化测试常用函数
前端·css·html5
码爸1 小时前
flink doris批量sink
java·前端·flink
深情废杨杨1 小时前
前端vue-父传子
前端·javascript·vue.js
J不A秃V头A2 小时前
Vue3:编写一个插件(进阶)
前端·vue.js