Bootstrap

Three.js机器人与星系动态场景(三):如何实现动画

在前面的博客中分别介绍了如何快速搭建3D交互场景以及通过坐标辅助工具加深对坐标系的理解。本文将继续探讨其中动画实现的细节。通过调整rotation加深对动画的印象。

Three.js机器人与星系动态场景:实现3D渲染与交互式控制-CSDN博客 

Three.js机器人与星系动态场景(二):强化三维空间认识-CSDN博客

 动画说白了就是一张张照片,连起来依次展示,这样就形成一个动画效果,只要帧率高,人的眼睛就感觉不到卡顿,是连续的视频效果。

 实现上述效果只需要添加如下动画,在动画中给物体旋转增加一个正向或负向的增量,在方法中调用自身达到一个循环


  const animate = () => {
    requestAnimationFrame(animate);
    robot.rotation.y -= 0.005; //机器人旋转
    robot2.rotation.y -= 0.005;
    // 粒子旋转
    starts.rotation.y -= 0.001;
    starts.rotation.z += 0.001;
    starts.rotation.x += 0.001;
    renderer.render(scene, camera);
  };
  animate(); //添加动画

requestAnimationFrame 动画循环

requestAnimationFrame 是一个循环机制,它会在浏览器准备好绘制下一帧时重复调用提供的函数。这样,您可以创建一个平滑且连续的动画效果。在 animate 函数内部,requestAnimationFrame(animate) 被调用,这确保了 animate 函数会在每一帧都被执行。这是创建无限动画循环的关键。

  1. 浏览器优化:当使用 requestAnimationFrame 时,浏览器会优化动画过程,减少页面闪烁和重绘,确保动画流畅运行。它会根据显示器的刷新率(通常是 60Hz)来调用动画函数,但实际的调用次数可能会根据电脑的性能和浏览器的工作负载动态调整。

  2. 时间间隔requestAnimationFrame 不接受任何时间间隔参数,它会在浏览器认为合适的时候调用函数,这通常与屏幕的刷新率同步。与 setInterval 或 setTimeout 不同,您不需要担心设置一个固定的时间间隔,这有助于避免动画在不同设备上出现不同步的问题。

  3. 暂停和恢复:当用户切换到另一个标签或最小化浏览器窗口时,requestAnimationFrame 会自动暂停,这有助于节省 CPU 和 GPU 资源。当用户返回到页面时,动画会自动恢复。

rotation旋转

rotation旋转,在前面的示例中机器人围绕y轴逆时针旋转。代码如下:

  const animate = () => {
    requestAnimationFrame(animate);
    robot.rotation.y -= 0.005; //机器人旋转
    robot2.rotation.y -= 0.005;
    ...

    renderer.render(scene, camera);
  };

通过rotation属性给robot.rotation.y一个负值,每帧robot绕着y轴逆时针旋转0.005个弧度单位。通过requestAnimationFrame进行动画循环,不断的帧连续播放就形成了动画的效果 

X轴旋转 

让处在原点的机器人让x轴转

  const animate = () => {
    requestAnimationFrame(animate);
    robot.rotation.x -= 0.005; //原点机器人旋转
    robot2.rotation.y -= 0.005;
    renderer.render(scene, camera);
  };

Z轴旋转 

绕z轴旋转,这种感觉演我们上班的状态,要死不活

  const animate = () => {
    requestAnimationFrame(animate);
    robot.rotation.z -= 0.005; //机器人旋转
    robot2.rotation.y -= 0.005;
    ...
    renderer.render(scene, camera);
  };

 完整代码

