threejs基础-从零创建一个运动的立方体

3,935 阅读12分钟

官网:threejs

一、搭建项目目录:

(1)新建一个文件夹,在文件夹下,初始化一个package.json文件,使用命令行npm init -y生成package.json。

(2)创建目录:结构如下。
image.png

(3) 在html中引入样式和入口文件。 image.png

(4)安装一个打包工具,这里使用parcel(这是一个零配置的构建工具),安装命令:npm install --save-dev parcel
(5)在package.json里添加配置:

"source": "src/index.html",
"scripts": {
"start": "parcel",
"build": "parcel build"
}

(6)安装threejs: npm install three --save
(7) 添加点样式启动看下效果。npm start

二、认识Threejs-创建一个立方体

在入口文件(我这里是index.js)中引入three.js。import * as THREE from 'three',打印下THREE看是否已经正确引入。

image.png

ok,没有问题,进入正题。
为了真正能够让你的场景借助three.js来进行显示,我们需要以下几个对象:场景、相机和渲染器,这样我们就能透过摄像机渲染出场景。

1、场景

来显示你的3d场景的地方 创建场景:const scene = new THREE.Scene();

2、相机

相当于人的眼睛,模拟人眼所看到的景象。官网中相机有很多种,这里先使用最简单的一种,透视相机 PerspectiveCamera 创建相机并添加到场景中:

// 创建相机
const camera = new THREE.PerspectiveCamera(75,window.innerWidth/window.innerHeight,0.1,1000)
// 设置相机位置
camera.position.set(0,0,10)
// 将相机添加到场景中
sence.add(camera);

参数:
fov — 摄像机视锥体垂直视野角度
aspect — 摄像机视锥体长宽比
near — 摄像机视锥体近端面
far — 摄像机视锥体远端面
看下这个官网示例来理解这些参数

image.png

3、物体

物体由几何体和材质组成,所以我们需要创建一个几何体和材质,并将几何体和材质合并为一个物体添加到场景中,物体才能渲染出来。这里拿最简单的立方体举例

// 创建几何体:立方缓冲几何体(BoxGeometry)
const geometry = new THREE.BoxGeometry( 1, 1, 1 );
// 创建物体材质
const material = new THREE.MeshBasicMaterial( {color: 0x00ff00} );
// 几何体和材质组成一个物体
const cube = new THREE.Mesh( geometry, material );
// 将物体添加到场景
sence.add(cube)

到这里打开浏览器看一下,依然什么都没有,因为还需要一个渲染器将场景渲染出来。

4、渲染器

初始化渲染器new THREE.WebGL1Renderer(),打印下这个渲染器可以看到有一个domElement,是一个canvas,将其添加到body中

image.png

const renderer = new THREE.WebGL1Renderer();
//设置渲染器尺寸
renderer.setSize(window.innerWidth,window.innerHeight)
// 将webGL渲染的canvas内容添加到body(将渲染器添加到body)
document.body.appendChild(renderer.domElement)
// 使用渲染器将场景通过相机渲染出来
renderer.render(sence,camera)

打开浏览器看下效果,有东西了,但是好像是个平面。我们需要一个控制器,使相机围绕目标进行运动才能看到立体效果。

image.png

5、控制器

控制器也有很多种,根据需求选择,这里使用轨道控制器(OrbitControls)
控制器需要单独引入:import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
创建轨道控制器。这里还创建了一个渲染函数,使得浏览器的帧发生变化时重新渲染。

// 创建控制器
const controls = new OrbitControls( camera, renderer.domElement );
// 创建渲染函数
function render() {
    renderer.render(sence,camera)
    requestAnimationFrame(render)
}
// 初始化时执行下渲染函数
render()

再去看下效果。鼠标拖动物体,发现可以动了。

box.gif

这样看着是不有点费劲,我们来添加个辅助坐标系来让它的位置更清晰明了。

6、辅助坐标系

const axesHelper = new THREE.AxesHelper( 5 );
// 添加到场景
sence.add( axesHelper );

效果:

image.png

三、让物体动起来

物体有一个position属性,是一个三维向量,有x、y、z三个方向。通过改变它的位置来使物体运动。 有两种方式:(1)可以通过直接改变x,y,z改变位置:,cube.position.x=3(2)或者通过set函数:cube.position.set(x,y,x)。
试一下,在render函数里面添加运动效果

cube.position.x += 0.1;
    cube.rotation.x += 0.01;
    if(cube.position.x > 5) {
        cube.position.x = 0;
        cube.rotation.x = 0;
    }

看下效果:

