说明

模仿项目地址参考的视频教程出处react入门

react three fiber

本文是对Island.jsx及其相关文件的学习解读,内容中含ChatGPT辅助生成❗自辨❗❗❗

相关代码

Island.jsx

/*
Auto-generated by: https://github.com/pmndrs/gltfjsx
Author: nimzu (https://sketchfab.com/nimzuk)
License: CC-BY-4.0 (http://creativecommons.org/licenses/by/4.0/)
Source: https://sketchfab.com/3d-models/foxs-islands-163b68e09fcc47618450150be7785907
Title: Fox's islands
*/

import { a } from '@react-spring/three';
import { useGLTF } from "@react-three/drei";
import { useFrame, useThree } from "@react-three/fiber";
import { useEffect, useRef } from "react";
import isLandScene from '../assets/3d/island.glb';


const Island = ({ isRotating, setIsRotating, setCurrentStage, ...props }) => {
const islandRef = useRef(); // 对应到了Three的Group, 用于控制整个模型(islandRef.current, 即island的gameobject)

const { gl, viewport } = useThree();// 获取Three的渲染器,视口
const { nodes, materials } = useGLTF(isLandScene); // 获取模型的nodes和materials

const lastX = useRef(0); // 用于记录上一次的鼠标位置。这个值在组件的重渲染之间保持不变,而且这些值的变化不会触发组件的重新渲染
const rotationSpeed = useRef(0); // 这个值在组件的重渲染之间保持不变,而且这些值的变化不会触发组件的重新渲染
const dampingFactor = 0.95; // 阻尼系数

const handlePointerDown = (e) => {
e.stopPropagation();
e.preventDefault();
setIsRotating(true);

const clientX = e.touches ? e.touches[0].clientX : e.clientX; //只需要考虑X方向的
lastX.current = clientX;

}

const handlePointerUp = (e) => {
e.stopPropagation();
e.preventDefault();
setIsRotating(false);
}

const handlePointerMove = (e) => {
e.stopPropagation();
e.preventDefault();

if (isRotating) {
const clientX = e.touches ? e.touches[0].clientX : e.clientX;

const delta = (clientX - lastX.current) / viewport.width;
islandRef.current.rotation.y += delta * 0.01 * Math.PI;
lastX.current = clientX;
rotationSpeed.current = delta * 0.01 * Math.PI;
}
}

const handleKeyDown = (e) => {
if (e.key === 'ArrowLeft') {
if (!isRotating) setIsRotating(true);
islandRef.current.rotation.y += 0.01 * Math.PI;
rotationSpeed.current = 0.0125;// 让旋转的效果更舒适些
} else if (e.key === 'ArrowRight') {
if (!isRotating) setIsRotating(true);
islandRef.current.rotation.y -= 0.01 * Math.PI;
rotationSpeed.current = -0.0125;// 让旋转的效果更舒适些

}
}

const handleKeyUp = (e) => {
if (e.key === 'ArrowLeft' || e.key === 'ArrowRight') {
setIsRotating(false);
}
}

useFrame(() => {
if (!isRotating) {
rotationSpeed.current *= dampingFactor; //每帧都会减小旋转速度

if (Math.abs(rotationSpeed.current) < 0.001) { //设定阈值后,停止旋转
rotationSpeed.current = 0;
}

islandRef.current.rotation.y += rotationSpeed.current;

} else {
const rotation = islandRef.current.rotation.y;

// Mormalize the rotation value to ensure it stays within the range[0, 2 * Math.PI].
// The goal is to ensure that the rotation value remains within a specific range to prevent potential issues with very large or negative rotation values.
// Here's a step-by-step explanation of what this code does:
// 1. rotation % (2 * Math.PI) calculates the remainder of the rotation value when divided by 2 * Math.PI.This essentially wraps the rotation value around once it reaches a full circle(360 degrees) so that it stays within the range of 0 to 2 * Math.PI.
// 2.(rotation % (2 * Math.PI)) + 2 * Math.PI adds 2 * Math.PI to the result from step 1. This is done to ensure that the value remains positive and within the range of 0 to 2 * Math.PI even if it was negative after the modulo operation in step 1.
// 3. Finally, ((rotation%(2*Math.PI))+2*Math.PI)%(2*Math.PI) applies another modulo operation to the value obtained in step 2. This step guarantees that the value always stays with the range of 0 to 2*Math.PI, which is equivalent to a full circle in radians.
const normalizeRotation = ((rotation % (2 * Math.PI)) + 2 * Math.PI) % (2 * Math.PI);

//set the current stage based on the island's orientation
switch (true) {
case normalizeRotation >= 5.45 && normalizeRotation <= 5.85:
setCurrentStage(4);
break;
case normalizeRotation >= 0.85 && normalizeRotation <= 1.3:
setCurrentStage(3);
break;
case normalizeRotation >= 2.4 && normalizeRotation <= 2.6:
setCurrentStage(2);
break;
case normalizeRotation >= 4.25 && normalizeRotation <= 4.75:
setCurrentStage(1);
break;
default:
setCurrentStage(null);
}
}
});


useEffect(() => {
const canvas = gl.domElement;
canvas.addEventListener('pointerdown', handlePointerDown);
canvas.addEventListener('pointerup', handlePointerUp);
canvas.addEventListener('pointermove', handlePointerMove);
document.addEventListener('keydown', handleKeyDown);
document.addEventListener('keyup', handleKeyUp);

return () => {
canvas.removeEventListener('pointerdown', handlePointerDown);
canvas.removeEventListener('pointerup', handlePointerUp);
canvas.removeEventListener('pointermove', handlePointerMove);
document.removeEventListener('keydown', handleKeyDown);
document.removeEventListener('keyup', handleKeyUp);
} // 返回的这个函数将在组件卸载或依赖项改变之前执行
}, [gl, handlePointerDown, handlePointerUp, handlePointerMove]); //只有当这三个变量改变时,里面函数才会执行

return (
<a.group ref={islandRef} {...props}>
<mesh
geometry={nodes.polySurface944_tree_body_0.geometry}
material={materials.PaletteMaterial001}
/>
<mesh
geometry={nodes.polySurface945_tree1_0.geometry}
material={materials.PaletteMaterial001}
/>
<mesh
geometry={nodes.polySurface946_tree2_0.geometry}
material={materials.PaletteMaterial001}
/>
<mesh
geometry={nodes.polySurface947_tree1_0.geometry}
material={materials.PaletteMaterial001}
/>
<mesh
geometry={nodes.polySurface948_tree_body_0.geometry}
material={materials.PaletteMaterial001}
/>
<mesh
geometry={nodes.polySurface949_tree_body_0.geometry}
material={materials.PaletteMaterial001}
/>
<mesh
geometry={nodes.pCube11_rocks1_0.geometry}
material={materials.PaletteMaterial001}
/>
</a.group>
);
}

