使用React复刻ThreeJS官网示例——keyframes动画

admin2024-07-01  41

使用React复刻ThreeJS官网示例——keyframes动画,第1张

本文参考的是threeJs给的第一个示例

使用React复刻ThreeJS官网示例——keyframes动画,第2张

 一、下载threeJS源码

 从git上拉取代码后可以找到示例一的源码使用React复刻ThreeJS官网示例——keyframes动画,第3张

拷贝threeJS的必要的模型和方法

为了方便后续学习,我们直接将这两个文件夹jsm和models拷贝到react项目中;注意路径最好是public下,public是默认的静态资源加载入口

 使用React复刻ThreeJS官网示例——keyframes动画,第4张

 二、功能解析与改写

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

 引入必要信息

import { useEffect, useRef } from "react";
import * as THREE from "three";
import Stats from "three/examples/jsm/libs/stats.module.js";
import { GLTF, GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
import { RoomEnvironment } from "three/examples/jsm/environments/RoomEnvironment.js";
import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader.js";

初始化Render渲染器/Scene场景/ camer相机/controls轨道控制器


// 初始化渲染器的函数
/**
 * 初始化 WebGL 渲染器
 * @returns {THREE.WebGLRenderer} 创建并配置好的渲染器实例
 */
// 初始化渲染
function initRender(): THREE.WebGLRenderer {
  // 创建一个WebGL渲染器
  const renderer = new THREE.WebGLRenderer({ antialias: true });
  // 根据设备像素比设置渲染器像素比
  renderer.setPixelRatio(window.devicePixelRatio);
  // 设置渲染器大小
  renderer.setSize(window.innerWidth, window.innerHeight);
  return renderer;
}

// 初始化场景的函数
/**
 * 初始化场景
 * @param {THREE.WebGLRenderer} renderer - 渲染器实例
 * @returns {THREE.Scene} 创建并配置好的场景实例
 */
function initScene(renderer: THREE.WebGLRenderer) {
  // 创建 PMREM 生成器
  const pmremGenerator = new THREE.PMREMGenerator(renderer);
  // 创建场景
  const scene = new THREE.Scene();
  // 设置场景背景
  scene.background = new THREE.Color(0xbfe3dd);
  // 设置场景环境
  scene.environment = pmremGenerator.fromScene(new RoomEnvironment(renderer), 0.04).texture;
  return scene;
}

// 初始化相机的函数
/**
 * 初始化相机
 * @param {number} x - 相机在 x 轴的位置
 * @param {number} y - 相机在 y 轴的位置
 * @param {number} z - 相机在 z 轴的位置
 * @returns {THREE.PerspectiveCamera} 创建并配置好位置的相机实例
 */
function initCamera(x: number, y: number, z: number) {
  const camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 1, 100);
  camera.position.set(x, y, z);
  return camera;
}

// 初始化控制器的函数
/**
 * 初始化轨道控制器
 * @param {THREE.PerspectiveCamera} camera - 相机实例
 * @param {THREE.WebGLRenderer} renderer - 渲染器实例
 * @returns {OrbitControls} 创建并配置好的轨道控制器实例
 */
function initControls(camera: THREE.PerspectiveCamera, renderer: THREE.WebGLRenderer) {
  const controls = new OrbitControls(camera, renderer.domElement);
  controls.update();
  controls.enablePan = false;
  controls.enableDamping = true;
  return controls;
}

 组件核心方法Keyframes

/**
 * Keyframes 组件函数
 */
