WebGL 3D模型优化

随着我在 Echobind 的投入时间以及我自己的空闲时间,我一直在慢慢学习 webGL 库,例如 three.js、Mozilla 的/Super Medium 的 A-Frame、Google 的模型查看器以及最近的 React-three- fiber(R3F)。

在第四季度的投资时间里,我与同事一起使用 R3F 创建了一个可滚动的 3D 网站。 在这个项目中,我想强调我为网络优化 3D 模型的过程。 从这个过程中,我能够将主要 3D 模型从 26MB 减少到 560KB。

three.js 等框架使 webGL编程变得更加容易。 此外,诸如 R3F 之类的库(它更多地抽象了 three.js)使在 web 上处理 3D 体验变得更加容易。

1、加载时间问题

我们在处理此场景时面临的挑战之一是如何最大限度地减少客户端的加载时间。 我们的第一个方法是创建一个加载屏幕。 但是,由于我们的 3D 模型文件太大 (26MB),加载时间太长。 当它最终加载时,交互动画缓慢且断断续续。 为了纠正这些问题,我想找到一种方法来最小化文件大小。

2、我的解决方案

大文件的主要原因是我们的 3D 模型及其相关纹理的复杂性。 因此,我负责探索如何通过优化我们的 3D 模型来最大限度地减少加载时间。 我还发现场景中的材质和网格太多了。 通过最小化这两个组件,可以减小导出模型的文件大小。 以下是我采取的步骤:

3、精简、合并网格

第一步要做的,就是优化网格、删除多边形,然后重新组合对象

首先优化网格:

因为我们担心这个场景在网络上运行不佳,所以我想删除 blender 项目中网格上任何可能不必要的多边形。 这是使用精简修改器(Decimate Modifier)完成的。 我通常从将网格比率降低到 0.5 开始,如果它不会导致任何不需要的伪影,则再降低。 对大多数网格重复此步骤。

接下来删除多边形:

为了帮助展开并移除任何不会出现在最终视图中的东西,我移除了所有网格底部的所有多边形,例如雪网格的底部。 这是通过使用选择套索进入编辑模式>面选择来完成的。 对大多数网格重复此步骤。

为了开始组合网格,我需要确定哪些网格适合组合。 最后,我将场景中位于地面和地板网格上的雪顶上的所有对象组合在一起——减去帧。 我决定这样做是因为它们都适合相同的 UV 空间,并且希望它们都具有相同尺寸的材料。 雪占据了相当大的空间,将其添加到相同的 UV 空间会减小所有对象的比例; 这也会降低对象的纹理质量。 从那里开始,我想结合与帧相关的所有网格——减去玻璃和照片——因为我希望能够为照片图像添加尽可能多的质量。 玻璃需要自己的材料来防止场景中不需要透明度的其他部分出现任何透明度问题。 我根据它们的内容适当地重命名了网格:物体、框架、玻璃、照片、地板、雪以及它们的相关材料。

这导致场景从 65 个单独的网格减少到 6 个相关网格的对象。 组合网格实际上增加了 Blender 文件的大小,但帮助我删除了场景中任何潜在的重复项并快速选择了场景中的大量多边形,以便在接下来的步骤中更容易地操纵它们。

4、UV 压缩

在我能够展开 UV 并将其打包在一起之前,我需要将每个网格的所有多边形关联到一种材质中。 在这一点上,项目中有很多材料。 因此,首先需要做的是移除所有未使用的孤立材料。 通过删除所有孤立材质,我将场景中的材质从 71 种减少到 6 种。

这是展开对象的结果:

这是展开帧的结果:

所有剩余的网格也被展开。

5、材质ID

如果你已确定该部分有特定材质,材质 ID 使你能够快速选择网格的一部分。 我们可以利用材质 ID 并将这些选择的网格转换为可在步骤 5 中使用的单个纹理,而不是将多个材质添加到一个网格中,这会增加模型的文件大小。

为此,我创建了各种具有高对比度颜色的材质。 在对象网格和帧网格的所有多边形都添加了各种颜色材料后,我将颜色烘焙出来,以便将它们添加到网格的 UV 坐标中。

6、烘焙材质 ID

为此,烘焙过程是从我刚刚添加的各种彩色材料中获取所有颜色信息,并将这些颜色投射到纹理上。

这是烘焙后的材质 ID 贴图,如上所示。 在 Blender 中烘焙材料之前需要注意的一些事项:

  • 将渲染模式更改为cycles
  • 关闭所有直射和间接照明
  • 确保已在着色器视口中选择漫反射贴图
  • 选择添加了颜色的重复网格
  • 将具有展开 UV 的网格作为你的活动选择
  • 确保烘焙类型为漫反射
  • 点击烘烤按钮