export default Island;

Home.jsx

import { Canvas } from '@react-three/fiber'
import { Suspense, useEffect, useRef, useState } from 'react'
import HomeInfo from '../components/HomeInfo'
import Loader from '../components/Loader'
import Island from '../models/Island'
......


const Home = () => {
......
const [isRotating, setIsRotating] = useState(false);
const [currentStage, setCurrentStage] = useState(1);
......

const adjustIslandForScreenSize = () => {
let screenScale = null;
let screenPosition = [0, -6.5, -43];
let rotation = [0.1, 4.7, 0];

if (window.innerWidth < 768) {
screenScale = [0.9, 0.9, 0.9];

} else {
screenScale = [1, 1, 1];
}
return [screenScale, screenPosition, rotation];
}

const [islandScale, islandPosition, islandRotation] = adjustIslandForScreenSize();
......

return (
<section className='w-full h-screen relative'>
<div className="absolute top-28 left-0 right-0 z-10 flex items-center justify-center">
{currentStage && <HomeInfo currentStage={currentStage} />} </div>

<Canvas
className={`w-full h-screen bg-transparent ${isRotating ? 'cursor-grabbing' : 'cursor-grab'} `}
camera={{ near: 0.1, far: 1000 }}
>
{/* Suspense 用于解决加载组件时的白屏,可以显示其他的内容,而其他内容不允许使用 lazy加载 */}
<Suspense fallback={<Loader />}>
<directionalLight position={[1, 1, 1]} intensity={2} />
......
<Island
position={islandPosition}
scale={islandScale}
rotation={islandRotation}
isRotating={isRotating}
setIsRotating={setIsRotating}
setCurrentStage={setCurrentStage}
/>

</Suspense>
</Canvas>
......
</section >
)
}

