react-three/fiber + gsap 滑板页面(1)

178 阅读6分钟

前置

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

效果图

图片.gif

使用gltfjsx模型转为tsx

  • gltfjsx
    自动将 .gltf/.glb 文件转换为 可复用的 React 组件,生成场景图结构(如 <mesh>、 层级)、材质和几何体的 JSX 代码,省去手动解析模型的时间。
  • 传统方法
    需手动编写加载代码,遍历模型节点,处理材质、几何体、动画等,代码量更大且易出错。
# 模型转化
gltfjsx model.gltf --typescript
# 或指定输出文件名
gltfjsx model.gltf -o Model.tsx --typescript
/*
Auto-generated by: https://github.com/pmndrs/gltfjsx
Command: npx gltfjsx@6.5.3 ./public/skateboard.gltf --typescript 
*/

import React from 'react'
import { useGLTF } from '@react-three/drei'
import * as THREE from 'three'
import { GLTF } from 'three-stdlib'

// 定义GLTFResult类型,参考Skateboard.tsx
type GLTFResult = GLTF & {
  nodes: {
    GripTape: THREE.Mesh
    Wheel1: THREE.Mesh
    Wheel2: THREE.Mesh
    Deck: THREE.Mesh
    Wheel4: THREE.Mesh
    Bolts: THREE.Mesh
    Wheel3: THREE.Mesh
    Baseplates: THREE.Mesh
    Truck1: THREE.Mesh
    Truck2: THREE.Mesh
  }
  materials: any
}

export function Skateboard(props: React.ComponentProps<'group'>) {
  const { nodes, materials } = useGLTF('/skateboard.gltf') as unknown as GLTFResult
  return (
    <group {...props} dispose={null}>
      <mesh geometry={nodes.GripTape.geometry} material={nodes.GripTape.material} position={[0, 0.286, -0.002]} />
      <mesh geometry={nodes.Wheel1.geometry} material={nodes.Wheel1.material} position={[0.238, 0.086, 0.635]} />
      <mesh geometry={nodes.Wheel2.geometry} material={nodes.Wheel2.material} position={[-0.237, 0.086, 0.635]} />
      <mesh geometry={nodes.Deck.geometry} material={nodes.Deck.material} position={[0, 0.271, -0.002]} />
      <mesh geometry={nodes.Wheel4.geometry} material={nodes.Wheel4.material} position={[-0.238, 0.086, -0.635]} rotation={[Math.PI, 0, Math.PI]} />
      <mesh geometry={nodes.Bolts.geometry} material={nodes.Bolts.material} position={[0, 0.198, 0]} rotation={[Math.PI, 0, Math.PI]} />
      <mesh geometry={nodes.Wheel3.geometry} material={nodes.Wheel3.material} position={[0.237, 0.086, -0.635]} rotation={[Math.PI, 0, Math.PI]} />
      <mesh geometry={nodes.Baseplates.geometry} material={nodes.Baseplates.material} position={[0, 0.211, 0]} />
      <mesh geometry={nodes.Truck1.geometry} material={nodes.Truck1.material} position={[0, 0.101, -0.617]} />
      <mesh geometry={nodes.Truck2.geometry} material={nodes.Truck2.material} position={[0, 0.101, 0.617]} rotation={[Math.PI, 0, Math.PI]} />
    </group>
  )
}

useGLTF.preload('/skateboard.gltf')

转化后的效果图

image.png

给模型上环境+阴影

这里使用的是Environment,ContactShadows 进行渲染

Environment:drei.docs.pmnd.rs/staging/env…

ContactShadows:drei.docs.pmnd.rs/staging/con…

"use client";

import * as THREE from "three";
import { Skateboard } from "../Skateboard-new";
import { ContactShadows, Environment } from "@react-three/drei";
import { Canvas, } from "@react-three/fiber";
import { Suspense, useRef } from "react";

const INITIAL_CAMERA_POSITION = [1.5, 1, 1.4] as const;

type Props = {};

export function InteractiveSkateboard({
  deckTextureURL,
  wheelTextureURL,
  truckColor,
  boltColor,
}: Props) {
  return (
    <div className="absolute inset-0 z-10 flex items-center justify-center">
      <Canvas
        className="min-h-[60rem] w-full"
        camera={{ position: INITIAL_CAMERA_POSITION, fov: 55 }}
      >
        <Suspense>
          <Scene />
        </Suspense>
      </Canvas>
    </div>
  );
}