move.gif
一些概念:
FPS(Frames Per Second,每秒帧数):指一秒钟渲染的图像数量。例如一个画面由连续的图像组成,刷新率为60Hz,就表示每秒钟该画面要被更新60次。前面渲染函数中使用的requestAnimationFrame()函数可以让我们更好地掌控页面的帧数。这个函数会根据浏览器判断最佳的更新时间,并将回调函数放入主线程队列中,保证了动画的平滑性和有效性。
浏览器中的每一帧(frame)所用的时间并不是一定相同的,它取决于两个因素:硬件性能和图形复杂度。 当浏览器处理较为简单的图形场景时,每一帧所用时间可能只有几毫秒;但在复杂的图形场景中,每一帧所用时间可能需要几十甚至上百毫秒。
综上所述:通过每次增加一个固定的值来更新位置和旋转角度,使得物体在前一段时间内运动的速度可能与后一段时间内的速度不同,导致运动不是匀速的。
如果要实现匀速运动,可以从物理学的角度出发,将“速度”这个概念引入进来。
改写下上述代码,实现物体匀速运动:

// 速度:
let speed = 1;
function render(time) {   
    // 让物体在0-5之间运动
    let t = (time / 1000) % 5;
    // 位移 = 时间 * 速度
    cube.position.x = t * speed;
}

在Three.js里,提供了一个计时工具类(Clock),用于精确测量从某个特定时间点(比如场景创建时间)开始经过的时间,并提供给动画循环函数等使用。
我们用Clock来获取时间让物体匀速移动:

// 初始化一个时钟
const clock = new THREE.Clock();
function render() {   
    // 获取每两帧之间的时间间隔
    const clockTime = clock.getElapsedTime() 
    // 移动时间 让物体在0-5之间运动
    let t = clockTime  % 5;
    // 时间 * 速度
    cube.position.x = t * speed;
}

一些问题: 1、转动物体的时候没有阻力,给轨道控制器添加阻尼,模拟更真实的物理场景

// 设置轨道控制器的阻尼
controls.enableDamping = true;
//同时render函数里要调用updata函数阻尼才会生效
controls.update()

2、改变窗口大小时,物体的大小不会跟着变化,会出现这样的情况。

image.png 我们可以监听浏览器的resize事件,在浏览器窗口尺寸变化时重新渲染下物体、相机、渲染器的大小,代码如下:

window.addEventListener('resize',() => {
    // 更新相机宽高比
    camera.aspect = window.innerWidth / window.innerHeight;
    // 修改相机矩阵(就是摄像机的视野以及渲染画面的范围)
    camera.updateProjectionMatrix()
    // 更新渲染器
    renderer.setSize(window.innerWidth, window.innerHeight);
    // 设置渲染器的像素比
    renderer.setPixelRatio(window.devicePixelRatio)
})

再来看下效果:

resize.gif 注:window.devicePixelRatio是一个浏览器提供的属性,用于描述浏览器物理像素和设备独立像素之间的比例关系。设备独立像素指的是在CSS中使用的像素,而物理像素则是真实待显示的像素。这个比例通常值为1、1.5或2。

四、和一些库结合使用

resize.gif

1、GSAP动画库

是一款强大且易于使用的JavaScript动画库,它能帮助开发者创建高性能、高效和兼容性极佳的Web动画。基本能够实现网页所需要的所有动画,如果你的网页需要炫酷动画,可以学一下这个库。基本使用。下面看下怎么在three.js中使用。
安装:npm install gsap
引入:import { gsap } from "gsap";
创建一个补间动画:

gsap.to(cube.position.x,{
    // 移动距离
    x:5,
    // 时间
    duration: 3,
    // 重复运动
    repeat: -1,
    // 往返运动
    yoyo: true,
    // 速率曲线
    ease: "power2.out",
    // 动画开始回调
    onStart:() => {
        console.log('动画开始了!')
    },
    // 动画完成回调
    onComplete:() => {
        console.log('动画完成')
    }
})

还可以给它添加一个双击播放或暂停事件:

window.addEventListener('dblclick',() => {
    // tween是补间动画 isActive是补间动画的方法
    if(tween.isActive()) {
        // 暂停
        tween.pause()
    } else {
        // 继续
        tween.resume()
    }
})

2、dat.gui:图形化界面

这个插件是用来在开发阶段控制物体,方便开发;

安装:npm install --save dat.gui
引入: import * as dat from 'dat.gui'
创建dat.gui:const gui = new dat.GUI();
使用:具体可以设置哪些属性可以看下官网

//向图形化界面添加一个移动属性
gui.add(cube.position,"x")
//移动最小值
.min(0)
//移动的最大值
.max(5)
//名称
.name('移动X轴')
//移动步长
.step(0.1)
.onChange((val)=>{
    // console.log(val);
}).onFinishChange((val)=>{

});
//修改颜色
const params = {
    color:'#ffff00',
}
//将选取的颜色,复制到材料上
gui.addColor(params,"color").onChange((value)=>{
    cube.material.color.set(value);
});
//添加是否显示到图形化界面
gui.add(cube,'visible').name('是否显示');