export default Home

涉及知识点

Three

a in ‘@react-spring/three’

import { a } from '@react-spring/three';
......
return (
<a.group ref={islandRef} {...props}>
<mesh
geometry={nodes.polySurface944_tree_body_0.geometry}
material={materials.PaletteMaterial001}
/>
......
</a.group>

在这个上下文中,a很可能是 @react-spring/three库的命名导入,用于访问 react-spring提供的用于Three.js对象的动画化包装器。react-spring是一个流行的React库,用于创建流畅和自然的动画效果。@react-spring/three是专门为Three.js集成提供的,使得在Three.js环境中使用 react-spring变得简单。

在这段代码中:

  • **a.group**:这是 @react-spring/three提供的一个组件,它是对Three.js中 THREE.Group的动画化包装。在Three.js中,Group对象用于创建对象的集合,这样你可以作为一个单元来平移、旋转和缩放它们。在 react-spring/three中,a.group允许你对这个组应用动画,比如平移、旋转或透明度变化等。
  • **<mesh />**:这是一个Three.js中用于表示具有几何形状和材质的物体的组件。在这个示例中,mesh组件使用了 geometry(几何体)和 material(材质)两个属性,分别指定了物体的形状和表面处理。这不是 react-spring特有的,而是Three.js中的基本概念,但在这里它被放置在 a.group内部,表明你可以对整个组和组内的单个物体进行动画处理。

a是一个特殊的前缀,用于访问 react-spring/three提供的动画化组件。使用 react-spring/three,你可以给Three.js的对象添加流畅的物理基础动画,比如弹簧动画。react-spring的动画不仅限于简单的过渡,它支持从初始状态到结束状态的自然动画,包括反弹、停止等自然运动的效果。

<a.group>组件继承自Three.js的 Group类,将Three.js对象(如 GroupMesh等)用 a.前缀包装,并通过 @react-spring/three获得了动画能力。在Three.js中,Group是一个用于包含和管理多个其他对象(例如,几何体、网格等)的容器。它本身是 Object3D的一个子类,这意味着它继承了 Object3D的所有属性,包括 positionscalerotation

useThree “@react-three/fiber”

@react-three/fiber 中,useThree 是一个 React Hook,它提供了访问 Three.js 渲染上下文中的各种属性和方法的能力。

当你在组件中调用 useThree() 时,它返回一个对象,这个对象包含了当前 Three.js 渲染上下文的多个属性和实例。这样,你就可以在你的组件中直接访问和使用这些属性和实例,而无需手动管理它们。

const { gl, viewport } = useThree();

这行代码的作用是从 useThree() 返回的上下文对象中解构出 glviewport 两个属性:

  • **gl**:这是对 WebGLRenderer 的引用,即 Three.js 使用的 WebGL 渲染器实例。通过这个实例,你可以控制渲染过程,比如调整清除颜色、执行后处理等。
  • **viewport**:这包含了关于当前视口的信息,如视口的宽度和高度,以及一些用于将屏幕坐标转换为Three.js世界坐标的工具函数。这对于响应式设计和动态布局非常有用。

在Three.js中,WebGLRenderer是用来渲染场景(THREE.Scene)到一个Web页面上的canvas元素中。它使用WebGL API来绘制定义好的3D对象。

const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);

