# 入门及调试

# 轨道控制器查看物体

除了渲染几何体,但是无法全方位的查看该三维几何体。这时候通过轨道控制器,让相机围绕几何体运动,如同太阳围绕地球一般,去观察几何体。实现旋转缩放预览

TIP

  • 旋转:拖动鼠标左键
  • 缩放:滚动鼠标中键
  • 平移:拖动鼠标右键

轨道控制器

# 创建轨道控制器

// 引入轨道控制器扩展库OrbitControls.js
import { OrbitControls } from "three/addons/controls/OrbitControls.js";

// 创建轨道控制器
const controls = new OrbitControls(camera, renderer.domElement);
// 创建控制器阻尼,让控制器看起来更真实。必须在动画循环里面调用update
controls.enableDamping = true

// 必须传入2个参数:
// 相机,让哪一个相机围绕目标运动。默认目标是原点。立方体在原点处。
// 渲染的画布dom对象,用于监听鼠标事件控制相机的围绕运动。

OrbitControls本质

OrbitControls 本质上就是改变相机的参数,比如相机的位置属性,改变相机位置也可以改变相机拍照场景中模型的角度,实现模型的360度旋转预览效果,改变透视投影相机距离模型的距离,就可以改变相机能看到的视野范围。

controls.addEventListener('change', function () {
    // 浏览器控制台查看相机位置变化
    console.log('camera.position',camera.position);
});

# 每一帧根据控制器更新画面

因为控制器监听鼠标事件之后,要根据鼠标的拖动,来控制相机围绕目标运动,并根据运动之后的效果,显示出画面来。 为了保证画面流畅渲染,选择使用请求动画帧requestAnimationFrame,在屏幕渲染下一帧画面时触发回调函数来执行画面的渲染。

function render() {
  // 如果后期需要控制器带有阻尼效果,或者自动旋转等效果,就需要加入controls.update()
  controls.update()
  renderer.render(scene, camera);
  // 渲染下一帧的时候就会调用render函数
  requestAnimationFrame(render);
}

render();

# 综合代码

<template>
  <div id="canvas-container" ref="container"></div>
</template>

<script setup>
import { ref, onMounted } from "vue";
import {
  Scene,
  Color,
  PerspectiveCamera,
  WebGLRenderer,
  Mesh,
  BoxBufferGeometry,
  MeshBasicMaterial,
  AxesHelper,
} from "three";

import { OrbitControls } from "three/addons/controls/OrbitControls.js";

const container = ref();

onMounted(() => {
  // 1、创建场景
  const scene = new Scene();
  // 设置场景的属性
  scene.background = new Color("black");

  // 2、创建相机
  const fov = 35; // AKA Field of View
  const aspect = container.value.clientWidth / container.value.clientHeight;
  const near = 0.1; // the near clipping plane
  const far = 100; // the far clipping plane

  const camera = new PerspectiveCamera(fov, aspect, near, far);
  // 设置相机位置
  camera.position.set(0, 0, 10);

  // 3、创建一个几何体
  const geometry = new BoxBufferGeometry(1, 1, 1);

  // 创建一个默认白色材质
  const material = new MeshBasicMaterial();

  // 根据几何体和材质创建物体
  const cube = new Mesh(geometry, material);

  // 添加几何体到场景中
  scene.add(cube);

  // 添加坐标辅助器
  const axesHelper = new AxesHelper(3);
  scene.add(axesHelper);

  // 初始化渲染器
  const renderer = new WebGLRenderer();

  // 接下来,将渲染器设置为与我们的容器元素相同的大小
  renderer.setSize(container.value.clientWidth, container.value.clientHeight);

  // 最后,设置像素比例,以便我们的场景在HiDPI显示器上看起来不错
  renderer.setPixelRatio(window.devicePixelRatio);

  // 将自动创建的元素添加到<canvas>页面
  container.value.append(renderer.domElement);

  //   // 渲染或“创建静止图像”场景
  //   renderer.render(scene, camera);

  // 添加控制器,鼠标可以交互旋转,看起来是一个真正的立方体。
  const controls = new OrbitControls(camera, renderer.domElement);

  function render() {
    renderer.render(scene, camera);
    requestAnimationFrame(render);
  }
  render();
});
</script>

<style lang="scss" scoped>
#canvas-container {
  width: 500px;
  height: 500px;
}
</style>

