Three-Fiber的一个实现
- Published on
- 莱子--8 min read
添加了啥?
实践了一次在next.js添加three fiber库的3D物体展示功能。在首页就看到了——是一只蓝鲸被猎杀的场景。自旋转、可以拖动放大旋转等。
使用的三方库
three.js
@react-three/fiber
@react-three/drei
题外话:
@react-three/drei
是个宝藏,这个博客首页实现的水面透视效果就是通过drei中的meshPhysicalMaterial
和MeshTransmissionMaterial
实现的。
几个大坑
- Next.js的SSR特性
- glb文件的draco压缩
- mesh节点解析
Next.js的SSR特性
Next.js特性是默认是ssr,即服务器端渲染,因此以下代码是关键:
export default dynamic(() => Promise.resolve(ThreeScene), {
ssr: false,
});
否则,开发预览会遇到错误:
同时文件头声明'use client';
确保这个组件作为client component进行前端渲染。
glb文件的draco压缩
说实话使用这个fiber组件换各种姿势写了不下20种代码来展示这个3D canvas,遇到最奇怪的问题就是yarn dev本地开发预览一切正常,控制台无任何错误,但是部署到Vercel build后就崩了,只可以看到导入的environment,任何mesh都渲染不出来。下图起始是初始的使用blender做的模型(以glb格式导出):
debug过程
直到最后我想很可能最开始的组件代码是没有任何问题的,因为我尝试减少mesh数量一步一步debug发现:当mesh只有上方的两个cube时(其实是一组cube,外层和内层做不同渲染)时,无论本地开发预览还是Vercel部署都可以正常看到正常的3D场景,一旦多加任何的mesh进去渲染,都无法正常得到正确的渲染,编译不会崩,但是canvas只会展示环境。
直到第二天才发现控制台有一条错误信息夹在中间大概意思是“object非有效的draco文件”
???
于是(在我换了20种coding尝试后)我尝试导出gltf资产包,以资产包的方式放入public内,小心翼翼添加了一个龟龟mesh,竟然可以了...
期间有尝试过不使用draco CDN的方式而是声明本地draco二进制文件的方式来decoding mesh,是同样的错误。
然而当我将所有的mesh逐渐添加进去,在blender中导出gltf资产包,将以下mesh逐一添加后,dev预览,控制台无任何错误,然后部署...
<mesh geometry={(nodes.cube1 as THREE.Mesh).geometry} position={[-0.56, -1.38, -0.11]}>
{config.meshPhysicalMaterial ? (
<meshPhysicalMaterial {...config} />
) : (
<MeshTransmissionMaterial background={new THREE.Color(config.bg)} {...config} />
)}
</mesh>
<mesh
castShadow
renderOrder={-100}
geometry={(nodes.cube2 as THREE.Mesh).geometry}
material={materials.cube_mat}
material-side={THREE.FrontSide}
position={[-0.56, -1.38, -0.11]}
/>
<mesh geometry={(nodes.cube1001 as THREE.Mesh).geometry} position={[-0.56, -1.34, -0.11]}>
{config.meshPhysicalMaterial ? (
<meshPhysicalMaterial {...config} />
) : (
<MeshTransmissionMaterial background={new THREE.Color(config.bg)} {...config} />
)}
</mesh>
<mesh
castShadow
renderOrder={-100}
geometry={(nodes.cube2001 as THREE.Mesh).geometry}
material={materials.cube_mat}
material-side={THREE.FrontSide}
position={[-0.56, -1.34, -0.11]}
/>
<mesh geometry={(nodes.cube1002 as THREE.Mesh).geometry} position={[-0.56, -1.38, -0.11]}>
{config.meshPhysicalMaterial ? (
<meshPhysicalMaterial {...config} />
) : (
<MeshTransmissionMaterial background={new THREE.Color(config.bg)} {...config} />
)}
</mesh>
<mesh
castShadow
renderOrder={-100}
geometry={(nodes.cube2002 as THREE.Mesh).geometry}
material={materials.cube_mat}
material-side={THREE.FrontSide}
position={[-0.56, -1.38, -0.11]}
/>
<mesh geometry={(nodes.cube1003 as THREE.Mesh).geometry} position={[-4.8, -1.38, -4.6]}>
{config.meshPhysicalMaterial ? (
<meshPhysicalMaterial {...config} />
) : (
<MeshTransmissionMaterial background={new THREE.Color(config.bg)} {...config} />
)}
</mesh>
<mesh
castShadow
renderOrder={-100}
geometry={(nodes.cube2003 as THREE.Mesh).geometry}
material={materials.cube_mat}
material-side={THREE.FrontSide}
position={[-4.8, -1.38, -4.6]}
/>
<mesh geometry={(nodes.球体 as THREE.Mesh).geometry} position={[-11.1, 0.38, -9.45]}>
{config.meshPhysicalMaterial ? (
<meshPhysicalMaterial {...config} />
) : (
<MeshTransmissionMaterial background={new THREE.Color(config.bg)} {...config} />
)}
</mesh>
<mesh
geometry={(nodes.bubbles as THREE.Mesh).geometry}
material={materials.cube_bubbles_mat}
position={[-0.56, -2.28, -0.11]}
/>
<group position={[-1.0, -2, -1.5]}>
<mesh geometry={(nodes.arrows as THREE.Mesh).geometry} material={materials.weapons_mat} />
</group>
<mesh
geometry={(nodes.bluewhale_1 as THREE.Mesh).geometry}
material={materials.bluewhale}
position={[0.0, -1.5, -0.5]}
rotation={[0, 1.2 * Math.PI, 0]}
/>
<mesh
geometry={(nodes.efish as THREE.Mesh).geometry}
material={materials.efish}
position={[-0.4, -2.18, 0.1]}
/>
<mesh
geometry={(nodes.shark as THREE.Mesh).geometry}
material={materials.shark}
position={[-0, -1.5, 0.5]}
rotation={[0, 2.5 * Math.PI, 0]}
/>
<mesh
geometry={(nodes.turtle as THREE.Mesh).geometry}
material={materials.turtle}
position={[-0.5, -1.68, 0.4]}
rotation={[0, 2.1 * Math.PI, 0]}
/>
...结果是,还是崩了。
进一步缩减mesh数量,直到目前的效果,一切正常。但是我还是感觉这是一个不正常的妥协,我在three fiber仓库和社区也没找到答案。但是几乎确定问题在于draco。
mesh节点解析
这里推荐一个解析3D文件的mesh元数据信息的网站:https://gltf.nsdt.cloud/ 由于我是自己在blender中制作的,所以mesh节点信息可以比较清楚的看到,但是仍然会有一些mesh信息在blender中显示和实际不同的情况。如果是网上下载的glb模型,我觉得使用这个工具查看mesh和material元数据会方便很多。
MeshTransmissionMaterial
再次感叹drei这个库的强大,模拟透明材质或者玻璃、水的效果无敌。这里使用如下参数实现:
const config = {
meshPhysicalMaterial: false,
transmissionSampler: false,
backside: false,
samples: 10,
resolution: 2048,
transmission: 1,
roughness: 0.0,
thickness: 3.5,
ior: 1.01,
chromaticAberration: 0.04,
anisotropy: 0.1,
distortion: 0.57,
distortionScale: 0.5,
temporalDistortion: 0.5,
clearcoat: 1,
attenuationDistance: 0.5,
attenuationColor: '#ffffff',
color: '#99ecff',
bg: '#839681',
具体参数意义可以在drei官方库中查看。
附本组件代码
'use client';
import * as THREE from 'three';
import { Canvas } from '@react-three/fiber';
import {
MeshTransmissionMaterial,
useGLTF,
AccumulativeShadows,
RandomizedLight,
Environment,
OrbitControls,
Center,
} from '@react-three/drei';
import dynamic from 'next/dynamic';
function frozenWhale() {
const config = {
meshPhysicalMaterial: false,
transmissionSampler: false,
backside: false,
samples: 10,
resolution: 2048,
transmission: 1,
roughness: 0.0,
thickness: 3.5,
ior: 1.01,
chromaticAberration: 0.04,
anisotropy: 0.1,
distortion: 0.57,
distortionScale: 0.5,
temporalDistortion: 0.5,
clearcoat: 1,
attenuationDistance: 0.5,
attenuationColor: '#ffffff',
color: '#99ecff',
bg: '#839681',
};
const { nodes, materials } = useGLTF('/newfronzen1/newfronzen.gltf');
if (!nodes || !materials) return null;
console.log(nodes, materials);
return (
<group dispose={null}>
<mesh geometry={nodes.cube1.geometry} position={[-0.56, -1.38, -0.11]}>
{config.meshPhysicalMaterial ? (
<meshPhysicalMaterial {...config} />
) : (
<MeshTransmissionMaterial background={new THREE.Color(config.bg)} {...config} />
)}
</mesh>
<mesh
castShadow
geometry={nodes.cube2.geometry}
material={materials.cube_mat}
material-side={THREE.FrontSide}
position={[-0.56, -1.38, -0.11]}
/>
<mesh
geometry={nodes.bluewhale002.geometry}
material={materials.bluewhale02}
position={[-0.26, 12.88, 0.11]}
/>
<mesh
geometry={nodes.bubbles.geometry}
material={materials.cube_bubbles_mat}
position={[-0.56, -1.38, -0.11]}
/>
<mesh
geometry={nodes.arrows.geometry}
material={materials.weapons_mat}
position={[-0.96, -1.0, -1.31]}
/>
</group>
);
}
function ThreeScene() {
return (
<Canvas shadows camera={{ position: [55, 8, 5], fov: 15 }} style={{ height: '50vh' }}>
<ambientLight intensity={Math.PI} />
<group position={[0, -2.5, 0]}>
<Center top>
<GelatinousCube />
</Center>
<AccumulativeShadows
temporal
frames={100}
alphaTest={0.9}
color="#3ead5d"
colorBlend={1}
opacity={0.8}
scale={40}
>
<RandomizedLight
radius={10}
ambient={0.5}
intensity={Math.PI}
position={[2.5, 8, -2.5]}
bias={0.001}
/>
</AccumulativeShadows>
</group>
<OrbitControls
minPolarAngle={0}
maxPolarAngle={Math.PI / 2}
autoRotate
autoRotateSpeed={0.3}
makeDefault
/>
<Environment files="/dancing_hall_1k.hdr" background backgroundBlurriness={1} />
</Canvas>
);
}
export default dynamic(() => Promise.resolve(ThreeScene), {
ssr: false,
});
具体还有一些three fiber在next.js中的坑,想起来再补充。
至于这个蓝鲸场景,是因为我本人抵制任何的海洋馆表演。大学的时候曾经参与过OPS的一些海洋保护的活动,一起看过《海豚湾》这个电影,影响至今。你在海洋馆里所看到的任何海豚、白鲸憨态可掬的表演、打招呼,都是条件反射的训练结果,他们并不开心。如果他们的活动领地是大海的话,在海洋馆里几乎等同于将一个人困在1.5平米的笼子里活动。另一个你可能不知道的事实是,大部分海洋馆里的海豚都患有口腔溃疡,因为他们压力大。