1.显示天空
models下新建文件Sky.jsx
Sky.jsx
// 从 React 库中导入 useRef 钩子,用于创建可变的 ref 对象
import { useRef } from "react";
// 从 @react-three/drei 库中导入 useGLTF 钩子,用于加载 GLTF 格式的 3D 模型
import { useGLTF } from "@react-three/drei";
// 从 @react-three/fiber 库中导入 useFrame 钩子,用于在每一帧渲染时执行代码
import { useFrame } from "@react-three/fiber";
// 导入天空 3D 模型的 GLB 文件
import skyScene from "../assets/3d/sky.glb";
// 3D 模型来源注释,表明模型来自 Sketchfab 网站
// 3D Model from: https://sketchfab.com/3d-models/phoenix-bird-844ba0cf144a413ea92c779f18912042
/**
* Sky 组件,用于渲染可旋转的天空 3D 模型
* @param {boolean} isRotating - 控制天空模型是否旋转的布尔值
* @returns {JSX.Element} 包含天空 3D 模型的 React 元素
*/
export function Sky({ isRotating }) {
// 使用 useGLTF 钩子加载天空 3D 模型,返回包含模型信息的对象
const sky = useGLTF(skyScene);
// 使用 useRef 钩子创建一个 ref 对象,用于引用天空模型的 DOM 元素
const skyRef = useRef();
// 注释说明动画名称可在 Sketchfab 网站找到,以及使用 delta 确保动画帧率独立
// Note: Animation names can be found on the Sketchfab website where the 3D model is hosted.
// It ensures smooth animations by making the rotation frame rate-independent.
// 'delta' represents the time in seconds since the last frame.
/**
* 在每一帧渲染时更新天空模型的旋转状态
*/
useFrame((_, delta) => {
// 如果 isRotating 为 true,则更新天空模型的 y 轴旋转角度
if (isRotating) {
// 调整旋转速度,delta 确保旋转速度不受帧率影响
skyRef.current.rotation.y += 0.25 * delta;
}
});
/**
* 渲染天空 3D 模型组件
* 返回一个包含天空 3D 模型的 mesh 元素
*/
return (
// 创建一个 mesh 元素,用于承载 3D 模型,并通过 ref 关联到 skyRef
<mesh ref={skyRef}>
{/*
使用 primitive 元素直接嵌入从 GLTF 文件加载的复杂 3D 模型或场景。
object 属性指定要嵌入的 3D 对象,这里使用从 GLTF 文件加载的场景。
*/}
<primitive object={sky.scene} />
</mesh>
);
}
models下新建文件index.jsx
index.jsx
export { Sky } from "./Sky";
export { Island } from "./Island";
删除Island.jsx末尾文件"export default Island"语句
在Home.jsx中添加Sky相关代码显示天空效果
Home.jsx
……
// 从 ../models/Island 路径导入 Island 组件,此组件用于渲染 3D 岛屿模型
import { Island, Sky } from "../models"
……
/**
* Home 组件,作为应用的主页组件。
* 该组件会依据屏幕尺寸对 Island 组件的缩放、位置和旋转进行调整,
* 并且在 Canvas 中渲染 Island 组件,同时处理异步加载状态。
* @returns {JSX.Element} 渲染后的 JSX 元素
*/
const Home = () => {
/**
* 根据屏幕尺寸调整 Island 组件的缩放、位置和旋转。
* @returns {Array} 包含屏幕缩放比例、位置和旋转值的数组
*/
// 定义一个状态变量 isRotating,用于控制 Sky 组件的旋转状态
const [isRotating, setIsRotating] = React.useState(false)
const adjustIslandForScreenSize = () => {
……
{/* 渲染 Island 组件,设置其位置、缩放和旋转属性 */}
<Island
position={islandPosition}
scale={islandScale}
rotation={islandRotation}
/>
{/* 渲染 Sky 组件,设置其旋转状态 */}
<Sky isRotating={isRotating}/>
……
效果图
2.飞翔的鸟
models下添加文件Bird.jsx
Bird.jsx
// 从 react 库中导入 useEffect 和 useRef 钩子
// useEffect 用于处理副作用,如数据获取、订阅等
// useRef 用于创建可变的 ref 对象,可在组件的整个生命周期内保持值
import { useEffect, useRef } from "react";
// 从 @react-three/fiber 库中导入 useFrame 钩子
// useFrame 用于在每一帧渲染时执行代码,常用于实现动画效果
import { useFrame } from "@react-three/fiber";
// 从 @react-three/drei 库中导入 useAnimations 和 useGLTF 钩子
// useGLTF 用于加载 GLTF 格式的 3D 模型
// useAnimations 用于管理 3D 模型的动画
import { useAnimations, useGLTF } from "@react-three/drei";
// 导入鸟类 3D 模型的 GLB 文件
import birdScene from "../assets/3d/bird.glb";
// 3D 模型来源注释,表明模型来自 Sketchfab 网站
// 3D Model from: https://sketchfab.com/3d-models/phoenix-bird-844ba0cf144a413ea92c779f18912042
/**
* Bird 组件,用于渲染会移动和播放动画的鸟类 3D 模型
* @returns {JSX.Element} 包含鸟类 3D 模型的 React 元素
*/
export function Bird() {
// 使用 useRef 钩子创建一个 ref 对象,用于引用鸟类 3D 模型的 DOM 元素
const birdRef = useRef();
// 使用 useGLTF 钩子加载鸟类 3D 模型和动画数据
// scene 为加载后的 3D 模型场景
// animations 为模型包含的动画数组
const { scene, animations } = useGLTF(birdScene);
// 使用 useAnimations 钩子获取动画动作对象
// actions 是一个包含动画动作的对象,可用于控制动画的播放、暂停等
const { actions } = useAnimations(animations, birdRef);
// 使用 useEffect 钩子,在组件挂载时执行一次
// 作用是播放名为 "Take 001" 的动画
useEffect(() => {
// 播放 "Take 001" 动画
actions["Take 001"].play();
}, []);
// 使用 useFrame 钩子,在每一帧渲染时执行代码
useFrame(({ clock, camera }) => {
// 使用正弦函数模拟鸟类上下波动的飞行效果
// clock.elapsedTime 表示从时钟启动到现在经过的时间
// 通过正弦函数计算出一个波动值,乘以 0.2 并加上 2,更新鸟类模型的 Y 坐标
birdRef.current.position.y = Math.sin(clock.elapsedTime) * 0.2 + 2;
// 检查鸟类模型的 X 坐标是否超过相机 X 坐标加 10 的位置
if (birdRef.current.position.x > camera.position.x + 10) {
// 若超过,将鸟类模型旋转 180 度(沿 Y 轴),改变飞行方向为向后
birdRef.current.rotation.y = Math.PI;
} else if (birdRef.current.position.x < camera.position.x - 10) {
// 若小于相机 X 坐标减 10 的位置,将鸟类模型旋转角度重置为 0,改变飞行方向为向前
birdRef.current.rotation.y = 0;
}
// 根据鸟类模型的旋转角度更新其 X 和 Z 坐标
// 如果旋转角度为 0,说明鸟类向前飞行
if (birdRef.current.rotation.y === 0) {
// 向前移动,X 坐标增加 0.01,Z 坐标减少 0.01
birdRef.current.position.x += 0.01;
birdRef.current.position.z -= 0.01;
} else {
// 向后移动,X 坐标减少 0.01,Z 坐标增加 0.01
birdRef.current.position.x -= 0.01;
birdRef.current.position.z += 0.01;
}
});
return (
// 创建一个 mesh 元素,用于承载 3D 模型
// ref 关联到 birdRef,方便后续操作模型
// position 设置模型的初始位置
// scale 设置模型的缩放比例
<mesh ref={birdRef} position={[-5, 2, 1]} scale={[0.003, 0.003, 0.003]}>
{/*
使用 primitive 元素直接嵌入从 GLTF 文件加载的复杂 3D 模型或场景
object 属性指定要嵌入的 3D 对象,这里使用从 GLTF 文件加载的场景
*/}
<primitive object={scene} />
</mesh>
);
}
index.jsx增加代码鸟相关代码,考虑到后面还有其他狐狸、飞机相关文件,这里统一一起添加了
index.jsx
export { Sky } from "./Sky";
export { Bird } from './Bird'
export { Plane } from "./Plane";
export { Island } from "./Island";
export { Fox } from "./Fox"
Home.jsx中增加代码
Home.jsx
……
// 从../models 路径导入组件,
import { Bird, Island, Sky } from "../models"
……
{/* 渲染 Bird 组件 */}
<Bird />
……
效果图
3.飞机
models下新建文件Plane.jsx
Plane.jsx
// 从 React 中导入 useEffect 和 useRef 钩子
// useEffect 用于处理副作用,如数据获取、订阅或手动修改 DOM 等
// useRef 用于创建可变的 ref 对象,可在组件的整个生命周期内保持值
import { useEffect, useRef } from "react";
// 从 @react-three/drei 库中导入 useGLTF 和 useAnimations 钩子
// useGLTF 用于加载 GLTF 格式的 3D 模型
// useAnimations 用于管理 3D 模型的动画
import { useGLTF, useAnimations } from "@react-three/drei";
// 导入飞机 3D 模型的 GLB 文件
import planeScene from "../assets/3d/plane.glb";
// 3D 模型来源注释,表明模型来自 Sketchfab 网站
// 3D Model from: https://sketchfab.com/3d-models/stylized-ww1-plane-c4edeb0e410f46e8a4db320879f0a1db
/**
* Plane 组件,用于渲染可控制动画的飞机 3D 模型
* @param {boolean} isRotating - 控制飞机动画播放的布尔值,true 播放,false 停止
* @param {Object} props - 其他传递给组件的属性
* @returns {JSX.Element} 包含飞机 3D 模型的 React 元素
*/
export function Plane({ isRotating, ...props }) {
// 使用 useRef 钩子创建一个 ref 对象,用于引用飞机 3D 模型的 DOM 元素
const ref = useRef();
// 使用 useGLTF 钩子加载飞机 3D 模型和动画数据
// scene 为加载后的 3D 模型场景
// animations 为模型包含的动画数组
const { scene, animations } = useGLTF(planeScene);
// 使用 useAnimations 钩子获取动画动作对象
// actions 是一个包含动画动作的对象,可用于控制动画的播放、暂停等
const { actions } = useAnimations(animations, ref);
// 使用 useEffect 钩子,当 isRotating 或 actions 变化时执行
// 作用是根据 isRotating 的值控制飞机动画的播放或停止
useEffect(() => {
if (isRotating) {
// 若 isRotating 为 true,播放名为 "Take 001" 的动画
actions["Take 001"].play();
} else {
// 若 isRotating 为 false,停止名为 "Take 001" 的动画
actions["Take 001"].stop();
}
}, [actions, isRotating]);
return (
// 创建一个 mesh 元素,用于承载 3D 模型
// 将传递给组件的其他属性展开到 mesh 元素上
// ref 关联到 ref 对象,方便后续操作模型
<mesh {...props} ref={ref}>
{/*
使用 primitive 元素直接嵌入从 GLTF 文件加载的复杂 3D 模型或场景
object 属性指定要嵌入的 3D 对象,这里使用从 GLTF 文件加载的场景
*/}
<primitive object={scene} />
</mesh>
);
}
Home.jsx
……
// 从../models 路径导入组件,
import { Bird, Island, Sky, Plane } from "../models"
……
/**
* 根据屏幕尺寸调整 Plane 组件(双翼飞机)的缩放和位置。
* @returns {Array} 包含飞机缩放比例和位置的数组
*/
const adjustBiplaneForScreenSize = () => {
let screenScale, screenPosition;
// If screen width is less than 768px, adjust the scale and position
if (window.innerWidth < 768) {
screenScale = [1.5, 1.5, 1.5];
screenPosition = [0, -1.5, 0];
} else {
screenScale = [3, 3, 3];
screenPosition = [0, -4, -4];
}
return [screenScale, screenPosition];
};
// 调用 adjustIslandForScreenSize 函数,获取调整后的岛屿缩放、位置和旋转参数
const [islandScale, islandPosition, islandRotation] = adjustIslandForScreenSize();
// 定义一个状态变量 currentFocusPoint,用于存储当前的焦点点
const [biplaneScale, biplanePosition] = adjustBiplaneForScreenSize();
……
<Bird />
{/* 渲染 Plane 组件 */}
{/* 渲染 Plane 组件,设置其旋转状态、位置、旋转和缩放属性 */}
<Plane
isRotating={isRotating}
position={biplanePosition}
rotation={[0, 20.1, 0]}
scale={biplaneScale}
/>
……
效果图
修复部分bug,得到Home.jsx完整代码
修复后Home.jsx
// 导入 React 库和 Suspense 组件,Suspense 用于处理异步组件加载
// 当异步组件还未加载完成时,可显示一个 fallback 组件
import React, { Suspense, useState } from 'react'
// 从 @react-three/fiber 库中导入 Canvas 组件,用于创建 Three.js 渲染上下文,
// 借助该组件能在 React 应用里渲染 3D 场景
import { Canvas } from '@react-three/fiber'
// 从 ../components/Loader 路径导入 Loader 组件,该组件会在异步加载时显示加载状态
import Loader from '../components/Loader'
// 从../models 路径导入组件,
import { Bird, Island, Sky, Plane } from "../models"
// <div className='absolute top-28 left-0 right-0 z-10 flex items-center justify-center'>
// 弹出窗口
// </div>
/**
* Home 组件,作为应用的主页组件。
* 该组件会依据屏幕尺寸对 Island 组件的缩放、位置和旋转进行调整,
* 并且在 Canvas 中渲染 Island 组件,同时处理异步加载状态。
* @returns {JSX.Element} 渲染后的 JSX 元素
*/
const Home = () => {
/**
* 根据屏幕尺寸调整 Island 组件的缩放、位置和旋转。
* @returns {Array} 包含屏幕缩放比例、位置和旋转值的数组
*/
// 定义当前阶段状态,初始值为 1
const [currentStage, setCurrentStage] = useState(1);
// 定义一个状态变量 isRotating,用于控制 Sky 组件和 Plane 组件的旋转状态
const [isRotating, setIsRotating] = React.useState(false)
/**
* 根据屏幕尺寸调整 Island 组件的缩放、位置和旋转。
* @returns {Array} 包含岛屿缩放比例、位置和旋转值的数组
*/
const adjustIslandForScreenSize = () => {
// 初始化屏幕缩放比例,初始值设为 null
let screenScale = null
// 初始化 Island 组件的位置,默认值为 [0, -6.5, -43]
let screenPosition = [0, -6.5, -43]
// 初始化 Island 组件的旋转值,默认值为 [0.1, 4.7, 0]
let rotation = [0.1, 4.7, 0]
// 判断当前窗口宽度是否小于 768px
if (window.innerWidth < 768) {
// 若窗口宽度小于 768px,将屏幕缩放比例设置为 [0.9, 0.9, 0.9]
screenScale = [0.9, 0.9, 0.9];
} else {
// 若窗口宽度大于等于 768px,将屏幕缩放比例设置为 [1, 1, 1]
screenScale = [1, 1, 1];
}
// 返回包含屏幕缩放比例、位置和旋转值的数组
return [screenScale, screenPosition, rotation];
}
/**
* 根据屏幕尺寸调整 Plane 组件(双翼飞机)的缩放和位置。
* @returns {Array} 包含飞机缩放比例和位置的数组
*/
const adjustBiplaneForScreenSize = () => {
let screenScale, screenPosition;
// 如果屏幕宽度小于 768px,调整缩放和位置
if (window.innerWidth < 768) {
screenScale = [1.5, 1.5, 1.5];
screenPosition = [0, -1.5, 0];
} else {
screenScale = [3, 3, 3];
screenPosition = [0, -4, -4];
}
return [screenScale, screenPosition];
};
// 调用 adjustIslandForScreenSize 函数,获取调整后的岛屿缩放、位置和旋转参数
const [islandScale, islandPosition, islandRotation] = adjustIslandForScreenSize();
// 调用 adjustBiplaneForScreenSize 函数,获取调整后的飞机缩放和位置参数
const [biplaneScale, biplanePosition] = adjustBiplaneForScreenSize();
return (
// 创建一个 section 元素,宽度和高度占满整个屏幕,且采用相对定位
<section className='w-full h-screen relative'>
{/* 创建 Three.js 渲染画布,宽度和高度占满整个屏幕,背景透明,
并设置相机的近裁剪面和远裁剪面 */}
<Canvas
className='w-full h-screen bg-transparent'
camera={{ near:0.1, far:1000 }}
>
{/* 使用 Suspense 组件处理异步加载,当 Island 组件未加载完成时,显示 Loader 组件 */}
<Suspense fallback={<Loader/>}>
{/* 添加定向光,为场景提供有方向的光照 */}
<directionalLight/>
{/* 添加环境光,为场景提供全局均匀的光照 */}
<ambientLight />
{/* 添加点光源,从一个点向四周发射光线 */}
<pointLight />
{/* 添加聚光灯,发射出类似圆锥形的光线 */}
<spotLight />
{/* 添加半球光,模拟天空和地面的光照效果 */}
<hemisphereLight />
{/* 渲染 Island 组件,传递相关状态和属性 */}
<Island
isRotating={isRotating}
setIsRotating={setIsRotating}
setCurrentStage={setCurrentStage}
position={islandPosition}
rotation={[0.1, 4.7077, 0]}
scale={islandScale}
/>
{/* 渲染 Sky 组件,设置其旋转状态 */}
<Sky isRotating={isRotating} />
{/* 渲染 Bird 组件 */}
<Bird />
{/* 渲染 Plane 组件,设置其旋转状态、位置、旋转和缩放属性 */}
<Plane
isRotating={isRotating}
position={biplanePosition}
rotation={[0, 20.1, 0]}
scale={biplaneScale}
/>
</Suspense>
</Canvas>
</section>
)
}
// 导出 Home 组件,供其他文件引入使用
export default Home