//图形化界面添加一个文件夹
const folder = gui.addFolder('设置立方体');
folder.add(cube.material,'wireframe');

看下效果:可以通过调节gui来查看物体效果,方便开发。

image.png

以下是完整代码:

import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
import { gsap } from 'gsap';
import * as dat from 'dat.gui';
const gui = new dat.GUI();
// 创建场景
const sence =  new THREE.Scene()
// 创建相机
const camera = new THREE.PerspectiveCamera(75,window.innerWidth/window.innerHeight,0.1,1000)
// 设置相机位置
camera.position.set(0,0,10)
// 将相机添加到场景中
sence.add(camera);

// 创建几何体
const geometry = new THREE.BoxGeometry( 1, 1, 1 );
// 创建物体材质
const material = new THREE.MeshBasicMaterial( {color: 0x00ff00} );
// 几何体和材质组成一个物体
const cube = new THREE.Mesh( geometry, material );
// 将物体添加到场景
sence.add(cube)


// 初始化一个渲染器
const renderer = new THREE.WebGL1Renderer();
//设置渲染器尺寸
renderer.setSize(window.innerWidth,window.innerHeight)
// 将webGL渲染的canvas内容添加到body(将渲染器添加到body)
document.body.appendChild(renderer.domElement)
// 使用渲染器将场景通过相机渲染出来
renderer.render(sence,camera)


// 创建控制器
const controls = new OrbitControls( camera, renderer.domElement );

// 设置轨道控制器的阻尼
controls.enableDamping = true;

// 创建一个补间动画
const tween = gsap.to(cube.position,{
    // 移动距离
    x:5,
    // 时间
    duration: 3,
    // 重复运动
    repeat: -1,
    // 往返运动
    yoyo:true,
    // 速率曲线
    ease: "power2.out",
    // 动画开始回调
    onStart:() => {
        console.log('动画开始了!')
    },
    // 动画完成回调
    onComplete:() => {
        console.log('动画完成')
    }
})


// 创建渲染函数
// 速度:
let speed = 1;
// 初始化一个时钟
const clock = new THREE.Clock();
function render(time) {   
    controls.update()
    // // 获取每两帧之间的时间间隔
    // const clockTime = clock.getElapsedTime() 
    // // 移动时间
    // let t = clockTime  % 5;
    // // 时间 * 速度
    // cube.position.x = t * speed;
    renderer.render(sence,camera)
    requestAnimationFrame(render)
}


// 创建辅助坐标系
const axesHelper = new THREE.AxesHelper( 5 );
// 添加到场景
sence.add( axesHelper );

window.addEventListener('resize',() => {
    // 更新相机宽高比
    camera.aspect = window.innerWidth / window.innerHeight;
    // 修改相机矩阵(就是摄像机的视野以及渲染画面的范围)
    camera.updateProjectionMatrix()
    // 更新渲染器
    renderer.setSize(window.innerWidth, window.innerHeight);
    // 设置渲染器的像素比
    renderer.setPixelRatio(window.devicePixelRatio)
})

window.addEventListener('dblclick',() => {
    // tween是补间动画 isActive是补间动画的方法
    if(tween.isActive()) {
        // 暂停
        tween.pause()
    } else {
        // 继续
        tween.resume()
    }
})

//向图形化界面添加一个移动属性
gui.add(cube.position,"x")
//移动最小值
.min(0)
//移动的最大值
.max(5)
//名称
.name('移动X轴')
//移动步长
.step(0.1)
.onChange((val)=>{
    // console.log(val);
}).onFinishChange((val)=>{

});
//修改颜色
const params = {
    color:'#ffff00',
}
//将选取的颜色,复制到材料上
gui.addColor(params,"color").onChange((value)=>{
    cube.material.color.set(value);
});
//添加是否显示到图形化界面
gui.add(cube,'visible').name('是否显示');

//图形化界面添加一个文件夹
const folder = gui.addFolder('设置立方体');
folder.add(cube.material,'wireframe');
// 初始化时执行下渲染函数
render()

五、BufferGeometry(几何缓冲体)

它是一个高效的数据结构,因为它使用了一系列的数组来存储几何体的信息。 是面片、线或点几何体的有效表述。包括顶点位置,面片索引、法相量、颜色值、UV 坐标和自定义缓存属性值。使用 BufferGeometry 可以有效减少向 GPU 传输上述数据所需的开销。BufferGeometry是一种基于缓冲区的顶点数据管理方式。
基于缓冲区的顶点数据管理方式是指将交互式3D图形渲染需要的大量数据存储在内存中的一种方法。
缓冲区是指一块预分配好的连续内存空间,可以用来存储大量相同类型的数据。
BufferAttribute这个类用于存储与BufferGeometry相关联的 attribute(例如顶点位置向量,面片索引,法向量,颜色值,UV坐标以及任何自定义 attribute )以下是三个很重要的attribute
normal(法向量):法线是一个垂直于面的向量,它被用来计算光照,阴影等效果。
position:顶点坐标。
uv:纹理坐标。可以把UV坐标理解为一个三维物体在二维平面上展开后的坐标。如正方体展开可以是这样的:

image.png

创建一个三角形

// 定义一个几何缓冲体
const geometry  = new THREE.BufferGeometry();
// 定义一个三角行数组 
const vertices  = new Float32Array([
    1,1,1,
    -1,1,1,
    -1,0,1
]);
// 设置顶点 itemSize = 3 因为每个顶点都是一个三元组。
geometry.setAttribute( 'position', new THREE.BufferAttribute( vertices, 3 ) );
//创建材料
const material = new THREE.MeshBasicMaterial({color: 0x00ff00})
// 将集合体和材料组合成物体
const triangle = new THREE.Mesh(geometry,material);
// 添加到场景
scene.add(triangle)

效果:

image.png

使用-创建多个三角形

for(let i=0; i<40; i++) {
    // 定义一个几何缓冲体
    const geometry = new THREE.BufferGeometry();
    //定义一个三角形数组
    const triangleArray = new Float32Array(9);
    // 随机生成三角形顶点坐标
    for(let i=0;i<9;i++) {
        triangleArray[i] = Math.random() * 10 -5
    }
    geometry.setAttribute('position',new THREE.BufferAttribute(triangleArray,3))
    // 设置随机颜色
    let color = new THREE.Color(Math.random(),Math.random(),Math.random())
    // 创建材料
    const material = new THREE.MeshBasicMaterial({
        color: color,
        transparent: true,
        opacity: .3
    })
    // 几何体和材料组合成物体
    const triangle = new THREE.Mesh(geometry,material)
    // 物体添加到场景
    scene.add(triangle)
}

效果:

image.png

完整代码:

import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
import * as dat from 'dat.gui';
const gui = new dat.GUI();

// 创建场景
const scene = new THREE.Scene();
//创建相机
const camera = new THREE.PerspectiveCamera(
    75,
    window.innerWidth/window.innerHeight,
    0.1,
    1000
);
//设置相机位置
camera.position.set(0,0,10);
//将相机添加到场景
scene.add(camera);

//初始化一个渲染器
const renderer = new THREE.WebGLRenderer();
// 设置渲染器的宽高
renderer.setSize(window.innerWidth,window.innerHeight);
//将webgl渲染的canvas内容添加到body中
document.body.appendChild(renderer.domElement);

// 定义一个几何缓冲体
// const geometry  = new THREE.BufferGeometry();
// // 定义一个三角行数组 
// const vertices  = new Float32Array([
//     1,1,1,
//     -1,1,1,
//     -1,0,1
// ]);
// // 设置顶点 itemSize = 3 因为每个顶点都是一个三元组。
// geometry.setAttribute( 'position', new THREE.BufferAttribute( vertices, 3 ) );
// //创建材料
// const material = new THREE.MeshBasicMaterial({color: 0x00ff00})
// // 将集合体和材料组合成物体
// const triangle = new THREE.Mesh(geometry,material);
// // 添加到场景
// scene.add(triangle)

for(let i=0; i<40; i++) {
    // 定义一个几何缓冲体
    const geometry = new THREE.BufferGeometry();
    //定义一个三角形数组
    const triangleArray = new Float32Array(9);
    // 随机生成三角形顶点坐标
    for(let i=0;i<9;i++) {
        triangleArray[i] = Math.random() * 10 -5
    }
    geometry.setAttribute('position',new THREE.BufferAttribute(triangleArray,3))
    // 设置随机颜色
    let color = new THREE.Color(Math.random(),Math.random(),Math.random())
    // 创建材料
    const material = new THREE.MeshBasicMaterial({
        color: color,
        transparent: true,
        opacity: .3
    })
    // 几何体和材料组合成物体
    const triangle = new THREE.Mesh(geometry,material)
    // 物体添加到场景
    scene.add(triangle)
}

// 创建一个轨道控制器
const controls = new OrbitControls(camera,renderer.domElement);
//设置轨道控制器的阻尼感
controls.enableDamping = true

//创建一个渲染函数
function render(time){
    renderer.render(scene,camera);
    requestAnimationFrame(render);
}

//定义一个坐标辅助器
const axesHelp = new THREE.AxesHelper(5);

//将坐标辅助器添加到场景当中
scene.add(axesHelp);

//起始渲染一下
render();

六、结语

加油。