function Keyframes() {
  const containerRef = useRef<HTMLDivElement>(null); // 创建用于引用 HTML 元素的 ref
  const clock = new THREE.Clock(); // 创建时钟实例
  const statsRef = useRef<Stats>(); // 创建用于引用统计信息的 ref
  const mixerRef = useRef<THREE.AnimationMixer>(); // 创建用于引用动画混合器的 ref

  const renderer = initRender(); // 初始化渲染器
  const scene = initScene(renderer); // 初始化场景
  const camera = initCamera(5, 2, 10); // 初始化相机
  const controls = initControls(camera, renderer); // 初始化控制器
  controls.target.set(0, 0.5, 0); // 设置控制器的目标

  const dracoLoader = new DRACOLoader(); // 创建 Draco 加载器
  dracoLoader.setDecoderPath("jsm/libs/draco/gltf/"); // 设置 Draco 解码器路径

  const loader = new GLTFLoader(); // 创建 GLTF 加载器
  loader.setDRACOLoader(dracoLoader); // 为 GLTF 加载器设置 Draco 加载器

  // 加载 GLTF 模型
  loader.load(
    "models/gltf/LittlestTokyo.glb",
    (gltf: GLTF) => {
      const model = gltf.scene; // 获取模型的场景
      model.position.set(1, 1, 0); // 设置模型的位置
      model.scale.set(0.01, 0.01, 0.01); // 设置模型的缩放
      scene.add(model); // 将模型添加到场景

      mixerRef.current = new THREE.AnimationMixer(model); // 创建动画混合器
      mixerRef.current.clipAction(gltf.animations[0]).play(); // 播放动画

      renderer.setAnimationLoop(animate); // 设置渲染循环
    },
    undefined,
    (e) => {
      console.error(e); // 处理加载错误
    },
  );

  // 渲染循环函数
  /**
   * 每一帧的更新和渲染逻辑
   */
  function animate() {
    const delta = clock.getDelta(); // 获取时间间隔
    mixerRef.current && mixerRef.current.update(delta); // 更新动画混合器
    controls.update(); // 更新控制器
    statsRef.current && statsRef.current.update(); // 更新统计信息
    renderer.render(scene, camera); // 渲染场景和相机
  }

  // 处理窗口大小改变的函数
  /**
   * 处理窗口大小改变时的相机和渲染器更新
   */
  function onWindowResize() {
    camera.aspect = window.innerWidth / window.innerHeight; // 更新相机的宽高比
    camera.updateProjectionMatrix(); // 更新相机的投影矩阵
    controls.update(); // 更新控制器
    renderer.setSize(window.innerWidth, window.innerHeight); // 更新渲染器的大小
  }

  // 使用 useEffect 钩子
  useEffect(() => {
    if (!containerRef.current) return;
    containerRef.current.appendChild(renderer.domElement); // 将渲染器的 DOM 元素添加到引用的元素中

    statsRef.current = new Stats(); // 创建统计信息实例

    containerRef.current.appendChild(statsRef.current.dom); // 将统计信息的 DOM 元素添加到引用的元素中

    window.addEventListener("resize", onWindowResize); // 添加窗口大小改变的监听事件
    return () => {
      window.removeEventListener("resize", onWindowResize); // 清除窗口大小改变的监听事件
      renderer.setAnimationLoop(null); // 清除渲染循环
    };
  }, []);

  return <div ref={containerRef}></div>; // 返回一个带有 ref 的 div 元素
}
export default Keyframes; // 导出 Keyframes 组件

 模型加载与Draco解码

示例模型提供的是压缩后的模型,在页面加载时需要进行解压,必须使用dracoLoader方法,设置解码方法所在路径。在通过GLTFLoader导入。示例如下:


  const dracoLoader = new DRACOLoader(); // 创建 Draco 加载器
  dracoLoader.setDecoderPath("jsm/libs/draco/gltf/"); // 设置 Draco 解码器路径

  const loader = new GLTFLoader(); // 创建 GLTF 加载器
  loader.setDRACOLoader(dracoLoader); // 为 GLTF 加载器设置 Draco 加载器

  // 加载 GLTF 模型
  loader.load(
    "models/gltf/LittlestTokyo.glb",
    (gltf: GLTF) => {
      //处理模型
    },
    undefined,
    (e) => {
      console.error(e); // 处理加载错误
    },
  );

AnimationMixer 动画混合器 

 在我们的示例中模型加载到场景时默认时没有动画的,也就是模型自身的动画比如小火车和风扇小人都是不动的。使用React复刻ThreeJS官网示例——keyframes动画,第5张

 在模型加载的时候通过AnimationMixer开启模型动画

  // 加载 GLTF 模型
  loader.load(
    "models/gltf/LittlestTokyo.glb",
    (gltf: GLTF) => {
      const model = gltf.scene; // 获取模型的场景
      model.position.set(1, 1, 0); // 设置模型的位置
      model.scale.set(0.01, 0.01, 0.01); // 设置模型的缩放
      scene.add(model); // 将模型添加到场景

      mixerRef.current = new THREE.AnimationMixer(model); // 创建动画混合器
      mixerRef.current.clipAction(gltf.animations[0]).play(); // 播放动画

      renderer.setAnimationLoop(animate); // 设置渲染循环
    },
    undefined,
    (e) => {
      console.error(e); // 处理加载错误
    },
  );
setAnimationLoop动画循环  

以下是为什么加载模型时必须使用 setAnimationLoop 的一些原因:

  1. 渲染控制:通过 setAnimationLoop,你可以控制渲染循环的开始和结束。如果你不设置它,即使模型加载完成,也不会自动开始渲染过程。

  2. 动画播放:在你的代码中,你使用了 AnimationMixer 来播放模型中的动画。这个动画需要在每一帧更新,以确保动画的连贯性和流畅性。setAnimationLoop 允许你在每一帧更新动画状态。

  3. 性能优化:使用 setAnimationLoop 可以让你在不需要渲染的时候停止渲染,比如在浏览器标签页不可见时,这样可以节省资源并提高性能。

  4. 逻辑更新:在 animate 函数中,你可以执行除了渲染之外的其他逻辑,比如更新动画、控制器和统计信息等。这些更新是渲染过程的一部分,需要在每一帧进行。

 可以看到模型自身的多个动画都动起来了使用React复刻ThreeJS官网示例——keyframes动画,第1张

