内容回顾
在上一篇文章讲了如何解析模型,如何新增材质贴图,如何使用gsap对滑板轮子进行转动等操作,这篇文章讲一下跳跃旋转如何操作,其实本质上和轮胎旋转的操作是一样的,都是获取某个轴的数据进行旋转变动,文章中的代码注释可以多看看,可以更好的理解代码。
前置配置
- @react-three/fiber@9.0.0-rc.1
- @react-three/fiber
- gsap@3.12.5
- gltfjsx
效果图
圆标显示
简单来说,就是在滑板上面的位置新增一个Mesh,Mesh中包含了circleGeometry,然后在Mesh上面新增点击事件。
// 封装Hotspot
import { Billboard } from "@react-three/drei";
import { useRef } from "react";
import * as THREE from "three";
/**
* Hotspot 组件的 props 定义
*/
interface HotspotProps {
/** 点的位置,一个包含三个数字的数组 [x, y, z] */
position: [number, number, number];
/** 点是否可见 */
isVisible: boolean;
/** 点的颜色,可选,默认为 '#E6FC6A' */
color?: string;
}
/**
* Hotspot 组件用于在 3D 场景中显示一个可交互的点。
* 它包含一个内部的可见圆点和一个外部的交互区域。
* 当鼠标悬停在交互区域上时,鼠标光标会改变。
*/
export function Hotspot({
position,
isVisible,
color = "#E6FC6A",
}: HotspotProps) {
const hotspotRef = useRef<THREE.Mesh>(null);
return (
// Billboard 组件使内部元素始终朝向相机
<Billboard position={position} follow={true}>
{/* 内部可见的圆点 */}
<mesh ref={hotspotRef} visible={isVisible}>
<circleGeometry args={[0.02, 32]} />
<meshStandardMaterial color={color} transparent opacity={1} />
</mesh>
{/* 外部的交互区域,比可见圆点稍大 */}
<mesh
visible={isVisible}
// 鼠标悬停时改变光标样式为指针
onPointerOver={() => {
document.body.style.cursor = "pointer";
}}
// 鼠标移开时恢复默认光标样式
onPointerOut={() => {
document.body.style.cursor = "default";
}}
>
<circleGeometry args={[0.03, 32]} />
{/* 使用基础材质,不受光照影响 */}
<meshBasicMaterial color={color} />
</mesh>
</Billboard>
);
}
<group>
<Environment files={"/hdr/warehouse-256.hdr"} />
<group ref={originRef}>
<group ref={containerRef} position={[-0.25, 0, -0.635]}>
<group position={[0, -0.086, 0.635]}>
<Skateboard
wheelTextureURLs={[wheelTextureURL]}
wheelTextureURL={wheelTextureURL}
deckTextureURLs={[deckTextureURL]}
deckTextureURL={deckTextureURL}
truckColor={truckColor}
boltColor={boltColor}
constantWheelSpin
/>
<Hotspot
isVisible={true}
position={[0, 0.38, 1]}
color="#B8FC39"
/>
<Hotspot
isVisible={true}
position={[0, 0.33, 0]}
color="#FF7A51"
/>
<Hotspot
isVisible={true}
position={[0, 0.35, -0.9]}
color="#46ACFA"
/>
</group>
</group>
</group>
<ContactShadows opacity={0.6} position={[0, -0.08, 0]} />
</group>
效果图
新增点击事件
在每一个圆点下创建一个BoxGeometry,通过visible={false},进行隐藏,你可以认为这个方体就是滑板的表面,这就可以进行点击事件的触发。
<Hotspot
isVisible={!animating && showHotspot.front}
position={[0, 0.38, 1]}
color="#B8FC39"
/>
<mesh position={[0, 0.27, 0.9]} name="front" onClick={onClick}>
<boxGeometry args={[0.6, 0.2, 0.58]} />
<meshStandardMaterial visible={false} />
</mesh>
<Hotspot
isVisible={!animating && showHotspot.middle}
position={[0, 0.33, 0]}
color="#FF7A51"
/>
<mesh position={[0, 0.27, 0]} name="middle" onClick={onClick}>
<boxGeometry args={[0.6, 0.1, 1.2]} />
<meshStandardMaterial visible={false} />
</mesh>
<Hotspot
isVisible={!animating && showHotspot.back}
position={[0, 0.35, -0.9]}
color="#46ACFA"
/>
<mesh position={[0, 0.27, -0.9]} name="back" onClick={onClick}>
<boxGeometry args={[0.6, 0.2, 0.58]} />
<meshStandardMaterial visible={false} />
</mesh>
没有隐藏方体的效果图
豚跳的效果
豚跳效果动作分析
- 整体Y轴向上移动(Y轴为正), 形成跳跃效果
- 在跳跃效果在的同时, 板尾向下(X轴为负)板头向上(X轴为正)
- 结束后恢复平衡(X轴为0)
跳跃动作
const jumpBoard = (board: THREE.Group) => {
// 创建跳跃动画的时间线
// 1. 向上移动到0.8的高度
// 2. 然后落回原位(y=0)
gsap
.timeline()
.to(board.position, {
y: 0.8, // 跳跃高度
duration: 0.51,
ease: "power2.out", // 出场缓动
delay: 0.26, // 延迟0.26秒开始跳跃
})
.to(board.position, {
y: 0, // 回到原点
duration: 0.43,
ease: "power2.in", // 入场缓动
});
}
效果图
板头板尾动作
/**
* Ollie (豚跳) 动画
* @param board 滑板的 Group 对象
*/
const ollie = (board: THREE.Group) => {
jumpBoard(board); // 首先执行跳跃动画
// 创建一个Ollie动作的动画序列 (timeline)
// 1. 板尾先向上翘起(-0.6弧度)
// 2. 然后板头抬起(0.4弧度)
// 3. 最后恢复平衡(0弧度)
gsap
.timeline()
.to(board.rotation, { x: -0.6, duration: 0.26, ease: "none" })
.to(board.rotation, { x: 0.4, duration: 0.82, ease: "power2.in" })
.to(board.rotation, { x: 0, duration: 0.12, ease: "none" });
}
效果图
尖翻效果
尖翻效果动作分析
- 整体Y轴向上移动(Y轴为正), 形成跳跃效果
- 在跳跃效果在的同时, 板尾向下(X轴为负)板头向上(X轴为正)
- 沿z轴的360度旋转 (Math.PI * 2)
- 最后恢复平衡
/**
* Kickflip (尖翻) 动画
* @param board 滑板的 Group 对象
*/
const kickflip = (board: THREE.Group) => {
jumpBoard(board); // 首先执行跳跃动画
// 创建一个Kickflip动作的动画序列
// 结合了Ollie的上翘动作和沿z轴的360度旋转 (Math.PI * 2)
// 旋转动画在0.3秒后开始执行 (相对于时间线的起点)
gsap
.timeline()
.to(board.rotation, { x: -0.6, duration: 0.26, ease: "none" })
.to(board.rotation, { x: 0.4, duration: 0.82, ease: "power2.in" })
.to(
board.rotation,
{
z: `+=${Math.PI * 2}`, // 沿Z轴旋转360度
duration: 0.78,
ease: "none",
},
0.3 // 动画开始的偏移时间
)
.to(board.rotation, { x: 0, duration: 0.12, ease: "none" });
}
效果图
内转360效果
内转360效果动作分析
- 整体Y轴向上移动(Y轴为正), 形成跳跃效果
- 在跳跃效果在的同时, 板尾向下(X轴为负)板头向上(X轴为正)
- 沿z轴的360度旋转, 围绕Y轴的360度旋转
- 最后恢复平衡
/**
* Frontside 360 (内转360) 动画
* @param board 滑板的 Group 对象
* @param origin 滑板父容器的 Group 对象,用于整体旋转
*/
frontside360 = (board: THREE.Group, origin: THREE.Group) => {
jumpBoard(board); // 首先执行跳跃动画
// 创建一个Frontside 360动作的动画序列
// 结合了Ollie动作和整个滑板(origin)围绕Y轴的360度旋转
// 旋转动画在0.3秒后开始执行
gsap
.timeline()
.to(board.rotation, { x: -0.6, duration: 0.26, ease: "none" })
.to(board.rotation, { x: 0.4, duration: 0.82, ease: "power2.in" })
.to(
origin.rotation, // 对父容器进行旋转
{
y: `+=${Math.PI * 2}`, // 沿Y轴旋转360度
duration: 0.77,
ease: "none",
},
0.3
)
.to(board.rotation, { x: 0, duration: 0.14, ease: "none" });
}
效果图
补充代码
- board 滑板主要容器,用于平移和跳跃动画,并设置初始偏移
- origin 父容器,用于整体旋转动画
<group>
{/* 环境光和反射,使用 HDR 图片 */}
<Environment files={"/hdr/warehouse-256.hdr"} />
{/* 父容器,用于整体旋转动画 */}
<group ref={originRef}>
{/* 滑板主要容器,用于平移和跳跃动画,并设置初始偏移 */}
<group ref={containerRef} position={[-0.25, 0, -0.635]}>
{/* 内部group,用于统一调整滑板和热点的位置 */}
<group position={[0, -0.086, 0.635]}>
{/* Skateboard 模型组件 */}
<Skateboard/>
{/* 前部点 */}
<Hotspot
isVisible={true} // 根据动画状态和点状态决定是否可见
position={[0, 0.38, 1]} // 热点在滑板上的3D位置
color="#B8FC39" // 点颜色
/>
{/* 前部热点的点击触发区域 (不可见) */}
<mesh position={[0, 0.27, 0.9]} name="front" onClick={onClick}>
<boxGeometry args={[0.6, 0.2, 0.58]} />
<meshStandardMaterial visible={false} />
</mesh>
{/* 中部点 */}
<Hotspot
isVisible={true}
position={[0, 0.33, 0]}
color="#FF7A51"
/>
{/* 中部热点的点击触发区域 (不可见) */}
<mesh position={[0, 0.27, 0]} name="middle" onClick={onClick}>
<boxGeometry args={[0.6, 0.1, 1.2]} />
<meshStandardMaterial visible={false} />
</mesh>
{/* 后部点 */}
<Hotspot
isVisible={true}
position={[0, 0.35, -0.9]}
color="#46ACFA"
/>
{/* 后部热点的点击触发区域 (不可见) */}
<mesh position={[0, 0.27, -0.9]} name="back" onClick={onClick}>
<boxGeometry args={[0.6, 0.2, 0.58]} />
<meshStandardMaterial visible={false} />
</mesh>
</group>
</group>
</group>
{/* 滑板下方的接触阴影 */}
<ContactShadows opacity={0.6} position={[0, -0.08, 0]} />
</group>
总结
跳跃效果已经完成,有空,我把分支切出来后,上传到github或者gitee。后续会发点在网上学习到关于react-three/fiber的项目,初学者,请多包涵,谢谢!