WebGLRenderer生成的canvas元素需要被添加到HTML文档中,这样渲染的结果才能显示给用户。

document.body.appendChild(renderer.domElement);
......
renderer.render(scene, camera);

useFrame()

useFrame 是一个来自 @react-three/fiber 的 React Hook,它允许你在 React 的函数组件中插入和使用渲染循环。在 3D 应用和游戏开发中,渲染循环(也称为动画循环)是核心概念之一,负责在每一帧更新场景、相机、物体状态等,以及执行渲染操作。

useFrame 接受一个回调函数作为参数,这个回调函数会在浏览器的动画帧循环中被不断调用。通常,这个回调函数接受两个参数:一个是渲染器的 state,另一个是渲染的 delta 时间(即从上一帧到当前帧的时间差,单位为秒)。

useFrame((state, delta) => {
// 在这里编写每帧要执行的操作
});

假设你想在一个三维场景中旋转一个立方体,你可以使用 useFrame 来更新立方体的旋转状态:

import { useRef } from 'react';
import { useFrame } from '@react-three/fiber';
import { Mesh } from 'three';

export function RotatingBox() {
const ref = useRef<Mesh>();

useFrame((state, delta) => {
if (ref.current) {
ref.current.rotation.x += delta;
ref.current.rotation.y += delta;
}
});

return (
<mesh ref={ref}>
<boxGeometry args={[1, 1, 1]} />
<meshStandardMaterial color="orange" />
</mesh>
);
}

在这个例子中,每一帧都会增加立方体的 xy 轴旋转,delta 参数确保了旋转速度与帧率无关,提供了平滑一致的动画效果。

注意事项

  • 使用 useFrame 时要考虑性能。因为回调函数会在每一帧被调用,避免在其中执行复杂的计算或状态更新,这可能会导致性能问题。
  • useFrame@react-three/fiber 特有的,不是 React 官方 API 的一部分。它专门用于在 @react-three/fiber 提供的 <Canvas> 组件中使用,这个组件用于渲染 Three.js 的 3D 场景。

交互事件pointerdown

在Three.js中,虽然库本身主要聚焦于3D场景的渲染,但它通常用于Web环境,因此经常与HTML元素(如 canvas)以及Web事件模型进行交互。pointerdownpointeruppointermove是指针事件,是Web标准的一部分,用于处理各种指针设备(如鼠标、触摸屏和笔设备)的输入。这些事件可以被用来增强Three.js场景的交互性。下面是每个事件的基本说明:

pointerdown

  • 操作:当用户按下任何指针设备(例如,鼠标按键、触摸屏的触摸)时触发。
  • 用途:在Three.js应用中,pointerdown可以用来检测用户开始与场景中的对象进行交互的时刻,比如开始拖动物体、选中一个物体等。

pointerup

  • 操作:当用户释放之前按下的指针设备时触发。
  • 用途:在Three.js应用中,pointerup事件可以用来检测用户结束交互的时刻,例如放开拖动的物体、确认选中的物体等。

pointermove

  • 操作:当指针设备在屏幕上移动时触发,不论是否按下。
  • 用途pointermove在Three.js中非常有用,可以用来实现对象的拖拽效果、在场景中导航(如旋转视角、缩放场景)或实时追踪鼠标/触摸的位置以创建动态效果。

为了在Three.js的 canvas元素上监听这些事件,你可以直接在 canvas元素上添加事件监听器:

const canvas = renderer.domElement; // 假设你已经有一个Three.js渲染器

canvas.addEventListener('pointerdown', (event) => {
console.log('Pointer down', event);
// 在这里添加当指针按下时的处理逻辑
console.log(`Pointer Type: ${event.pointerType}`);
console.log(`Pointer Position: (${event.clientX}, ${event.clientY})`);
// 阻止默认行为和事件冒泡
event.preventDefault();
event.stopPropagation();
});

