Three-Fiber的一个实现

Published on
莱子-
8 min read

添加了啥?

实践了一次在next.js添加three fiber库的3D物体展示功能。在首页就看到了——是一只蓝鲸被猎杀的场景。自旋转、可以拖动放大旋转等。

使用的三方库

three.js @react-three/fiber @react-three/drei

题外话:@react-three/drei是个宝藏,这个博客首页实现的水面透视效果就是通过drei中的meshPhysicalMaterialMeshTransmissionMaterial实现的。

几个大坑

  • 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平米的笼子里活动。另一个你可能不知道的事实是,大部分海洋馆里的海豚都患有口腔溃疡,因为他们压力大。