说明 模仿项目地址 ,参考的视频教程出处 ,react入门
react three fiber
本文是对Island.jsx及其相关文件的学习解读,内容中含ChatGPT辅助生成❗自辨❗❗❗
相关代码 Island.jsx 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 (); const { gl, viewport } = useThree (); const { nodes, materials } = useGLTF (isLandScene); 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 ; 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 ; const normalizeRotation = ((rotation % (2 * Math .PI )) + 2 * Math .PI ) % (2 * Math .PI ); 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对象(如 Group
、Mesh
等)用 a.
前缀包装,并通过 @react-spring/three
获得了动画能力。在Three.js中,Group
是一个用于包含和管理多个其他对象(例如,几何体、网格等)的容器。它本身是 Object3D
的一个子类,这意味着它继承了 Object3D
的所有属性,包括 position
、scale
和 rotation
。
useThree “@react-three/fiber” 在 @react-three/fiber
中,useThree
是一个 React Hook,它提供了访问 Three.js 渲染上下文中的各种属性和方法的能力。
当你在组件中调用 useThree()
时,它返回一个对象,这个对象包含了当前 Three.js 渲染上下文的多个属性和实例。这样,你就可以在你的组件中直接访问和使用这些属性和实例,而无需手动管理它们。
const { gl, viewport } = useThree ();
这行代码的作用是从 useThree()
返回的上下文对象中解构出 gl
和 viewport
两个属性:
**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 > ); }
在这个例子中,每一帧都会增加立方体的 x
和 y
轴旋转,delta
参数确保了旋转速度与帧率无关,提供了平滑一致的动画效果。
注意事项
使用 useFrame
时要考虑性能。因为回调函数会在每一帧被调用,避免在其中执行复杂的计算或状态更新,这可能会导致性能问题。
useFrame
是 @react-three/fiber
特有的,不是 React 官方 API 的一部分。它专门用于在 @react-three/fiber
提供的 <Canvas>
组件中使用,这个组件用于渲染 Three.js 的 3D 场景。
交互事件pointerdown 在Three.js中,虽然库本身主要聚焦于3D场景的渲染,但它通常用于Web环境,因此经常与HTML元素(如 canvas
)以及Web事件模型进行交互。pointerdown
、pointerup
和 pointermove
是指针事件,是Web标准的一部分,用于处理各种指针设备(如鼠标、触摸屏和笔设备)的输入。这些事件可以被用来增强Three.js场景的交互性。下面是每个事件的基本说明:
pointerdown
操作 :当用户按下任何指针设备(例如,鼠标按键、触摸屏的触摸)时触发。
用途 :在Three.js应用中,pointerdown
可以用来检测用户开始与场景中的对象进行交互的时刻,比如开始拖动物体、选中一个物体等。
pointerup
操作 :当用户释放之前按下的指针设备时触发。
用途 :在Three.js应用中,pointerup
事件可以用来检测用户结束交互的时刻,例如放开拖动的物体、确认选中的物体等。
pointermove
操作 :当指针设备在屏幕上移动时触发,不论是否按下。
用途 :pointermove
在Three.js中非常有用,可以用来实现对象的拖拽效果、在场景中导航(如旋转视角、缩放场景)或实时追踪鼠标/触摸的位置以创建动态效果。
为了在Three.js的 canvas
元素上监听这些事件,你可以直接在 canvas
元素上添加事件监听器:
const canvas = renderer.domElement ; 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.clientX
和 event.clientY
属性提供了指针在视口中的坐标位置,而 event.pageX
和 event.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,如 title
和 onClick
等,而这些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
),包括 isRotating
、setIsRotating
、setCurrentStage
以及其他可能的属性(如 position
、scale
、rotation
等, 这些属性对于Three.js中的对象来说是常见的变换属性)。在 Island
组件的函数签名中,...props
使用了JavaScript的展开运算符来收集除 isRotating
、setIsRotating
、setCurrentStage
之外的所有传递给组件的属性。
在 Island
组件内部,...props
的作用是将这些额外的属性传递给 <a.group>
组件。这样做有几个好处:
灵活性 :允许 Island
组件接收任何额外的属性,并将它们直接传递给内部的 <a.group>
组件,而无需组件显式地声明或处理这些属性。这使得 Island
组件更加灵活,可以适应不同的使用场景。
简洁性 :避免了需要显式地为每个可能的属性编写传递逻辑,从而使组件代码更加简洁和易于维护。
组件封装 :保持了 Island
组件对于其内部实现的封装性。调用者无需关心 Island
如何处理或转发这些属性,只需要知道它可以接收并适当地使用这些属性。
<a.group>
组件继承自Three.js的 Group
类,将Three.js对象(如 Group
、Mesh
等)用 a.
前缀包装,并通过 @react-spring/three
获得了动画能力。在Three.js中,Group
是一个用于包含和管理多个其他对象(例如,几何体、网格等)的容器。它本身是 Object3D
的一个子类,这意味着它继承了 Object3D
的所有属性,包括 position
、scale
和 rotation
。