event对象的作用

  • 事件类型识别event.type属性可以告诉你事件的具体类型(如 "pointerdown"),有助于在使用同一个事件处理函数处理多种事件类型时进行区分。
  • 获取指针位置event.clientXevent.clientY属性提供了指针在视口中的坐标位置,而 event.pageXevent.pageY则提供了指针在整个页面中的位置。这对于实现拖拽功能或在画布上绘图等功能非常有用。
  • 区分指针设备event.pointerType属性可以告诉你触发事件的指针设备类型(如 "mouse""pen""touch"),这有助于实现针对不同设备的特定交互响应。
  • 阻止默认行为event.preventDefault()方法可以用来阻止事件的默认行为(如果有的话),例如阻止点击链接导航到新页面的默认行为。
  • 停止事件冒泡event.stopPropagation()方法可以阻止事件进一步传播到其他事件监听器。

注意事项

  • 使用指针事件可以让你的应用更好地适应不同的输入设备,而不仅仅是鼠标。
  • 确保合理使用事件监听器,避免在高频事件(如 pointermove)中执行计算量大的操作,这可能会影响性能。
  • 在处理这些事件时,考虑事件对象(event)提供的信息,如指针位置、按下的按钮等,这些信息对于实现精确的交互逻辑非常有用。

React

useState

useState 是React的一个Hook,它允许你在函数组件中添加状态。在React 16.8之前,函数组件被称为无状态组件,意味着你不能在其中使用状态(state)或生命周期方法。useState的引入改变了这一点,让函数组件也能够拥有和类组件一样的状态管理能力。

useState的基本用法非常简单。它接受一个参数,这个参数是状态的初始值,然后返回一个数组,这个数组包含两个元素:当前的状态值和一个更新这个状态值的函数。

import React, { useState } from 'react';

function Example() {
 // 声明一个新的状态变量,我们将其称之为 "count"
 const [count, setCount] = useState(0);

 return (
   <div>
     <p>You clicked {count} times</p>
     <button onClick={() => setCount(count + 1)}>
      Click me
     </button>
   </div>
);
}

在这个例子中,useState(0)声明了一个新的状态变量 count,并将其初始值设为0。setCount是一个函数,用于更新 count的值。当你点击按钮时,调用 setCount(count + 1)来增加 count的值。

你可以在一个组件中多次调用 useState,以声明多个状态变量。

import React, { useState } from 'react';

function ExampleWithManyStates() {
 // 声明多个状态变量
 const [age, setAge] = useState(42);
 const [fruit, setFruit] = useState('banana');
 const [todos, setTodos] = useState([{ text: '学习 Hook' }]);

 // ...
}

函数式更新:如果新的状态依赖于前一个状态,你可以给更新函数传入一个函数,这个函数将接收前一个状态作为参数,并返回一个新状态。

function Counter() {
 const [count, setCount] = useState(0);

 return (
   <div>
     <p>The count is {count}</p>
     <button onClick={() => setCount(prevCount => prevCount + 1)}>+1</button>
     <button onClick={() => setCount(prevCount => prevCount - 1)}>-1</button>
   </div>
);
}

惰性初始状态

useState允许你传入一个函数来延迟计算初始状态,这对于初始状态计算开销较大的情况非常有用。

const [state, setState] = useState(() => {
 const initialState = someExpensiveComputation(props);
 return initialState;
});

在这个例子中,someExpensiveComputation只会在组件初次渲染时被调用,从而避免了在每次渲染时都重新计算初始状态的开销。

通过 useState,React为函数组件提供了状态管理的能力,使得开发者能够在不使用类组件的情况下,以一种简洁且易于理解的方式构建动态且响应式的用户界面。

useRef