Stats.js帧检测工具 

示例程序的左上角有个工具窗口持续监测FPS数值 

使用React复刻ThreeJS官网示例——keyframes动画,第7张

用法 

可以这样引入到项目中

import Stats from "three/examples/jsm/libs/stats.module.js";

通过new Stats()方法创建一个stats实例 。默认showPanel是0,显示FPS面板。

使用React复刻ThreeJS官网示例——keyframes动画,第8张

 通过showPanel方法切换显示方式;可以根据dom改变stats面板的位置,使用示例如下

    const statsRef = useRef<Stats>(); // 创建用于引用统计信息的 ref

    statsRef.current = new Stats(); // 创建统计信息实例
    statsRef.current.showPanel(1);
    statsRef.current.dom.style.position = "absolute"; // 设置统计信息的 DOM 元素的位置
    statsRef.current.dom.style.top = "0px"; // 设置统计信息的 DOM 元素的位置
    statsRef.current.dom.style.left = "0px"; // 设置统计信息的 DOM 元素的位置

通过操作dom的方式将stats节点追加到3D场景中

    containerRef.current.appendChild(statsRef.current.dom); // 将统计信息的 DOM 元素添加到引用的元素中

使用React复刻ThreeJS官网示例——keyframes动画,第9张

 响应式窗口

页面加载时给了初始的renderer的宽高,但是如果用户使用过程中可视区域发生了变化renderer无法自动使用屏幕

使用React复刻ThreeJS官网示例——keyframes动画,第10张

 可以在useEffect里通过事件监听浏览器的resize事件,当浏览器尺寸变化时重新以最新的宽高设为renderer的尺寸信息

  // 处理窗口大小改变的函数
  /**
   * 处理窗口大小改变时的相机和渲染器更新
   */
  function onWindowResize() {
    camera.aspect = window.innerWidth / window.innerHeight; // 更新相机的宽高比
    camera.updateProjectionMatrix(); // 更新相机的投影矩阵
    controls.update(); // 更新控制器
    renderer.setSize(window.innerWidth, window.innerHeight); // 更新渲染器的大小
  }

  // 使用 useEffect 钩子
  useEffect(() => {
    if (!containerRef.current) return;
    containerRef.current.appendChild(renderer.domElement); // 将渲染器的 DOM 元素添加到引用的元素中

    statsRef.current = new Stats(); // 创建统计信息实例

    containerRef.current.appendChild(statsRef.current.dom); // 将统计信息的 DOM 元素添加到引用的元素中

    window.addEventListener("resize", onWindowResize); // 添加窗口大小改变的监听事件
    return () => {
      window.removeEventListener("resize", onWindowResize); // 清除窗口大小改变的监听事件
      renderer.setAnimationLoop(null); // 清除渲染循环
    };
  }, []);

使用React复刻ThreeJS官网示例——keyframes动画,第11张

完整代码 

import { useEffect, useRef } from "react";
import * as THREE from "three";
import Stats from "three/examples/jsm/libs/stats.module.js";
import { GLTF, GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
import { RoomEnvironment } from "three/examples/jsm/environments/RoomEnvironment.js";
import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader.js";

// 初始化渲染器的函数
/**
 * 初始化 WebGL 渲染器
 * @returns {THREE.WebGLRenderer} 创建并配置好的渲染器实例
 */
// 初始化渲染
function initRender(): THREE.WebGLRenderer {
  // 创建一个WebGL渲染器
  const renderer = new THREE.WebGLRenderer({ antialias: true });
  // 根据设备像素比设置渲染器像素比
  renderer.setPixelRatio(window.devicePixelRatio);
  // 设置渲染器大小
  renderer.setSize(window.innerWidth, window.innerHeight);
  return renderer;
}

// 初始化场景的函数
/**
 * 初始化场景
 * @param {THREE.WebGLRenderer} renderer - 渲染器实例
 * @returns {THREE.Scene} 创建并配置好的场景实例
 */
function initScene(renderer: THREE.WebGLRenderer) {
  // 创建 PMREM 生成器
  const pmremGenerator = new THREE.PMREMGenerator(renderer);
  // 创建场景
  const scene = new THREE.Scene();
  // 设置场景背景
  scene.background = new THREE.Color(0xbfe3dd);
  // 设置场景环境
  scene.environment = pmremGenerator.fromScene(new RoomEnvironment(renderer), 0.04).texture;
  return scene;
}

// 初始化相机的函数
/**
 * 初始化相机
 * @param {number} x - 相机在 x 轴的位置
 * @param {number} y - 相机在 y 轴的位置
 * @param {number} z - 相机在 z 轴的位置
 * @returns {THREE.PerspectiveCamera} 创建并配置好位置的相机实例
 */
function initCamera(x: number, y: number, z: number) {
  const camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 1, 100);
  camera.position.set(x, y, z);
  return camera;
}