# requestAnimationFrame

是HTML5的新特性,区别于setTimeout和setInterval。requestAnimationFrame比后两者精确,采用系统时间间隔,保持最佳绘制效率,不会因为间隔时间过短,造成过度绘制,增加开销;也不会因为间隔时间太长,使动画卡顿不流畅,让各种网页动画效果能够有一个统一的刷新机制,从而节省系统资源,提高系统性能,改善视觉效果。

requestAnimationFrame是由浏览器专门为动画提供的API,在运行时浏览器会自动优化方法的调用,并且如果页面不是激活状态下的话,动画会自动暂停,有效节省了CPU开销。因此屏幕每一帧都刷新一次画面,就需要执行

# 坐标轴辅助器

开发阶段便于调试查看,添加坐标辅助器。红色代表 X 轴. 绿色代表 Y 轴. 蓝色代表 Z 轴。

import { AxesHelper } from 'three'

const axesHelper = new AxesHelper( 5 );
scene.add( axesHelper ); // 添加到创建的场景中

# 控制物体移动

为了让物体移动起来。我们可以设置它的position属性进行位置的设置。

相机和几何体都是物体。每个物体都是1个对象。

  • 在官方文档里,我们可以看到相机camera和物体mesh都继承Object3D类。所以camera、mesh都属于3d对象。
  • 从3d对象的官方文档里,我们可以找到position属性,并且该属性一个vector3对象
// 方式1:设置该向量的x、y 和 z 分量。
cube.position.set(x,y,z); // set继承至vector3类

// 方式2:直接设置position的x,y,z属性
cube.position.x = x;
cube.position.y = y;
cube.position.z = z;

# 每一帧修改一点位置形成动画

例如,每一帧让立方体向右移动0.01,并且当位置大于5时,从0开始。

function render() {
  cube.position.x += 0.01;
  if (cube.position.x > 5) {
    cube.position.x = 0;
  }
  renderer.render(scene, camera);
  //   渲染下一帧的时候就会调用render函数
  requestAnimationFrame(render);
}

render();

控制物体移动

# 控制物体缩放

因为物体的scale属性是vector3对象,因此按照vector的属性和方法,设置x/y/z轴方向的缩放大小。

//例如设置x轴放大3倍、y轴方向放大2倍、z轴方向不变
cube.scale.set(3, 2, 1);

//单独设置某个轴的缩放
cube.scale.x = 3
cube.scale.y = 2
cube.scale.z = 1

# 控制物体旋转

因为的旋转通过设置rotation属性,该属性是Euler类 的实例[Euler:欧拉],因此可以通过Euler类的方法进行设置旋转角度。

//直接设置旋转属性,例如围绕x轴旋转90度
cube.rotation.x = -Math.PI/2

//围绕x轴旋转45度
cube.rotation.set(-Math.PI / 4, 0, 0, "XZY");

//  继承的方法: .set ( x : Float, y : Float, z : Float, order : String ) : this
//  参数说明: x - 用弧度表示x轴旋转量。 y - 用弧度表示y轴旋转量。 z - 用弧度表示z轴旋转量。 order - (optional) 表示旋转顺序的字符串。

# 正确处理动画的运行

为了最好的利用性能和渲染效果,那么我们只需要在绘制每一帧画面的时候,计算需要渲染的画面即可。 这个时候就可以使用window.requestAnimationFrame方法。

确保总是使用第一个参数 (或其它获得当前时间的方法) 计算每次调用之间的时间间隔,否则动画在高刷新率的屏幕中会运行得更快。

let preTime;
function render(time) {
  //第一次调用render函数,没有上一帧的时间
  if (preTime === undefined) {
    preTime = time;
  }
  
  //计算每帧画面的间隔时间,单位毫秒
  const deltaTime = time - preTime;
  //保留当前时间作为上一帧时间,用于下一帧计算2帧间隔
  preTime = time;
  //renderer.render(scene, camera);
  //   渲染下一帧的时候就会调用render函数
  requestAnimationFrame(render);
}

render();

TIP

为了确保不同时间间隔,运动的速度一致,那么应该按照

移动距离 = 速度 * 时间

那么如果想要1m/s的速度远离原点的匀速运动

