react-three/fiber + gsap 滑板(2)跳跃效果

100 阅读4分钟

内容回顾

上一篇文章讲了如何解析模型,如何新增材质贴图,如何使用gsap对滑板轮子进行转动等操作,这篇文章讲一下跳跃旋转如何操作,其实本质上和轮胎旋转的操作是一样的,都是获取某个轴的数据进行旋转变动,文章中的代码注释可以多看看,可以更好的理解代码。

前置配置

  • @react-three/fiber@9.0.0-rc.1
  • @react-three/fiber
  • gsap@3.12.5
  • gltfjsx

效果图

图片.gif

圆标显示

简单来说,就是在滑板上面的位置新增一个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>

效果图

image.png

新增点击事件

在每一个圆点下创建一个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>

没有隐藏方体的效果图

image.png

豚跳的效果

豚跳效果动作分析

  1. 整体Y轴向上移动(Y轴为正), 形成跳跃效果
  2. 在跳跃效果在的同时, 板尾向下(X轴为负)板头向上(X轴为正)
  3. 结束后恢复平衡(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", // 入场缓动
      });
}

效果图

2.gif

板头板尾动作

/**
* 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" });
}

效果图

1.gif

尖翻效果

尖翻效果动作分析

  1. 整体Y轴向上移动(Y轴为正), 形成跳跃效果
  2. 在跳跃效果在的同时, 板尾向下(X轴为负)板头向上(X轴为正)
  3. 沿z轴的360度旋转 (Math.PI * 2)
  4. 最后恢复平衡
/**
* 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" });
}

效果图

1.gif

内转360效果

内转360效果动作分析

  1. 整体Y轴向上移动(Y轴为正), 形成跳跃效果
  2. 在跳跃效果在的同时, 板尾向下(X轴为负)板头向上(X轴为正)
  3. 沿z轴的360度旋转, 围绕Y轴的360度旋转
  4. 最后恢复平衡
/**
* 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" });
}

效果图

1.gif

补充代码

  1. board 滑板主要容器,用于平移和跳跃动画,并设置初始偏移
  2. 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的项目,初学者,请多包涵,谢谢!