function Scene({
}: Props) {
  const containerRef = useRef<THREE.Group>(null);
  const originRef = useRef<THREE.Group>(null);

  return (
    <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/>
          </group>
        </group>
      </group>
      <ContactShadows opacity={0.6} position={[0, -0.08, 0]} />
    </group>
  );
}

效果图

image.png

模型上贴图

给轮子上贴图

    
  const wheelTexture = useTexture('/skateboard/SkateWheel1.png')
  wheelTexture.flipY = false // 翻转y轴
  wheelTexture.colorSpace = THREE.SRGBColorSpace // 设置颜色空间  

  const wheelMaterial = useMemo(
    () =>
      new THREE.MeshStandardMaterial({
        map: wheelTexture,
        roughness: 0.35, // 粗糙度
      }),
    [wheelTexture]
  );
<group {...props} dispose={null}>
    <mesh geometry={nodes.GripTape.geometry} material={nodes.GripTape.material} position={[0, 0.286, -0.002]} />
    <mesh geometry={nodes.Wheel1.geometry} material={wheelMaterial} position={[0.238, 0.086, 0.635]} />
    <mesh geometry={nodes.Wheel2.geometry} material={wheelMaterial} position={[-0.237, 0.086, 0.635]} />
    <mesh geometry={nodes.Deck.geometry} material={nodes.Deck.material} position={[0, 0.271, -0.002]} />
    <mesh geometry={nodes.Wheel4.geometry} material={wheelMaterial} position={[-0.238, 0.086, -0.635]} rotation={[Math.PI, 0, Math.PI]} />
    <mesh geometry={nodes.Bolts.geometry} material={nodes.Bolts.material} position={[0, 0.198, 0]} rotation={[Math.PI, 0, Math.PI]} />
    <mesh geometry={nodes.Wheel3.geometry} material={wheelMaterial} position={[0.237, 0.086, -0.635]} rotation={[Math.PI, 0, Math.PI]} />
    <mesh geometry={nodes.Baseplates.geometry} material={nodes.Baseplates.material} position={[0, 0.211, 0]} />
    <mesh geometry={nodes.Truck1.geometry} material={nodes.Truck1.material} position={[0, 0.101, -0.617]} />
    <mesh geometry={nodes.Truck2.geometry} material={nodes.Truck2.material} position={[0, 0.101, 0.617]} rotation={[Math.PI, 0, Math.PI]} />
</group>

使用wheelMaterial 代替 nodes.Wheel2.material,这就展示了模型转化为tsx的好处了,不需要在代码里写很多的if判断进行材质处理。

同理,板面,板底,支撑等都可以使用这样的方法进行材质贴图

/*
Auto-generated by: https://github.com/pmndrs/gltfjsx
Command: npx gltfjsx@6.5.3 ./public/skateboard.gltf --typescript 
*/

import React, { useMemo } from 'react'
import { useGLTF, useTexture } from '@react-three/drei'
import * as THREE from 'three'
import { GLTF } from 'three-stdlib'

// 定义GLTFResult类型,参考Skateboard.tsx
type GLTFResult = GLTF & {
  nodes: {
    GripTape: THREE.Mesh
    Wheel1: THREE.Mesh
    Wheel2: THREE.Mesh
    Deck: THREE.Mesh
    Wheel4: THREE.Mesh
    Bolts: THREE.Mesh
    Wheel3: THREE.Mesh
    Baseplates: THREE.Mesh
    Truck1: THREE.Mesh
    Truck2: THREE.Mesh
  }
  materials: any
}