let preTime;
function render(time) {
  //第一次调用render函数,没有上一帧的时间
  if (preTime === undefined) {
    preTime = time;
  }
  
  //计算每帧画面的间隔时间,单位毫秒,当前时间减去上一帧的时间,即为2帧直接的间隔时间
  const deltaTime = time - preTime;
  
  preTime = time;
  
  // cube物体允许运动
  // elapsedTime/1000是将毫秒改为秒
  // 1m/s的速度* 时间(秒)= 移动的距离
  // 将当前位置+=移动的距离,即为最后的距离
  cube.position.x += 1 * (deltaTime/1000)
  
  
  // renderer.render(scene, camera);
  // 渲染下一帧的时候就会调用render函数
  requestAnimationFrame(render);
}

render();

# 尺寸自适应

 window.addEventListener("resize", () => {
    // 更新摄像头
    camera.aspect = container.value.clientWidth / container.value.clientHeight;
    // 更新摄像机的投影矩阵
    camera.updateProjectionMatrix();

    // 更新渲染器
    renderer.setSize(container.value.clientWidth, container.value.clientHeight);
    // 最后,设置像素比例
    renderer.setPixelRatio(window.devicePixelRatio);
});

# 控制场景全屏

window.addEventListener("dblclick", () => {
    const fullScreenElement = document.fullscreenElement;
    if (!fullScreenElement) {
      // 双击控制屏幕进入全屏,退出全屏
      // 让画布对象全屏
      renderer.domElement.requestFullscreen();
    } else {
      // 退出全屏,使用document对象
      document.exitFullscreen();
    }
});

# gui 可视化改变三维场景

  • 安装dat.gui
npm install dat.gui --save
  • 引入
// 导入dat.gui
import * as dat from "dat.gui";

示例代码

// 控制变量图形界面
const gui = new dat.GUI();
gui
  .add(cube.position, "x")
  .min(0)
  .max(5)
  .step(0.01)
  .name("移动x轴坐标")
  // 改变事件
  .onChange((value) => {
    console.log(value);
  })
  // 改变完成后事件回调
  .onFinishChange((value) => {
    console.log("value", value);
  });

// 修改物体的颜色
gui.addColor({ color: "#fff" }, "color").onChange((color) => {
  cube.material.color.set(color);
});
// .onFinishChange((color) => {
//   cube.material.color.set(color);
// });

// 设置选项框
gui.add(cube, "visible").name("是否显示");

// 点击触发某个事件
const fun = () => {
  console.log("事件已触发");
  // do something
};
gui.add({ fun: fun }, "fun").name("点击触发事件");

 // 设置文件夹
var folder = gui.addFolder("设置立方体属性");
folder.add(cube.material, "wireframe"); // 只显示线框
folder.add(cube.material, "visible"); // 是否显示

# .domElement

通过.domElement属性可以获取gui界面的HTML元素,那就意味着你可以改变默认的style样式,比如位置、宽度等

//改变交互界面style属性
gui.domElement.style.right = '0px';
gui.domElement.style.width = '300px';

# .name()

.add()创建的交互界面,会默认显示所改变属性的名字,为了通过交互界面更好理解你改变的某个对象属性,你可以通过.name()方法改变gui生成交互界面显示的内容。示例如上。

# .step()

步长.step()方法可以设置交互界面每次改变属性值间隔是多少。示例如上。

# .onChange()

当gui界面某个值的时候,.onChange()方法就会执行,这时候你可以根据需要通过.onChange()执行某些代码。示例如上。

# addColor()

.addColor()生成颜色值改变的交互界面。

# gsap动画库

# 安装

npm install gsap 

# 导入使用

import gsap from 'gsap'

var moveXanimation = gsap.to(cube.position, {
  x: 3,
  duration: 5,
  ease: "", // 具体查看官网
  repeat: -1, // -1 无线循环
  yoyo: true, // 往返运动
  delay: 2, // 动画开始延迟时间
  onStart: () => {
    console.log("动画开始");
  },
  onComplete: () => {
    console.log("动画完成");
  },
});

gsap.to(cube.rotation, {
  x: 2 * Math.PI,
  duration: 5,
  repeat: -1,
  yoyo: true,
});

// 控制动画开始与暂停
window.addEventListener("dblclick", () => {
  if (moveXanimation.isActive()) {
    moveXanimation.pause();
  } else {
    moveXanimation.resume();
  }
});
Last Updated: 4/10/2023, 9:08:41 PM