import { useEffect, useRef } from "react";
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
import { FontLoader } from "three/examples/jsm/loaders/FontLoader";
import { TextGeometry } from "three/examples/jsm/geometries/TextGeometry";
//机器人脑袋
function createHead() {
  //SphereGeometry创建球形几何体
  const head = new THREE.SphereGeometry(4, 32, 16, 0, Math.PI * 2, 0, Math.PI * 0.5);
  const headMaterial = new THREE.MeshStandardMaterial({
    color: 0x43b988,
    roughness: 0.5,
    metalness: 1.0,
  });
  const headMesh = new THREE.Mesh(head, headMaterial);
  return headMesh;
}
//触角
function generateHorn(y: number, z: number, angle: number) {
  //触角 CapsuleGeometry 创建胶囊形状的几何体。胶囊形状可以看作是一个圆柱体两端加上半球体
  const line = new THREE.CapsuleGeometry(0.1, 2);
  const lineMaterial = new THREE.MeshStandardMaterial({
    color: 0x43b988,
    roughness: 0.5,
    metalness: 1.0,
  });
  const lineMesh = new THREE.Mesh(line, lineMaterial);
  lineMesh.position.y = y;
  lineMesh.position.z = z;
  lineMesh.rotation.x = angle;
  return lineMesh;
}
//机器人眼睛
function generateEye(x: number, y: number, z: number) {
  //SphereGeometry创建球形几何体
  const eye = new THREE.SphereGeometry(0.5, 32, 16, 0, Math.PI * 2, 0, Math.PI * 2);
  const eyeMaterial = new THREE.MeshStandardMaterial({
    color: 0x212121,
    roughness: 0.5,
    metalness: 1.0,
  });
  const eyeMesh = new THREE.Mesh(eye, eyeMaterial);
  eyeMesh.position.x = x;
  eyeMesh.position.y = y;
  eyeMesh.position.z = z;
  return eyeMesh;
}
//机器人身体
function generateBody() {
  //CylinderGeometry第一个参数是上部分圆的半径,第二个参数是下部分圆的半径,第三个参数是高度,材质使用的跟腿一样
  const body = new THREE.CylinderGeometry(4, 4, 6);
  const bodyMaterial = new THREE.MeshStandardMaterial({
    color: 0x43b988,
    roughness: 0.5,
    metalness: 1.0,
  });
  const bodyMesh = new THREE.Mesh(body, bodyMaterial);
  return bodyMesh;
}
//胳膊、腿
function generateLegs(y: number, z: number) {
  const leg1 = new THREE.CapsuleGeometry(1, 4);
  const legMaterial1 = new THREE.MeshStandardMaterial({
    color: 0x43b988,
    roughness: 0.5,
    metalness: 1.0,
  });
  const leg1Mesh = new THREE.Mesh(leg1, legMaterial1);
  leg1Mesh.position.y = y;
  leg1Mesh.position.z = z;
  return leg1Mesh;
}
//创建机器人
function generateRobot() {
  // 创建一个Three.js对象,用于存放机器人
  const robot = new THREE.Object3D();
  const headMesh = createHead();
  headMesh.position.y = 6.5;
  robot.add(headMesh);
  //眼睛
  const leftEye = generateEye(3, 8, -2);
  const rightEye = generateEye(3, 8, 2);
  robot.add(leftEye);
  robot.add(rightEye);
  const leftHorn = generateHorn(11, -1, (-Math.PI * 30) / 180);
  const rightHorn = generateHorn(11, 1, (Math.PI * 30) / 180);
  robot.add(leftHorn);
  robot.add(rightHorn);
  const body = generateBody();
  body.position.y = 4;
  robot.add(body);

  // 生成机器人左腿
  robot.add(generateLegs(0, -2));
  // 生成机器人右腿
  robot.add(generateLegs(0, 2));
  //胳膊
  robot.add(generateLegs(3, 5));

  robot.add(generateLegs(3, -5));
  //物体缩放
  robot.scale.x = 0.3;
  robot.scale.y = 0.3;
  robot.scale.z = 0.3;
  return robot;
}
//创建粒子星星
function generateStarts(num: number) {
  //制作粒子特效
  const starts = new THREE.Object3D();
  const obj = new THREE.SphereGeometry(0.2, 3, 3);
  const material = new THREE.MeshStandardMaterial({
    color: 0x43b988,
    roughness: 0.5,
    metalness: 5,
  });
  const mesh = new THREE.Mesh(obj, material);
  for (let i = 0; i < num; i++) {
    const target = new THREE.Mesh();
    target.copy(mesh);
    target.position.x = Math.floor(Math.random() * 18 + Math.floor(Math.random() * -18));
    target.position.y = Math.floor(Math.random() * 18 + Math.floor(Math.random() * -18));
    target.position.z = Math.floor(Math.random() * 18 + Math.floor(Math.random() * -18));
    starts.add(target);
  }
  return starts;
}
//创建文本
function createText(content: string, font: any) {
  const textGeometry = new TextGeometry(content, {
    font: font,
    size: 1,
    height: 0.1,
    curveSegments: 1,
  });
  textGeometry.center();
  const textMaterial = new THREE.MeshPhongMaterial({ color: 0xffffff, flatShading: true }); // front
  const mesh = new THREE.Mesh(textGeometry, textMaterial);
  return mesh;
}
/**
 * 创建一个Three.js场景,包括相机和渲染器
 */