useRef 是React的一个Hook,它用于在函数组件中存储一个可变的引用对象,这个对象在组件的整个生命周期内保持不变。useRef最常见的用途有两个:一是访问DOM节点,二是存储任何可变值。

访问DOM节点

当你需要直接操作DOM元素时,比如设置焦点、测量元素大小或位置等,useRef可以用来获取DOM节点的引用。

import React, { useRef, useEffect } from 'react';

function TextInputWithFocusButton() {
 // 初始化一个ref
 const inputEl = useRef(null);

 const onButtonClick = () => {
   // 当按钮被点击时,使用current属性访问DOM节点,并调用focus方法
   inputEl.current.focus();
};

 return (
   <>
    {/* 使用ref属性将inputEl ref附加到输入元素上 */}
     <input ref={inputEl} type="text" />
     <button onClick={onButtonClick}>Focus the input</button>
   </>
);
}

存储任何可变值

useRef也可以用来存储任何可变值,这个值在组件的重渲染之间保持不变。这对于存储任意值,如计时器ID、外部库的实例等特别有用,而且这些值的变化不会触发组件的重新渲染

import React, { useRef, useEffect } from 'react';

function TimerComponent() {
 const intervalId = useRef(null);

 useEffect(() => {
   intervalId.current = setInterval(() => {
     console.log('Timer tick');
  }, 1000);

   return () => {
     clearInterval(intervalId.current);
  };
}, []);

 return <div>Check the console for timer ticks.</div>;
}

在这个例子中,intervalId用于存储计时器ID,以便可以在组件卸载时清除计时器,防止内存泄露。

useRef vs. useState

虽然 useState也可以用于存储数据,但与 useRef相比,当状态更新时,useState会触发组件的重新渲染,而 useRef中的变化不会。因此,如果你需要在组件的多次渲染之间保持不变的数据,且这些数据的变化不应该触发组件的重新渲染,useRef是更合适的选择。

总结

useRef在React函数组件中提供了一种简单的方式来访问DOM节点和存储可变数据。通过 useRef,你可以在组件的整个生命周期内保持对某个值的引用,而不会引起额外的渲染。这使得 useRef成为在需要直接操作DOM或需要跨渲染周期保持数据不变时的理想选择。

useEffect

useEffect 是 React 的一个 Hook,它允许你在函数组件中执行副作用操作。副作用可以包括数据获取、订阅或手动修改 DOM 等操作,这些操作通常在组件渲染到屏幕之后执行。useEffect 的用法提供了一个优雅的方式来处理这些操作。useEffect 提供了一种在函数组件中处理生命周期事件的灵活方式,使得开发者能够更容易地管理副作用和资源。

useEffect 接收两个参数:一个是副作用函数,另一个是依赖数组。

useEffect(() => {
 // 在这里执行副作用操作
}, [/* 依赖项列表 */]);

没有依赖的 useEffect

如果 b 的依赖项列表为空([]),副作用函数只会在组件挂载(mount)后执行一次。

useEffect(() => {
 // 只会在组件挂载后执行一次
}, []);

带有依赖的 useEffect

当你在依赖项列表中指定变量时,只有当这些变量改变时,副作用函数才会执行。这提供了一种方式来优化性能,避免不必要的副作用操作。

const [count, setCount] = useState(0);

useEffect(() => {
 // 当 `count` 变化时,这个副作用就会执行
 document.title = `You clicked ${count} times`;
}, [count]); // 依赖项列表中包含 `count`

清理副作用

有时候,你可能需要在组件卸载(unmount)或下一次副作用执行之前执行一些清理操作,比如取消订阅或清除定时器等。为此,你的副作用函数可以返回一个清理函数。

useEffect(() => {
 const id = setInterval(() => {
   // 执行一些重复的操作
}, 1000);

 // 返回的这个函数将在组件卸载或依赖项改变之前执行
 return () => clearInterval(id);
}, [/* 依赖项 */]);

没有依赖项的 useEffect