export function Skateboard(props: React.ComponentProps<'group'>) {
  const { nodes, materials } = useGLTF('/skateboard.gltf') as unknown as GLTFResult

  const wheelTexture = useTexture('/skateboard/SkateWheel1.png')
  wheelTexture.flipY = false // 翻转y轴
  wheelTexture.colorSpace = THREE.SRGBColorSpace // 设置颜色空间  

  const wheelMaterial = useMemo(
    () =>
      new THREE.MeshStandardMaterial({
        map: wheelTexture,
        roughness: 0.35,
      }),
    [wheelTexture]
  );

  // 加载砂纸漫反射和粗糙度贴图
  const gripTapeDiffuse = useTexture("/skateboard/griptape-diffuse.webp");
  const gripTapeRoughness = useTexture("/skateboard/griptape-roughness.webp");

  const gripTapeMaterial = useMemo(() => {
    const material = new THREE.MeshStandardMaterial({
      map: gripTapeDiffuse, // 漫反射贴图
      bumpMap: gripTapeRoughness, // 凹凸贴图
      roughnessMap: gripTapeRoughness, // 粗糙度贴图
      bumpScale: 3.5, // 凹凸强度
      roughness: 0.8, // 粗糙度
      color: "#555555", // 基础色
    });

    // 设置砂纸漫反射贴图在水平方向重复模式为RepeatWrapping(重复平铺)
    gripTapeDiffuse.wrapS = THREE.RepeatWrapping;
    // 设置砂纸漫反射贴图在垂直方向重复模式为RepeatWrapping(重复平铺)
    gripTapeDiffuse.wrapT = THREE.RepeatWrapping;
    // 设置砂纸漫反射贴图在两个方向上各重复9次,增加细节
    gripTapeDiffuse.repeat.set(9, 9);
    // 标记贴图需要更新,否则Three.js不会立即应用新设置
    gripTapeDiffuse.needsUpdate = true;

    // 设置砂纸粗糙度贴图在水平方向重复模式为RepeatWrapping(重复平铺)
    gripTapeRoughness.wrapS = THREE.RepeatWrapping;
    // 设置砂纸粗糙度贴图在垂直方向重复模式为RepeatWrapping(重复平铺)
    gripTapeRoughness.wrapT = THREE.RepeatWrapping;
    // 设置砂纸粗糙度贴图在两个方向上各重复9次,保证与漫反射贴图一致
    gripTapeRoughness.repeat.set(9, 9);
    // 标记贴图需要更新,否则Three.js不会立即应用新设置
    gripTapeRoughness.needsUpdate = true;

    // 提高各向异性过滤等级,使贴图在斜视角下更清晰
    gripTapeRoughness.anisotropy = 8;

    return material;
  }, [gripTapeDiffuse, gripTapeRoughness]);

    // ------------------- 螺丝材质 -------------------
    const boltColor = "#6F6E6A";
  // 螺丝材质,颜色可自定义
  const boltMaterial = useMemo(
    () =>
      new THREE.MeshStandardMaterial({
        color: boltColor,
        metalness: 0.5, // 金属度
        roughness: 0.3, // 粗糙度
      }),
    [boltColor]
  );


  const truckColor = "#6F6E6A";
  const metalNormal = useTexture("/skateboard/metal-normal.avif");
  metalNormal.wrapS = THREE.RepeatWrapping;
  metalNormal.wrapT = THREE.RepeatWrapping;
  metalNormal.anisotropy = 8;
  metalNormal.repeat.set(8, 8);

  // 卡车材质,颜色和法线贴图可自定义
  const truckMaterial = useMemo(
    () =>
      new THREE.MeshStandardMaterial({
        color: truckColor,
        normalMap: metalNormal, // 法线贴图
        normalScale: new THREE.Vector2(0.3, 0.3), // 法线强度
        metalness: 0.8,
        roughness: 0.25,
      }),
    [truckColor, metalNormal]
  );

  const deckTexture = useTexture("/skateboard/Deck.webp");
  const deckMaterial = useMemo(
    () =>
      new THREE.MeshStandardMaterial({
        map: deckTexture,
        roughness: 0.1,
      }),
    [deckTexture]
  );

  return (
    <group {...props} dispose={null}>
      <mesh geometry={nodes.GripTape.geometry} material={gripTapeMaterial} position={[0, 0.286, -0.002]} />
      <mesh geometry={nodes.Wheel1.geometry} material={wheelMaterial} position={[0.238, 0.086, 0.635]} />
      <mesh geometry={nodes.Wheel2.geometry} material={wheelMaterial} position={[-0.237, 0.086, 0.635]} />
      <mesh geometry={nodes.Deck.geometry} material={deckMaterial} position={[0, 0.271, -0.002]} />
      <mesh geometry={nodes.Wheel4.geometry} material={wheelMaterial} position={[-0.238, 0.086, -0.635]} rotation={[Math.PI, 0, Math.PI]} />
      <mesh geometry={nodes.Bolts.geometry} material={boltMaterial} position={[0, 0.198, 0]} rotation={[Math.PI, 0, Math.PI]} />
      <mesh geometry={nodes.Wheel3.geometry} material={wheelMaterial} position={[0.237, 0.086, -0.635]} rotation={[Math.PI, 0, Math.PI]} />
      <mesh geometry={nodes.Baseplates.geometry} material={truckMaterial} position={[0, 0.211, 0]} />
      <mesh geometry={nodes.Truck1.geometry} material={truckMaterial} position={[0, 0.101, -0.617]} />
      <mesh geometry={nodes.Truck2.geometry} material={truckMaterial} position={[0, 0.101, 0.617]} rotation={[Math.PI, 0, Math.PI]} />
    </group>
  )
}

useGLTF.preload('/skateboard.gltf')

效果图