function Robot() {
  // 创建一个div容器,用于存放渲染的Three.js场景
  const containerRef = useRef<HTMLDivElement>(null);
  const scene = new THREE.Scene();
  // 创建一个Three.js相机,包括透视投影、宽高比、近裁剪面和远裁剪面
  const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
  camera.position.set(15, 12, 8);
  camera.lookAt(0, 0, 0);
  // 创建一个Three.js渲染器,包括抗锯齿
  const renderer = new THREE.WebGLRenderer({ antialias: true });
  renderer.setSize(window.innerWidth, window.innerHeight);

  //添加坐标系
  const axisHelper = new THREE.AxesHelper(150);
  scene.add(axisHelper);
  //坐标系添加文字
  const loader = new FontLoader();
  let meshX = new THREE.Mesh();
  let meshY = new THREE.Mesh();
  let meshZ = new THREE.Mesh();
  loader.load("fonts/optimer_regular.typeface.json", function (font) {
    meshX = createText("X", font);
    meshY = createText("Y", font);
    meshZ = createText("Z", font);
    meshX.position.x = 12;
    meshY.position.y = 12;
    meshZ.position.z = 12;
    scene.add(meshX);
    scene.add(meshY);
    scene.add(meshZ);
  });

  const robot = generateRobot();
  const robot2 = generateRobot();
  robot2.position.x = 6;
  robot2.position.z = 6;
  // 将机器人身体添加到场景中
  scene.add(robot);
  scene.add(robot2);
  // 创建一个Three.js方向光,包括颜色、强度
  const straightLight = new THREE.DirectionalLight(0xffffff, 5);
  // 设置方向光的位置
  straightLight.position.set(20, 20, 20);
  // 将方向光添加到场景中
  scene.add(straightLight);

  const starts = generateStarts(200);
  scene.add(starts);

  //轨道控制器
  const controls = new OrbitControls(camera, renderer.domElement);
  controls.update();

  const animate = () => {
    requestAnimationFrame(animate);
    robot.rotation.z -= 0.005; //机器人旋转
    robot2.rotation.y -= 0.005;
    // 粒子旋转
    starts.rotation.y -= 0.001;
    starts.rotation.z += 0.001;
    starts.rotation.x += 0.001;
    //
    meshX.lookAt(camera.position);
    meshY.lookAt(camera.position);
    meshZ.lookAt(camera.position);
    renderer.render(scene, camera);
  };
  animate(); //添加动画

  // 监听组件挂载和卸载
  useEffect(() => {
    // 如果div存在,将渲染器dom元素添加到div中
    if (containerRef.current) {
      containerRef.current.appendChild(renderer.domElement);
      // 渲染场景
      renderer.render(scene, camera);
    }
  }, [containerRef]);

  // 返回div容器,用于存放渲染的Three.js场景
  return <div ref={containerRef} style={{ width: "100vw", height: "100vh" }}></div>;
}

// 导出Robot组件
export default Robot;

;