前置
- @react-three/fiber@9.0.0-rc.1
- @react-three/fiber
- gsap@3.12.5
- gltfjsx
效果图
使用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')
转化后的效果图
给模型上环境+阴影
这里使用的是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>
);
}
效果图
模型上贴图
给轮子上贴图
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')
效果图
使用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')
效果图
总结
下一篇文章讲如何使用gsap进行跳跃旋转等操作,谢谢