将3D模型加载到WebGL场景中是在浏览器中进行3D渲染最复杂的方面之一。虽然Three.js和Babylon.js等库提供了强大的API,有助于缓解使用WebGL的紧张气氛,但它们也不能避免自己的繁琐过程,比如从头开始创建着色器,以及在向场景添加对象时重复使用scene.add()
。
react-three-fiber是一个适用于Three.js的React渲染器,它通过在引擎盖下处理必要的Three.js功能,并通过Hooks提供对原始Three.js对象的访问,缓解了网络上的3D模型工作。
在这篇文章中,我们将介绍如何使用react-three-fiber在React项目中渲染和配置在Blender或Maya等3D软件中创建的3D资产。在本文结束时,你将能够在你的网站上渲染3D模型。
前提条件
要跟上本教程,你需要对React和Three.js的架构有基本了解,包括照明、几何和画布。你还需要在你的机器上安装Node.js。让我们开始吧!
为网络创建和准备一个三维模型
开箱后,Three.js支持几种3D模型格式。由于这些文件格式大多比较复杂和困难,建议尽可能地使用glTF(GL传输格式)。
glTF资产专注于运行时的资产交付,加载速度快,可以压缩成紧凑的大小进行传输。glTF资产以JSON.gltf
和二进制.glb
两种格式提供。每种支持的文件格式在 Three.js 中都有一个相应的加载器。例如,你可以使用glTF加载器将glTF或GLB文件加载到react-three-fiber场景中。
要创建你的模型,你可以使用你选择的三维建模软件,但根据我的经验,大多数人更喜欢Blender。如果你像我一样,没有任何创建3D模型的经验,你可以从sketchfab.com这样的网站上外包公共域名的glTF文件。
这些网站上的大多数免费资产都属于知识共享协议,这意味着你可以在你的项目中使用它们,只要你注明原创作者。明智的做法是,在将某项资产纳入你的项目之前,先检查其使用许可。
如果你打算在你的模型中添加动画或事件映射等动作,你应该设计你的资产,使每个部分,或网格,被正确地分组和分离。不幸的是,很少有在线的免费资产能实现这一点,所以你需要购买一个或建立自己的资产。
在本教程中,我们将使用鞋类资产作为一个示例项目。如果你发现很难建立或找到一个模型,你可以从Three.js的GitHub repo中获得一个。
将glTF转换为GLB
glTF是基于JSON的,这意味着它的一些数据是存储在外部的,例如,几何体、着色器、纹理和动画数据。然而,GLB 在内部存储这些数据,使得它的大小比 glTF 小得多。因此,.glb
二进制格式最好在你的项目中使用。现在,让我们来看看如何将glTF文件转换为GLB。
有几个工具你可以用于这个操作,然而,glTF-pipeline是最好的选择之一。它是一个灵活的优化glTF资产的工具,它有几个扩展,可以从你的命令行工具中直接执行常见的操作,比如GLB到glTF的转换,glTF的压缩,等等。
另外,你可以使用VS Code的官方glTF工具扩展,从编辑器中直接预览、调试和转换模型。你还可以使用这个扩展来编辑和调整glTF文件。例如,如果你的模型的网格命名不正确,你可以编辑文件,并按你的意愿重新命名。
要开始使用glTF-pipeline的转换过程,首先,我们需要用以下命令在我们的机器上全局安装它。
npm install -g gltf-pipeline
接下来,将glTF文件放在一个空的文件夹中,并打开你的命令行工具。cd
,进入该文件夹并运行下面的命令。
gltf-pipeline -i <source file> -o <output file>
将上面命令中的第一个占位符替换为源文件的名称。将第二个占位符替换为你喜欢的输出文件名。
gltf-pipeline -i shoe.gltf -o shoe.glb
运行上述命令后,你应该在文件夹中看到一个新的GLB文件,与之前的glTF文件一起。这个过程也适用于将GLB转换为glTF格式的其他方法。
压缩模型
为了避免我们的3D资产的大小降低我们网站的性能,我们将在加载到我们的场景之前对它进行压缩。建议你的文件大小不要超过一到两兆字节。我们将使用一个叫做Draco的glTF管道压缩扩展。
gltf-pipeline -i <source file> -o <output file> --draco.compressionLevel=10
上面的命令与我们之前在转换过程中使用的命令相似,唯一的区别是压缩级别标志。Draco压缩的最大值为10,最小值为0。在压缩级别中传递10
,可以提供最大可能的压缩。
现在,我们可以设置一个项目,创建一个场景,并加载我们的模型。
设置我们的 react-three-fiber 项目
首先,让我们用Create React App创建一个新的React项目。
npx create-react-app react-three
之后,用下面的命令安装 react-three-fiber。
npm install three @react-three/fiber
完成后,继续运行下面的命令来安装我们项目所需的所有依赖项。
npm i @react-three/drei react-colorful valtio
上面的命令将安装以下依赖项。
- react-colorful:React的颜色选择器组件。
- drei:为react-three-fiber提供有用的附加组件,如相机、平面和控制等
- Valtio:React的一个轻量级、基于代理的状态管理工具。
接下来,我们将在一个空组件中创建一个场景。首先,导入并创建一个空画布,如下所示。
import React from "react";
import { Canvas } from "react-three-fiber";
import "./styles.css";
export default function App() {
return <Canvas style={{ background: "#171717" }}></Canvas>;
}
现在,我们的项目和场景已经设置好了,准备好让我们开始加载我们的模型了在我们将资产加载到场景中并对其进行配置之前,我们需要找到一个简单的方法将我们的模型转换为一个组件。
将模型转换为可重用的React组件
将glTF模型加载到Three.js场景中是一项很重要的工作。在配置或动画化我们模型的网格之前,我们需要迭代我们模型的每一部分网格,并分别保存它们。
幸运的是,react-three-fiber有一个了不起的实用程序包,叫做gltfjsx,它可以分解模型并将其编译成一个声明性的、可重用的JSX组件。
首先,打开你的命令行工具,cd
,进入你的压缩glTF模型所在的文件夹,并运行以下命令。
npx gltfjsx <glTF model source file>
在我们之前下载的鞋子模型上运行上面的命令。现在,上面的代码将看起来像下面这样。
npx gltfjsx shoe-draco.gltf
上面的命令将返回一个JavaScript文件,该文件以React功能组件的格式规划出所有资产的内容。该文件的内容将类似于下面的代码。
import React, { useRef } from 'react'
import { useGLTF } from '@react-three/drei/useGLTF'
function Shoe({ ...props }) {
const group = useRef()
const { nodes, materials } = useGLTF('/shoe-draco.glb')
const snap = useSnapshot(state);
return (
<group ref={group} {...props} dispose={null}>
<group ref={group} {...props} dispose={null}>
<mesh geometry={nodes.shoe.geometry} material={materials.laces}/>
<mesh geometry={nodes.shoe_1.geometry} material={materials.mesh}/>
<mesh geometry={nodes.shoe_2.geometry} material={materials.caps} />
<mesh geometry={nodes.shoe_3.geometry} material={materials.inner} />
<mesh geometry={nodes.shoe_4.geometry} material={materials.sole}/>
<mesh geometry={nodes.shoe_5.geometry} material={materials.stripes}/>
<mesh geometry={nodes.shoe_6.geometry} material={materials.band}/>
<mesh geometry={nodes.shoe_7.geometry} material={materials.patch} />
</group>
</group>
)}
节点和材料的值被useGLTF
Hook解构,而我们模型的路径被作为参数传入Hook。
解构的节点是一个对象,包含了我们模型中的所有信息,包括动画、纹理和几何。在这种情况下,该节点使用了模型的重组几何体,并作为道具被传入网格组件。
将模型组件添加到场景中
你可以把新创建的文件原封不动地放到你的项目的src文件夹中,或者你可以把文件中的代码复制并粘贴到你的项目中的一个现有组件中。
要把模型添加到场景中,像其他React组件一样导入它。
import Shoe from './Shoe.js'
接下来,把你之前压缩的glTF文件移到/public
。最后,在你的画布内添加该组件。
import React from "react";
import { Canvas } from "react-three-fiber";
import Shoe from './Shoe.js'
import "./styles.css";
export default function App() {
return(
<Canvas style={{ background: "#171717" }}>
<Shoe />
</Canvas>;
)};
你也可以按以下方式把它添加到你的组件中。
import React, { useRef } from "react"
import { Canvas } from "react-three-fiber"
import { useGLTF } from '@react-three/drei/useGLTF'
import "./styles.css";
function Shoe({ ...props }) {
const group = useRef()
const { nodes, materials } = useGLTF('/shoe-draco.glb')
return (
<group ref={group} {...props} dispose={null}>
<group ref={group} {...props} dispose={null}>
<mesh geometry={nodes.shoe.geometry} material={materials.laces}/>
<mesh geometry={nodes.shoe_1.geometry} material={materials.mesh}/>
<mesh geometry={nodes.shoe_2.geometry} material={materials.caps} />
<mesh geometry={nodes.shoe_3.geometry} material={materials.inner} />
<mesh geometry={nodes.shoe_4.geometry} material={materials.sole}/>
<mesh geometry={nodes.shoe_5.geometry} material={materials.stripes}/>
<mesh geometry={nodes.shoe_6.geometry} material={materials.band}/>
<mesh geometry={nodes.shoe_7.geometry} material={materials.patch} />
</group>
</group>
)}
export default function App() {
return(
<Canvas style={{ background: "#171717" }}>
<Shoe />
</Canvas>;
)};
在这一点上,如果你启动开发服务器,React会抛出一个编译错误。模型组件是异步的,因此,我们必须将它嵌套在画布中的<Suspense>
组件内,让我们控制中间的加载回退和错误处理。
首先,我们需要从React导入模型组件,然后把它包在画布中的模型周围。
import React, { Suspense, useRef } from "react";
import { Canvas } from "react-three-fiber";
import { useGLTF } from '@react-three/drei/useGLTF'
import "./styles.css";
function Shoe({ ...props }) {
const group = useRef()
const { nodes, materials } = useGLTF('/shoe-draco.glb')
return (
<group ref={group} {...props} dispose={null}>
<group ref={group} {...props} dispose={null}>
<mesh geometry={nodes.shoe.geometry} material={materials.laces}/>
<mesh geometry={nodes.shoe_1.geometry} material={materials.mesh}/>
<mesh geometry={nodes.shoe_2.geometry} material={materials.caps} />
<mesh geometry={nodes.shoe_3.geometry} material={materials.inner} />
<mesh geometry={nodes.shoe_4.geometry} material={materials.sole}/>
<mesh geometry={nodes.shoe_5.geometry} material={materials.stripes}/>
<mesh geometry={nodes.shoe_6.geometry} material={materials.band}/>
<mesh geometry={nodes.shoe_7.geometry} material={materials.patch} />
</group>
</group>
)}
export default function App() {
return(
<Canvas style={{ background: "#171717" }}>
<Suspense fallback={null}>
<Shoe />
</Suspense>
</Canvas>;
)};
上面的代码将清除错误,我们的模型将在浏览器上成功显示。然而,无论我们把什么东西加载到我们的场景中,都只能以模型的剪影形象出现。
目前,我们的场景并没有照明源。一个常见的解决方案是简单地在画布中的ambientLight
和spotLight
组件之前添加<Suspense>
。
<Canvas style={{ background: "#171717" }}>
<ambientLight intensity={1} />
<spotLight intensity={0.5} angle={0.1} penumbra={1} position={[10, 15, 10]} castShadow />
<Suspense fallback={null}>
<Shoe />
</Suspense>
</Canvas>;
之后,模型就会正常显示。
继续下去,摆弄一下灯光的强度、角度和位置。如果你对结果不满意,你也可以通过添加一个定位道具来定位灯光。
配置模型
现在我们的模型是声明性的,我们可以完全访问它的网格,这意味着我们可以添加、删除和改变我们模型的一部分。
例如,如果代码中的第一个网格组件代表鞋子的鞋带,我们删除或注释该网格,鞋子的鞋带将在场景中消失。你可以决定在你的模型上做动画,添加事件,甚至执行条件。
要改变模型,只需改变道具。如果我们想把第一个网格的颜色改成红色,添加material-color="Red"
作为道具。在场景中,模型的这一部分将变为红色。
<mesh material={materials.White} geometry={nodes['buffer-0-mesh-0'].geometry} material-color="Red"/>
我们可以用React的useState
Hook,以及任何支持Suspense的状态管理库动态地做同样的事情。在这种情况下,我们将使用Valtio,我们之前已经安装了它。
用Valtio配置我们的模型
继续从Valtio导入proxy
和useSnapshot
。
import {proxy, useSnapshot} from 'valtio'
接下来,复制下面的代码并粘贴到你的模型组件之前。
const state = proxy({
current: null,
items: {
laces: "#ff3",
mesh: "#3f3",
caps: "#3f3",
inner: "#3f3",
sole: "#3f3",
stripes: "#3f3",
band: "#3f3",
patch: "#3f3",
},
})
我们创建了一个有两个键的对象,items
和current
。current
的值是null
,而items
有一个对象值,其键代表我们模型的每个部分和一个十六进制颜色值。然后,该对象被包裹在代理中。
为了在组件内部使用状态,我们将在模型组件内部创建一个常量snap
变量,并将其分配给useSnapshot
。之后,我们将把状态作为一个参数传入快照。
import React, { useRef, Suspense} from 'react'
import { Canvas } from "react-three-fiber";
import "./styles.css";
import { useGLTF } from '@react-three/drei/useGLTF'
import {proxy, useSnapshot} from 'valtio';
const state = proxy({
current: null,
items: {
laces: "#ff3",
mesh: "#3f3",
caps: "#3f3",
inner: "#3f3",
sole: "#3f3",
stripes: "#3f3",
band: "#3f3",
patch: "#3f3",
},
})
function Shoe({ ...props }) {
const group = useRef()
const { nodes, materials } = useGLTF('/shoe-draco.glb')
const snap = useSnapshot(state);
return (
<group ref={group} {...props} dispose={null}>
<group ref={group} {...props} dispose={null}>
<mesh geometry={nodes.shoe.geometry} material={materials.laces}/>
<mesh geometry={nodes.shoe_1.geometry} material={materials.mesh}/>
<mesh geometry={nodes.shoe_2.geometry} material={materials.caps} />
<mesh geometry={nodes.shoe_3.geometry} material={materials.inner} />
<mesh geometry={nodes.shoe_4.geometry} material={materials.sole}/>
<mesh geometry={nodes.shoe_5.geometry} material={materials.stripes}/>
<mesh geometry={nodes.shoe_6.geometry} material={materials.band}/>
<mesh geometry={nodes.shoe_7.geometry} material={materials.patch} />
</group>
</group>
)}
export default function App() {
return <Canvas style={{ background: "#171717" }}>
<ambientLight intensity={1} />
<spotLight intensity={0.5} angle={0.1} penumbra={1} position={[10, 15, 10]} castShadow />
<Suspense fallback={null}>
<Shoe />
</Suspense>
</Canvas>;
}
在我们可以在组件中使用状态的快照之前,我们必须将每个网格的材料颜色设置为状态变量的颜色。为了做到这一点,我们将像之前那样给每个网格添加material-color
道具。这一次,我们将把我们状态的快照传递给它。
const state = proxy({
current: null,
items: {
laces: "#ff3",
mesh: "#3f3",
caps: "#3f3",
inner: "#3f3",
sole: "#3f3",
stripes: "#3f3",
band: "#3f3",
patch: "#3f3",
},
})
function Shoe({ ...props }) {
const group = useRef()
const { nodes, materials } = useGLTF('/shoe-draco.glb')
const snap = useSnapshot(state);
return (
<group ref={group} {...props} dispose={null}>
<group ref={group} {...props} dispose={null}>
<mesh geometry={nodes.shoe.geometry} material={materials.laces} material-color={snap.items.laces}/>
<mesh geometry={nodes.shoe_1.geometry} material={materials.mesh} material-color={snap.items.mesh}/>
<mesh geometry={nodes.shoe_2.geometry} material={materials.caps} material-color={snap.items.caps}/>
<mesh geometry={nodes.shoe_3.geometry} material={materials.inner} material-color={snap.items.inners}/>
<mesh geometry={nodes.shoe_4.geometry} material={materials.sole} material-color={snap.items.sole}/>
<mesh geometry={nodes.shoe_5.geometry} material={materials.stripes} material-color={snap.items.stripes}/>
<mesh geometry={nodes.shoe_6.geometry} material={materials.band} material-color={snap.items.band}/>
<mesh geometry={nodes.shoe_7.geometry} material={materials.patch} material-color={snap.items.patch}/>
</group>
</group>
)
}
现在,如果我们改变了状态项中的任何一个颜色,模型的那个部分就会在场景中更新其颜色。
添加事件
我们可以向我们的模型添加事件,而不是手动改变状态,这样用户就可以选择鞋子的每个部分,用颜色选择器改变颜色。
首先,我们将为模型的onPointerDown
和onPointerMissed
事件添加到group
组件。我们将创建DOM事件函数,为模型中被点击的部分设置状态,当没有选择时,null
。
<group ref={group} {...props} dispose={null}
onPointerDown={(e) => {e.stopPropagation(); state.current = e.object.material.name }}
onPointerMissed={(e) =>{state.current = null} }
>
现在,我们将从 react-colorful 中导入HexColorPicker
。我们将创建一个新的组件,它将容纳HexColorPicker
组件和一个标题标签,显示被点击的模型部分的名称。
import {HexColorPicker} from 'react-colorful'
function ColorPicker(){
const snap = useSnapshot(state);
return(
<div>
<HexColorPicker color={snap.items[snap.current]} onChange={(color)=>(state.items[state.current] = color)}/>
<h1>{snap.current}</h1>
</div>
)
}
在我们保存代码后,浏览器上应该立即出现一个颜色选择器GUI,看起来像下面的图片。
现在,要改变我们模型的网格颜色,我们可以点击它,用颜色选择器选择一种颜色。你也可以在颜色选择器上添加classNames
,以及一个<H1>
标签。
给模型做动画
现在,这个模型感觉不像一个三维物体。具体来说,它缺乏深度和像静态图像一样的运动。我们可以通过在画布上添加一个orbitControls
组件来解决这个问题。
export default function App() {
return (
<Canvas style={{ background: "#171717" }}>
<spotLight intensity={0.5} angle={0.1} penumbra={1} position={[10, 15, 10]} castShadow />
<ambientLight intensity={1} />
<Suspense fallback={null}>
<Shoe />
</Suspense>
<orbitControls />
</Canvas>;
)}
在 react-three-fiber 中,模型是通过useFrame
Hook 来实现动画的,这与 JavaScript 的requestAnimationFrame()
方法类似。它每秒钟返回一个回调60次或更多,这取决于显示器的刷新率。
如果我们在useFrame
回调里面将我们的模型在y轴上的旋转值设置为"5.09"
,那么模型将每秒沿y轴上下平移60次,创造出一个漂浮物体的幻觉。
为了获得更真实的动画,你可以在useFrame
Hook中改变z轴和x轴的旋转值。为了节省时间,我们将使用下面的代码。
useFrame( (state) => {
const t = state.clock.getElapsedTime()
ref.current.rotation.z = -0.2 - (1 + Math.sin(t / 1.5)) / 20
ref.current.rotation.x = Math.cos(t / 4) / 8
ref.current.rotation.y = Math.sin(t / 4) / 8
ref.current.position.y = (1 + Math.sin(t / 1.5)) / 10
})
记得从react-three-fiber导入useFrame
。
我们项目的完整代码如下。
import {HexColorPicker} from 'react-colorful'
import React, { useRef, Suspense} from 'react'
import { Canvas, useFrame } from "react-three-fiber";
import "./styles.css";
import { useGLTF } from '@react-three/drei/useGLTF'
import {proxy, useSnapshot} from 'valtio';
const state = proxy({
current: null,
items: {
laces: "#ff3",
mesh: "#3f3",
caps: "#3f3",
inner: "#3f3",
sole: "#3f3",
stripes: "#3f3",
band: "#3f3",
patch: "#3f3",
},
})
function Shoe({ ...props }) {
const group = useRef()
useFrame( (state) => {
const t = state.clock.getElapsedTime()
ref.current.rotation.z = -0.2 - (1 + Math.sin(t / 1.5)) / 20
ref.current.rotation.x = Math.cos(t / 4) / 8
ref.current.rotation.y = Math.sin(t / 4) / 8
ref.current.position.y = (1 + Math.sin(t / 1.5)) / 10
})
const { nodes, materials } = useGLTF('/shoe-draco.glb')
const snap = useSnapshot(state);
return (
<group
ref={group}
{...props}
dispose={null}
onPointerDown={(e) => {e.stopPropagation(); state.current = e.object.material.name }}
onPointerMissed={(e) =>{state.current = null} }
>
<group ref={group} {...props} dispose={null}>
<mesh geometry={nodes.shoe.geometry} material={materials.laces}/>
<mesh geometry={nodes.shoe_1.geometry} material={materials.mesh}/>
<mesh geometry={nodes.shoe_2.geometry} material={materials.caps} />
<mesh geometry={nodes.shoe_3.geometry} material={materials.inner} />
<mesh geometry={nodes.shoe_4.geometry} material={materials.sole}/>
<mesh geometry={nodes.shoe_5.geometry} material={materials.stripes}/>
<mesh geometry={nodes.shoe_6.geometry} material={materials.band}/>
<mesh geometry={nodes.shoe_7.geometry} material={materials.patch} />
</group>
</group>
)}
function ColorPicker(){
const snap = useSnapshot(state);
return(
<div>
<HexColorPicker color={snap.items[snap.current]} onChange={(color)=>(state.items[state.current] = color)}/>
<h1>{snap.current}</h1>
</div>
)
}
export default function App() {
return (
<>
<Canvas style={{ background: "#171717" }}>
<ambientLight intensity={1} />
<spotLight intensity={0.5} angle={0.1} penumbra={1} position={[10, 15, 10]} castShadow />
<Suspense fallback={null}>
<Shoe />
</Suspense>
</Canvas>;
<ColorPicker />
</>
)}
结语
在这篇文章中,我们介绍了如何为网络配置3D模型,以及如何用Valtio状态管理将事件映射到模型。我们澄清了glTF和GLB文件的区别,确定了在我们的项目中使用哪种文件是最好的。我们把事件添加到我们的模型中,使我们能够创建自定义的工具,如颜色选择器和照明源。你可以挑战自己,为你的模型添加更多的元素,比如阴影投射器或悬停事件。
希望这篇文章可以作为一个有用的资源,将3D模型纳入你的网站。编码愉快!
The postConfigure 3D models with react-three-fiberappeared first onLogRocket Blog.