探索three.js
github源碼
build文件夾 库的核心
examples 包含示例源代码、字体、模型、音效
如何在你的项目中引入 three.js
npm install --save three
导入类
import {
Camera,
Material,
Texture,
} from 'three';
导入插件
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
基本组件
scene 世界空间
相机 指向小宇宙的望远镜
渲染器
以上三个都不可见
网格 Mesh
Mesh构造函数有两个参数:几何和材质
创建模型的六大步骤
-
初始设置
创建网页来放场景
-
创建场景
-
创建相机
-
创建可见对象
-
创建渲染器
-
渲染场景
基于物理的渲染和照明
three.js 中的大小单位是米
Three.js 中的光照:
-
直接照明
-
DirectionalLight => 阳光
MeshBasicMaterial是最基本的材料 不会对灯光做出反应,整个mesh都是单一颜色着色 可以用环境贴图来改善外观。环境贴图是基于图像照明的一种形式`MeshStandardMaterial可以使用真实世界的物理方程对光做出反应 -
PointLight => 灯泡
-
RectAreaLight => 条形照明或明亮的窗户
-
SpotLight => 聚光灯
默认情况下禁用阴影
-
-
环境光
变换和坐标系
场景图中的每个对象(顶级场景除外)只有一个父对象,并且可以有任意数量的子对象。
世界坐标系
Scene定义了世界空间
直接将一个对象加到场景中,然后对它平移、旋转或缩放,该对象将相对于世界空间移动,即相对于场景的中心
局部坐标系
Scene的子对象都定义了自己的局部空间
每个对象都有一个局部坐标系 在创建mesh的时候 我们创建了一个新的局部坐标系,mesh位于其中心
每当我们变换一个对象时,我们都是相对于它的父坐标系进行的。
每个对象最开始的位置都是相对于它的父对象的中心(0,0,0)。
坐标系的正方向:x :右侧 y :上方 z :朝向我们的方向
位置被存储在Vector3类中
Vector3是表示3D向量的类 这个类有.x .y .z 属性和 .set方法
创建任何场景对象,Vector3都会被自动创建并存储在场景对象的 .position属性中
缩放
缩放相对于对象的初始大小
统一缩放:x y z轴 缩放的比例一样
mesh.scale.set(2, 2, 2);
非均匀缩放:每个轴上的缩放值不同 会压缩或拉长物体
负值会镜像对象
// mirror the mesh across the X-axis
mesh.scale.x = -1;
将对象的大小放大一倍并在Y轴镜像,使用缩放值(2,−2,2):
mesh.scale.set(2, -2, 2);
相机和灯光无法缩放
旋轉
three.js中旋转是使用弧度而不是 度数
转换角度为弧度
import { MathUtils } from 'three';
const rads = MathUtils.degToRad(90); // 1.57079... = π/2
局部矩阵是对象相对于父对象的位置
世界矩阵是对象相对于场景(世界空间)的位置
世界矩阵存储对象在世界空间中的位置。如果对象是场景的直接子对象,这两个矩阵将是相同的,但如果对象位于场景图更深的某个位置(对象里的对象),则局部矩阵和世界矩阵很可能是不同的。
使我们的场景具有响应性(以及处理锯齿)
监听浏览器的resize事件 缩放窗口就重新设置renderer的大小,重新渲染
const setSize = (container, camera, renderer) => {
camera.aspect = container.clientWidth / container.clientHeight;
camera.updateProjectionMatrix();
renderer.setSize(container.clientWidth, container.clientHeight);
renderer.setPixelRatio(window.devicePixelRatio);
};
class Resizer {
constructor(container, camera, renderer) {
// 初次调用时调整大小
setSize(container, camera, renderer);
// 浏览器窗口大小改变时调整大小
window.addEventListener("resize", () => {
setSize(container, camera, renderer);
//改变了窗口大小后,相机、渲染器和canvas都已经是正确大小,
// 但是只调用了一次render,需要重新渲染
// 不想传递World 定义空方法 在World中生成该类的实例,再赋给新的onResize方法,调用World的render方法
this.onResize();
});
}
onResize() {}
}
export { Resizer };
World.js
const resizer = new Resizer(container, camera, renderer);
resizer.onResize = () => {
this.render();
};
动画循环
固定帧和动态帧
电影中的帧速率是 固定 的。但是动画循环不会以固定速率生成帧
动画的旋转速度会根据不同的设备而不一样。在新的设备中,它会旋转很快。在旧的慢速设备中,会转得慢
根据前一帧花费的时间来缩放移动的大小
const clock = new Clock();
const delta = clock.getDelta(); //将告诉我们前一帧花了多长时间
cube.js
// 将度数转为弧度
const radiansPerSecond = MathUtils.degToRad(30);
cube.tick = (delta) => {
// 围绕每个轴每秒旋转30度
// 无论在哪种设备 动画速度都一样
cube.rotation.z += radiansPerSecond * delta;
cube.rotation.x += radiansPerSecond * delta;
cube.rotation.y += radiansPerSecond * delta;
};
纹理映射
UV映射:一种获取二维纹理并将其映射到三维几何体的方法。 允许我们在几何体上的点和纹理的点之间创建连接。
因为我们已经使用了字母X、Y和Z作为我们的3D坐标系,我们将使用字母U和V来指代2D纹理坐标系. 这就是UV映射名称的由来。
(u,v)⟶(x,y,z)
(u,v)表示纹理上的一个点,而(x,y,z)表示几何上的一个点
几何体上的一个点称为顶点vertex
一旦我们有了一个带有UV映射的几何体,我们就可以获取任何纹理并将其应用于几何体,它会立即起作用。
Texture
包裹img标签,具有一些与用作纹理而不是普通图像相关的额外设置
贴图步骤
- 引入TextureLoader
- 创建TextureLoader实例
- 使用TextureLoader.load加载纹理
// 创建TextureLoader实例
const textureLoader = new TextureLoader();
// 使用TextureLoader.load加载纹理
const texture = textureLoader.load(
'/assets/textures/uv-test-bw.png',
);
//将纹理分配给材质的颜色贴图插槽
const material = new MeshStandardMaterial({
map: texture,
});
使用相机控制插件
//启用阻尼 物体惯性
controls.enableDamping = true;
controls.tick =() => controls.update()//更新动画循环中的控件 阻尼才生效
使用OrbitControls按需渲染
没有动画循环的情况下,当用户交互时,控件会将相机移动到新的位置,所以要绘制一个新的帧
controls.addEventListener('change', () => {
renderer.render(scene, camera);
});
OrbitControls配置
- 启用或禁用控件
controls.enabled = false;
也可以单独禁用3种控制模式中的任何一种
controls.enableRotate = false;
controls.enableZoom = false;
controls.enablePan = false;
-
自动旋转 修改后要调用
controls.updatecontrols.autoRotate = true;//使相机自动围绕target旋转 controls.autoRotateSpeed = 1;//控制速度 -
限制缩放的距离
controls.minDistance = 5; controls.maxDistance = 20;
环境光
在three.js中灯光分为2类:
-
直接光照,模拟直接光照。
DirectionalLight -
环境光,这是一种廉价且可信的间接照明方式。
环境光分为2类
- AmbientLight从各个方向向每个对象添加恒定数量的光。
- 天空颜色和地面颜色之间的 HemisphereLight渐变,可用于模拟许多常见的照明场景。
DirectionalLight与环境光配对使用是最常见的照明设置之一。
环境光继承自Light 所以有color和intensity属性 环境光不能投射阴影,一个场景只需要1个环境光
AmbientLight 环境光 会从各个方向对场景中的每个对象添加恒定数量的光照
添加AmbientLight 步骤
- 引入
AmbientLight类 - 创建实例
- scene.add(
AmbientLight)
组织场景
SphereBufferGeometry
import { SphereBufferGeometry } from "three";
const radius = 0.25;
c//几何体在宽度和高度周围有多少小三角形onst widthSegments = 16;
const heightSegments = 16;
const geometry = new SphereBufferGeometry(
radius,//半径
//几何体在宽度和高度周围有多少小三角形
widthSegments,
heightSegments
);
Group对象
组在场景图中占据一个位置,并且可以有子对象,但是本身不可见
移动一个组,所有子对象都会移动,子对象也可以独立平移、旋转或缩放(车和车窗)
clone方法
克隆mesh
克隆后分别调整原始mesh和克隆mesh
// only mesh will move
mesh.position.x = 20;
// only clonedMesh will increase in size
clonedMesh.scale.set(5, 5, 5);
但是几何体和材质不是克隆的,它们是共享的。自定义属性不会克隆
给克隆的mesh全新的材料,而原来的材料不会受到影响
clonedMesh.material = new MeshStandardMaterial({ color: "indigo" });
// mesh.material -> still red
旋转的小球案例:
import {
SphereBufferGeometry,
Group,
Mesh,
MeshStandardMaterial,MathUtils
} from 'three';
function createMeshGroup() {
// a group holds other objects
// but cannot be seen itself
const group = new Group();
const geometry = new SphereBufferGeometry(0.25, 16, 16);
const material = new MeshStandardMaterial({
color: 'indigo',
});
const protoSphere = new Mesh(geometry, material);
// add the sphere to the group
group.add(protoSphere);
for (let i = 0; i < 1; i += 0.05) {
const sphere = protoSphere.clone();//生成20个球
// 让球体围绕着protoSphere成一个圆,原本clone出来是叠在一起的
sphere.position.x = Math.cos(2 * Math.PI * i);
sphere.position.y = Math.sin(2 * Math.PI * i);
// group.scale.multiplyScalar(2)//放大一倍
sphere.scale.multiplyScalar(0.01 + i)
group.add(sphere);
}
// 将30度转为弧度
const radiansPerSecond = MathUtils.degToRad(30);
// 每一帧转动30度
group.tick = (delta) => {
group.rotation.z -= delta * radiansPerSecond;
};
return group;
}
export { createMeshGroup };
内置几何体
Material.flatShading属性 默认为false 如果启用了,则不再混合相邻面,会有三角形棱角
`CylinderBufferGeometry
属性:
- radiusTop: 圆柱体顶部的半径。
- radiusBottom: 圆柱底部的半径。
- height: 圆柱体的高度。
three.js中的正旋转方向是逆时针方向。
小火车案例:
大轮子缩放
通过缩放,我们将大轮的直径增加了一倍,并将其长度增加了1.25。但是我们如何确定要在哪些轴上进行缩放?
bigWheel.scale.set(2, 1.25, 2);
即使我们旋转了网格,我们也必须根据原始的、未旋转的几何体来决定如何缩放。对于原始的几何体,增加直径需要在x和y轴上等比例缩放
轮子旋转
对于原始的几何体,我们希望它围绕通过其中心的轴旋转,即是Y轴。
以glTF格式加载3D模型
在web发送3D模型的方式:glTF
加载鹦鹉
async function loadBirds() {
const loader = new GLTFLoader();
const parrotData = await loader.loadAsync('/assets/models/Parrot.glb');
console.log('Squaaawk!', parrotData);
}
GLTFLoader返回的数据:重點关注:
animations 动画剪辑数组
cameras 一组相机
scene 包含文件中任何网格的Group 这是模型存放的地方
提取模型
function setupModel(data) {
const model = data.scene.children[0];
return model;
}
将模型添加到场景
async init() {
const { parrot } = await loadBirds();
scene.add(parrot);
}
批量加载模型
const [parrotData, flamingoData, storkData] = await Promise.all([
loader.loadAsync("/assets/models/Parrot.glb"),
loader.loadAsync("/assets/models/Flamingo.glb"),
loader.loadAsync("/assets/models/Stork.glb"),
]);
three.js 动画系统
动画系统
创建动画涉及三个元素:关键帧、KeyframeTrack和AnimationClip。
关键帧
每个关键帧由三部分信息组成:时间time、属性property和值 value,例如:
-
在 0 秒 .position是(0,0,0)。
-
在 3 秒 .scale是(1,1,1)。
关键帧不指定任何特定对象:位置关键帧可用于为任何具有
.position属性的对象设置动画,缩放关键帧可以为任何具有.scale属性的对象设置动画
KeyframeTrack 关键帧轨迹
KeyframeTrack是基类,每种数据类型都有一个子类:
一般直接用子类,不用KeyframeTrack
-
StringKeyframeTrack创建一个表示位置的矢量关键帧轨迹,包含三个关键帧
- 在 0 秒 .position等于(0,0,0)。
- 在 3 秒 .position等于(2,2,2)。
- 在 6 秒 .position等于(0,0,0)。
import { VectorKeyframeTrack } from "three"; const times = [0, 3, 6]; const values = [0, 0, 0, 2, 2, 2, 0, 0, 0]; const positionKF = new VectorKeyframeTrack(".position", times, values);
AnimationClip 动画剪辑
动画剪辑是附加到单个对象的任意数量的关键帧的集合
动画剪辑存储3部分信息:剪辑名称、剪辑长度,组成剪辑的轨迹数组
import { AnimationClip, VectorKeyframeTrack } from "three";
const times = [0, 3, 6];
const values = [0, 0, 0, 2, 2, 2, 0, 0, 0];
const positionKF = new VectorKeyframeTrack(".position", times, values);
// just one track for now
const tracks = [positionKF];
// use -1 to自动计算tracks的长度
const length = -1;
const clip = new AnimationClip("slowmove", length, tracks);//剪辑名称、剪辑长度,组成剪辑的轨迹数组
AnimationMixer 混合器,将静态对象转为动画对象
我们需要为场景中的每个动画对象使用一个混合,混合器和模型之间存在一对一的关系
AnimationMixer.clipAction 将动画对象连接到动画剪辑
混合器和模型之间存在一对一的关系
import { AnimationClip, AnimationMixer } from "three";
const moveBlinkClip = new AnimationClip("move-n-blink", -1, [
positionKF,
opacityKF,
]);
const mixer = new AnimationMixer(mesh);
const action = mixer.clipAction(moveBlinkClip);
更新循环中的动画
const mixer = new AnimationMixer(mesh);
const clock = new Clock();
const delta = clock.getDelta();//测量每帧渲染所需的时间
mesh.tick = (delta) => mixer.update(delta);
updatables.push(mesh);
以下是播放每只鸟附带的动画剪辑所需要做的事情:
- 从每个 glTF 文件加载的数据中找到飞行剪辑。
- 创建一个
AnimationMixer来控制每个鸟模型。 - 创建一个
AnimationAction将剪辑连接到混合器。 - 为每只鸟添加一个
.tick方法,并在每一帧更新鸟的混合器。