**如果 **useEffect 被调用时没有提供依赖项列表,副作用函数将在组件每次渲染后执行。这通常不推荐,因为它可能会导致性能问题。

useEffect(() => {
 // 每次组件渲染后都会执行
});

…props

在JSX中,...props是一个使用了JavaScript的展开运算符(Spread Operator)的表达式,它用于将一个对象的所有可枚举属性,复制到当前对象中。在React组件中,这种语法经常用来传递 props(属性)。

当你在JSX标签中使用 ...props时,你实际上是将一个包含多个属性的对象“展开”,并将这些属性作为单独的props传递给组件。这样做的好处是可以保持组件接口的灵活性,同时减少了代码的冗余。

假设你有一个组件 <MyComponent />,你想传递给它多个props,如 titleonClick等,而这些props是通过一个对象 props管理的,如下所示:

const props = {
 title: 'Hello World',
 onClick: () => console.log('Clicked'),
 // 更多props...
};

在不使用展开运算符的情况下,你需要逐一传递每个属性:

<MyComponent title={props.title} onClick={props.onClick} />

使用展开运算符,上面的代码可以简化为:

<MyComponent {...props} />

这行代码的作用是将 props对象中的每个属性都作为单独的prop传递给 MyComponent组件。这不仅使代码更简洁,还使得 MyComponent组件能够接收任何通过 props对象传递的额外属性,而无需每次都显式声明它们。

虽然使用展开运算符可以提高代码的灵活性和可读性,但在某些情况下也需要谨慎使用,因为:

  • 性能影响:如果对象很大,或这种操作在一个大型应用中频繁执行,它可能会对性能产生影响。
  • 属性覆盖:如果存在多个相同的属性名,后面的属性会覆盖前面的属性。在使用展开运算符时,需要注意属性的顺序和重复性。
  • 类型检查:在TypeScript等静态类型检查的环境中,使用展开运算符传递props时,可能需要确保传递的对象属性与组件的props类型兼容。
调用为:
<Island
position={islandPosition}
scale={islandScale}
rotation={islandRotation}
isRotating={isRotating}
setIsRotating={setIsRotating}
setCurrentStage={setCurrentStage}
/>

内容为:
const Island = ({ isRotating, setIsRotating, setCurrentStage, ...props }) => {
   ......
   return (
       <a.group ref={islandRef} {...props}>
           <mesh
               geometry={nodes.polySurface944_tree_body_0.geometry}
               material={materials.PaletteMaterial001}
               />
          ......
  )}

在这个示例中,<Island />组件接收了多个属性(props),包括 isRotatingsetIsRotatingsetCurrentStage以及其他可能的属性(如 positionscalerotation等, 这些属性对于Three.js中的对象来说是常见的变换属性)。在 Island组件的函数签名中,...props使用了JavaScript的展开运算符来收集除 isRotatingsetIsRotatingsetCurrentStage之外的所有传递给组件的属性。

Island组件内部,...props的作用是将这些额外的属性传递给 <a.group>组件。这样做有几个好处:

  1. 灵活性:允许 Island组件接收任何额外的属性,并将它们直接传递给内部的 <a.group>组件,而无需组件显式地声明或处理这些属性。这使得 Island组件更加灵活,可以适应不同的使用场景。
  2. 简洁性:避免了需要显式地为每个可能的属性编写传递逻辑,从而使组件代码更加简洁和易于维护。
  3. 组件封装:保持了 Island组件对于其内部实现的封装性。调用者无需关心 Island如何处理或转发这些属性,只需要知道它可以接收并适当地使用这些属性。

<a.group>组件继承自Three.js的 Group类,将Three.js对象(如 GroupMesh等)用 a.前缀包装,并通过 @react-spring/three获得了动画能力。在Three.js中,Group是一个用于包含和管理多个其他对象(例如,几何体、网格等)的容器。它本身是 Object3D的一个子类,这意味着它继承了 Object3D的所有属性,包括 positionscalerotation