Blender 的最后一步是将所有内容导出为 .fbx 以及导出所有材质 ID 纹理。

7、在 Substance Painter 中创建材质

Substance Painter 是我最喜欢的 3D 工具之一。 它可以使纹理化过程变得如此有趣和符合情境。 它基本上是 3D 模型的 photoshop。

在导入 .fbx 时,我还导入了对象网格和帧网格的材质 ID 纹理。

烘焙网格贴图(法线、世界空间法线、环境遮挡、曲线等)后,我为对象网格和相框网格的每种颜色创建了文件夹,并使用遮罩选择每个材质 ID 以归因于遮罩。 这就是我们如何能够向网格的一部分添加不同的颜色和纹理。

由于我们想要的预期结果不太现实,我决定只导出 baseColor 和环境遮挡,但由于我想将环境遮挡的阴影/照明添加到 baseColor,我需要将两者组合成一个纹理 . 为此,我需要在导出纹理视口中添加一个新预设。

8、在 Blender 中添加 baseColor

这一步真的很快。 我只需要添加来自 Substance Painter 的烘焙纹理结果并将它们添加到每个材质的 baseColor。 之后,我将所有内容导出为 .glb。 此时,导出的文件大小为 5.1MB。

9、glTF 压缩

既然模型已纹理化并从 Blender 导出,我将模型上传到 gltf.report 以进一步压缩 .glb。

将模型上传到 gltf.report 后,我单击脚本选项卡。 从那里我将纹理大小从 1024 x 1024 减小到 128 x 128,然后单击运行然后导出。

结果文件从 5.1MB 降到了 3MB。 然后将生成的简化网格添加到 /public/meshes/ 下的项目中。

在运行下一步之前,重要的是安装 nvm 并安装较早版本的节点。 否则,可能会在运行 gltfjsx 时遇到错误。

NVM ls
NVM use 14.20.0

在运行命令行工具之前,我需要将目录从项目文件夹更改为网格所在的位置。 否则结果将被添加到项目文件夹中。

cd /public/meshes/

现在,我可以运行 gltfjsx

npx gltfjsx main_scene.glb -T

运行 gltfjsx 的结果提供了一个较小的 .glb 文件以及 3D 模型的 react.js 组件。 结果将文件从 3MB 减少到 581KB。

如果你不想安装 gltfjsx 和更早的node版本,你也可以前往这里, 但结果可能会有所不同。

这是 560KB 的最终结果:

以下是来自 gltfjsx 的 main_scene.js 的结果:

/*
Auto-generated by: https://github.com/pmndrs/gltfjsx
*/

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


export function Main(props) {
  const { nodes, materials } = useGLTF('/meshes/main_scene.glb')
  return (
    <group {...props} dispose={null}>
      <mesh geometry={nodes.floor.geometry} material={materials.floor} position={[-0.01, 0, -0.61]} />
      <mesh geometry={nodes.Snow.geometry} material={materials.Snow} position={[0, 0, -0.61]} />
      <mesh geometry={nodes.objects.geometry} material={materials.objects} position={[1.13, 0.28, 0.53]} rotation={[-Math.PI, -0.09, -Math.PI]} />
      <mesh 
        // onClick={() => {console.log('frame clicked')}}
        geometry={nodes.frame.geometry} material={materials.frame} position={[-0.02, 1.27, -1.89]} rotation={[Math.PI, -0.02, Math.PI]} />
      <mesh 
        onClick={() => props.setModalIsOpen(true)}
        geometry={nodes.photo.geometry} material={materials.photo} position={[-0.02, 1.24, -1.89]} rotation={[Math.PI, -0.02, Math.PI]} scale={0.09} >
          {/* testing out how to add cursor pointer based on mouse over the photo */}
          {/* docs here https://github.com/pmndrs/drei#cycleraycast */}
          {/* example use of useCursor https://codesandbox.io/s/ny3p4?file=/src/App.js:977-983 */}
          <CycleRaycast
            preventDefault={true} // Call event.preventDefault() (default: true)
            scroll={true} // Wheel events (default: true)
            keyCode={9} // Keyboard events (default: 9 [Tab])
            onChanged={(objects, cycle) => console.log(objects)} // Optional onChanged event
            onPointerOver={() => {console.log('hovered')}}
          />
        </mesh>
      <mesh geometry={nodes.glass.geometry} material={materials.glass} position={[-0.02, 1.27, -1.89]} rotation={[0, 0.02, 0]} />
    </group>
  )
}

useGLTF.preload('/meshes/main_scene.glb')

原文链接:3D Optimization for Web—How I Got a Model From 26MB Down to 560KB

BimAnt翻译整理,转载请标明出处