image.png

使用gsap让轮子旋转,整体X轴移动

GSAP(GreenSock Animation Platform)是一个高性能、功能强大的 JavaScript 动画库,专门用于创建复杂的网页动画和交互效果。它支持对 DOM 元素、SVG、Canvas、WebGL 以及 JavaScript 对象进行动画控制,广泛应用于网页开发、游戏开发、数据可视化等领域。

useEffect(() => {
    if (!containerRef.current) return;

    // 创建一个无限循环的水平移动动画
    // 使滑板在x轴上来回移动0.2个单位,动画时长3秒
    // repeat: -1 表示无限循环,yoyo: true 表示往返动画
    gsap.to(containerRef.current.position, {
        x: 0.2,
        duration: 3,
        repeat: -1,
        yoyo: true,
        ease: "sine.inOut",
    });
}, []);
    /*
Auto-generated by: https://github.com/pmndrs/gltfjsx
Command: npx gltfjsx@6.5.3 ./public/skateboard.gltf --typescript 
*/

import React, { useEffect, useMemo, useRef } from 'react'
import { useGLTF, useTexture } from '@react-three/drei'
import * as THREE from 'three'
import { GLTF } from 'three-stdlib'
import { useFrame } from '@react-three/fiber'
import gsap from 'gsap'
// 定义GLTFResult类型,参考Skateboard.tsx
type GLTFResult = GLTF & {
  nodes: {
    GripTape: THREE.Mesh
    Wheel1: THREE.Mesh
    Wheel2: THREE.Mesh
    Deck: THREE.Mesh
    Wheel4: THREE.Mesh
    Bolts: THREE.Mesh
    Wheel3: THREE.Mesh
    Baseplates: THREE.Mesh
    Truck1: THREE.Mesh
    Truck2: THREE.Mesh
  }
  materials: any
}

