《探索three.js》 笔记

249 阅读10分钟

探索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构造函数有两个参数:几何材质

创建模型的六大步骤

  1. 初始设置

    创建网页来放场景

  2. 创建场景

  3. 创建相机

  4. 创建可见对象

  5. 创建渲染器

  6. 渲染场景

基于物理的渲染和照明

three.js 中的大小单位是

Three.js 中的光照:

  1. 直接照明

    • DirectionalLight => 阳光

      MeshBasicMaterial是最基本的材料 不会对灯光做出反应,整个mesh都是单一颜色着色 可以用环境贴图来改善外观。环境贴图是基于图像照明的一种形式

      `MeshStandardMaterial 可以使用真实世界的物理方程对光做出反应

    • PointLight => 灯泡

    • RectAreaLight => 条形照明或明亮的窗户

    • SpotLight => 聚光灯

    默认情况下禁用阴影

  2. 环境光

变换和坐标系

场景图中的每个对象(顶级场景除外)只有一个父对象,并且可以有任意数量的子对象。

世界坐标系

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标签,具有一些与用作纹理而不是普通图像相关的额外设置

贴图步骤
  1. 引入TextureLoader
  2. 创建TextureLoader实例
  3. 使用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.update

    
    controls.autoRotate = true;//使相机自动围绕target旋转
    controls.autoRotateSpeed = 1;//控制速度
    
  • 限制缩放的距离

    
    controls.minDistance = 5;
    controls.maxDistance = 20;
    

环境光

在three.js中灯光分为2类:

  1. 直接光照,模拟直接光照。 DirectionalLight

  2. 环境光,这是一种廉价且可信的间接照明方式。

    环境光分为2类

    • AmbientLight从各个方向向每个对象添加恒定数量的光。
    • 天空颜色和地面颜色之间的 HemisphereLight渐变,可用于模拟许多常见的照明场景。

DirectionalLight与环境光配对使用是最常见的照明设置之一。

环境光继承自Light 所以有color和intensity属性 环境光不能投射阴影,一个场景只需要1个环境光

AmbientLight 环境光 会从各个方向对场景中的每个对象添加恒定数量的光照

添加AmbientLight 步骤

  1. 引入AmbientLight
  2. 创建实例
  3. 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 动画系统

动画系统

创建动画涉及三个元素:关键帧、KeyframeTrackAnimationClip

关键帧

每个关键帧由三部分信息组成:时间time、属性property和值 value,例如:

  • 在 0 秒 .position是(0,0,0)。

  • 在 3 秒 .scale是(1,1,1)。

    关键帧不指定任何特定对象:位置关键帧可用于为任何具有.position属性的对象设置动画,缩放关键帧可以为任何具有.scale属性的对象设置动画

KeyframeTrack 关键帧轨迹

KeyframeTrack是基类,每种数据类型都有一个子类:

一般直接用子类,不用KeyframeTrack

  • NumberKeyframeTrack

  • VectorKeyframeTrack

  • QuaternionKeyframeTrack

  • BooleanKeyframeTrack

  • StringKeyframeTrack

    创建一个表示位置的矢量关键帧轨迹,包含三个关键帧

    1. 在 0 秒 .position等于(0,0,0)。
    2. 在 3 秒 .position等于(2,2,2)。
    3. 在 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);

以下是播放每只鸟附带的动画剪辑所需要做的事情:

  1. 从每个 glTF 文件加载的数据中找到飞行剪辑。
  2. 创建一个AnimationMixer来控制每个鸟模型。
  3. 创建一个AnimationAction将剪辑连接到混合器。
  4. 为每只鸟添加一个.tick方法,并在每一帧更新鸟的混合器。