// 初始化控制器的函数
/**
 * 初始化轨道控制器
 * @param {THREE.PerspectiveCamera} camera - 相机实例
 * @param {THREE.WebGLRenderer} renderer - 渲染器实例
 * @returns {OrbitControls} 创建并配置好的轨道控制器实例
 */
function initControls(camera: THREE.PerspectiveCamera, renderer: THREE.WebGLRenderer) {
  const controls = new OrbitControls(camera, renderer.domElement);
  controls.update();
  controls.enablePan = false;
  controls.enableDamping = true;
  return controls;
}

/**
 * Keyframes 组件函数
 */
function Keyframes() {
  const containerRef = useRef<HTMLDivElement>(null); // 创建用于引用 HTML 元素的 ref
  const clock = new THREE.Clock(); // 创建时钟实例
  const statsRef = useRef<Stats>(); // 创建用于引用统计信息的 ref
  const mixerRef = useRef<THREE.AnimationMixer>(); // 创建用于引用动画混合器的 ref

  const renderer = initRender(); // 初始化渲染器
  const scene = initScene(renderer); // 初始化场景
  const camera = initCamera(5, 2, 10); // 初始化相机
  const controls = initControls(camera, renderer); // 初始化控制器
  controls.target.set(0, 0.5, 0); // 设置控制器的目标

  const dracoLoader = new DRACOLoader(); // 创建 Draco 加载器
  dracoLoader.setDecoderPath("jsm/libs/draco/gltf/"); // 设置 Draco 解码器路径

  const loader = new GLTFLoader(); // 创建 GLTF 加载器
  loader.setDRACOLoader(dracoLoader); // 为 GLTF 加载器设置 Draco 加载器

  // 加载 GLTF 模型
  loader.load(
    "models/gltf/LittlestTokyo.glb",
    (gltf: GLTF) => {
      const model = gltf.scene; // 获取模型的场景
      model.position.set(1, 1, 0); // 设置模型的位置
      model.scale.set(0.01, 0.01, 0.01); // 设置模型的缩放
      scene.add(model); // 将模型添加到场景

      mixerRef.current = new THREE.AnimationMixer(model); // 创建动画混合器
      mixerRef.current.clipAction(gltf.animations[0]).play(); // 播放动画
      renderer.setAnimationLoop(animate); // 设置渲染循环
    },
    undefined,
    (e) => {
      console.error(e); // 处理加载错误
    },
  );

  // 渲染循环函数
  /**
   * 每一帧的更新和渲染逻辑
   */
  function animate() {
    const delta = clock.getDelta(); // 获取时间间隔
    mixerRef.current && mixerRef.current.update(delta); // 更新动画混合器
    controls.update(); // 更新控制器
    statsRef.current && statsRef.current.update(); // 更新统计信息
    renderer.render(scene, camera); // 渲染场景和相机
  }

  // 处理窗口大小改变的函数
  /**
   * 处理窗口大小改变时的相机和渲染器更新
   */
  function onWindowResize() {
    camera.aspect = window.innerWidth / window.innerHeight; // 更新相机的宽高比
    camera.updateProjectionMatrix(); // 更新相机的投影矩阵
    controls.update(); // 更新控制器
    renderer.setSize(window.innerWidth, window.innerHeight); // 更新渲染器的大小
  }

  // 使用 useEffect 钩子
  useEffect(() => {
    if (!containerRef.current) return;
    containerRef.current.appendChild(renderer.domElement); // 将渲染器的 DOM 元素添加到引用的元素中

    statsRef.current = new Stats(); // 创建统计信息实例

    containerRef.current.appendChild(statsRef.current.dom); // 将统计信息的 DOM 元素添加到引用的元素中

    window.addEventListener("resize", onWindowResize); // 添加窗口大小改变的监听事件
    return () => {
      window.removeEventListener("resize", onWindowResize); // 清除窗口大小改变的监听事件
      renderer.setAnimationLoop(null); // 清除渲染循环
    };
  }, []);

  return <div ref={containerRef}></div>; // 返回一个带有 ref 的 div 元素
}

export default Keyframes; // 导出 Keyframes 组件
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明原文出处。如若内容造成侵权/违法违规/事实不符,请联系SD编程学习网:675289112@qq.com进行投诉反馈,一经查实,立即删除!