export function Skateboard(props: React.ComponentProps<'group'>) {
  const { nodes, materials } = useGLTF('/skateboard.gltf') as unknown as GLTFResult
  const wheelRefs = useRef<THREE.Object3D[]>([]);
  const containerRef = useRef<THREE.Group>(null);
  const wheelTexture = useTexture('/skateboard/SkateWheel1.png')
  wheelTexture.flipY = false // 翻转y轴
  wheelTexture.colorSpace = THREE.SRGBColorSpace // 设置颜色空间  

  const wheelMaterial = useMemo(
    () =>
      new THREE.MeshStandardMaterial({
        map: wheelTexture,
        roughness: 0.35,
      }),
    [wheelTexture]
  );

  // 加载砂纸漫反射和粗糙度贴图
  const gripTapeDiffuse = useTexture("/skateboard/griptape-diffuse.webp");
  const gripTapeRoughness = useTexture("/skateboard/griptape-roughness.webp");

  const gripTapeMaterial = useMemo(() => {
    const material = new THREE.MeshStandardMaterial({
      map: gripTapeDiffuse, // 漫反射贴图
      bumpMap: gripTapeRoughness, // 凹凸贴图
      roughnessMap: gripTapeRoughness, // 粗糙度贴图
      bumpScale: 3.5, // 凹凸强度
      roughness: 0.8, // 粗糙度
      color: "#555555", // 基础色
    });

    // 设置砂纸漫反射贴图在水平方向重复模式为RepeatWrapping(重复平铺)
    gripTapeDiffuse.wrapS = THREE.RepeatWrapping;
    // 设置砂纸漫反射贴图在垂直方向重复模式为RepeatWrapping(重复平铺)
    gripTapeDiffuse.wrapT = THREE.RepeatWrapping;
    // 设置砂纸漫反射贴图在两个方向上各重复9次,增加细节
    gripTapeDiffuse.repeat.set(9, 9);
    // 标记贴图需要更新,否则Three.js不会立即应用新设置
    gripTapeDiffuse.needsUpdate = true;

    // 设置砂纸粗糙度贴图在水平方向重复模式为RepeatWrapping(重复平铺)
    gripTapeRoughness.wrapS = THREE.RepeatWrapping;
    // 设置砂纸粗糙度贴图在垂直方向重复模式为RepeatWrapping(重复平铺)
    gripTapeRoughness.wrapT = THREE.RepeatWrapping;
    // 设置砂纸粗糙度贴图在两个方向上各重复9次,保证与漫反射贴图一致
    gripTapeRoughness.repeat.set(9, 9);
    // 标记贴图需要更新,否则Three.js不会立即应用新设置
    gripTapeRoughness.needsUpdate = true;

    // 提高各向异性过滤等级,使贴图在斜视角下更清晰
    gripTapeRoughness.anisotropy = 8;

    return material;
  }, [gripTapeDiffuse, gripTapeRoughness]);

  // ------------------- 螺丝材质 -------------------
  const boltColor = "#6F6E6A";
  // 螺丝材质,颜色可自定义
  const boltMaterial = useMemo(
    () =>
      new THREE.MeshStandardMaterial({
        color: boltColor,
        metalness: 0.5, // 金属度
        roughness: 0.3, // 粗糙度
      }),
    [boltColor]
  );


  const truckColor = "#6F6E6A";
  const metalNormal = useTexture("/skateboard/metal-normal.avif");
  metalNormal.wrapS = THREE.RepeatWrapping;
  metalNormal.wrapT = THREE.RepeatWrapping;
  metalNormal.anisotropy = 8;
  metalNormal.repeat.set(8, 8);

  // 卡车材质,颜色和法线贴图可自定义
  const truckMaterial = useMemo(
    () =>
      new THREE.MeshStandardMaterial({
        color: truckColor,
        normalMap: metalNormal, // 法线贴图
        normalScale: new THREE.Vector2(0.3, 0.3), // 法线强度
        metalness: 0.8,
        roughness: 0.25,
      }),
    [truckColor, metalNormal]
  );

  const deckTexture = useTexture("/skateboard/Deck.webp");
  const deckMaterial = useMemo(
    () =>
      new THREE.MeshStandardMaterial({
        map: deckTexture,
        roughness: 0.1,
      }),
    [deckTexture]
  );

  // 用于收集所有轮子的ref,方便后续统一旋转等操作
  const addToWheelRefs = (ref: THREE.Object3D | null) => {
    if (ref && !wheelRefs.current.includes(ref)) {
      wheelRefs.current.push(ref);
    }
  };

  useFrame(() => {
    if (!wheelRefs.current) return;
    for (const wheel of wheelRefs.current) {
      wheel.rotation.x += 0.2; // x轴旋转
    }
  });

  useEffect(() => {
    if (!containerRef.current) return;

    // 创建一个无限循环的水平移动动画
    // 使滑板在x轴上来回移动0.2个单位,动画时长3秒
    // repeat: -1 表示无限循环,yoyo: true 表示往返动画
    gsap.to(containerRef.current.position, {
      x: 0.2,
      duration: 3,
      repeat: -1,
      yoyo: true,
      ease: "sine.inOut",
    });
  }, []);

  return (
    <group dispose={null} ref={containerRef}>
      <group name='Scene'>
        <mesh name='GripTape' geometry={nodes.GripTape.geometry} material={gripTapeMaterial} position={[0, 0.286, -0.002]} />
        <mesh name='Wheel1' ref={addToWheelRefs} geometry={nodes.Wheel1.geometry} material={wheelMaterial} position={[0.238, 0.086, 0.635]} />
        <mesh name='Wheel2' ref={addToWheelRefs} geometry={nodes.Wheel2.geometry} material={wheelMaterial} position={[-0.237, 0.086, 0.635]} />
        <mesh name='Deck' geometry={nodes.Deck.geometry} material={deckMaterial} position={[0, 0.271, -0.002]} />
        <mesh name='Wheel4' ref={addToWheelRefs} geometry={nodes.Wheel4.geometry} material={wheelMaterial} position={[-0.238, 0.086, -0.635]} rotation={[Math.PI, 0, Math.PI]} />
        <mesh name='Bolts' geometry={nodes.Bolts.geometry} material={boltMaterial} position={[0, 0.198, 0]} rotation={[Math.PI, 0, Math.PI]} />
        <mesh name='Wheel3' ref={addToWheelRefs} geometry={nodes.Wheel3.geometry} material={wheelMaterial} position={[0.237, 0.086, -0.635]} rotation={[Math.PI, 0, Math.PI]} />
        <mesh name='Baseplates' geometry={nodes.Baseplates.geometry} material={truckMaterial} position={[0, 0.211, 0]} />
        <mesh name='Truck1' geometry={nodes.Truck1.geometry} material={truckMaterial} position={[0, 0.101, -0.617]} />
        <mesh name='Truck2' geometry={nodes.Truck2.geometry} material={truckMaterial} position={[0, 0.101, 0.617]} rotation={[Math.PI, 0, Math.PI]} />
      </group>
    </group>
  )
}

useGLTF.preload('/skateboard.gltf')

效果图

图片1.gif

总结

下一篇文章讲如何使用gsap进行跳跃旋转等操作,谢谢

参考视频:www.youtube.com/watch?v=LBO…