Visual LabINTERACTIVE LEARNING
OpenGL 与 3D第 2 / 4 章

Worker 端:glTF 加载流程

处理文件映射、LoadingManager、加载进度和资源释放。

Web WorkerglTF性能
专题预计 44 分钟查看源文 ↗

worker-load

async function loadAndMergeGLTF(model_id, files, isMainModel = false) {
  let blobs = new Map()
  let gltfName = ''
  let isBulked = false // 标识模型是单个文件还是分离的文件
  files.forEach(file => {
    const { name: modelName } = file
    blobs.set(modelName, file)
    if (modelName.toLowerCase().endsWith('.gltf') || modelName.toLowerCase().endsWith('.glb')) {
      gltfName = modelName
    }
    if (modelName.toLowerCase().endsWith('.bin')) {
      isBulked = true
    }
  })

  // 取出内存中的 map
  let fileURLMap = new Map()
  let mainModelUrl = null
  let loadUrl
  const args = {
    side: isMainModel ? DoubleSide : FrontSide,
    isStandMat: isMainModel,
    manager: null,
  }
  if (!isBulked) {
    mainModelUrl = URL.createObjectURL(blobs.get(gltfName)) // 单个文件直接做成 ObjectURL
    fileURLMap.set(gltfName, mainModelUrl)
    loadUrl = mainModelUrl
  } else {
    const manager = new LoadingManager()
    loadUrl = gltfName
    manager.setURLModifier(url => {
      const pruneUrl = url.startsWith('./') ? url.slice(2) : url
      let objUrl = fileURLMap.get(pruneUrl)
      if (!objUrl) {
        //map中不存在url
        objUrl = URL.createObjectURL(blobs.get(pruneUrl))
        fileURLMap.set(pruneUrl, objUrl)
      }
      return objUrl
    })
    manager.onProgress = (_url, itemsLoaded, itemsTotal) => {
      postMessage({
        model_id,
        percent: ((itemsLoaded / itemsTotal) * 100 - 3) | 0,
        type: 2,
      })
    }
    manager.onError = url => {
      postMessage({
        model_id,
        type: 3,
        error: `Manager There was an error loading ${url}`,
      })
    }
    args.manager = manager
  }

  return new Promise((resolve, reject) => {
    const gltfLoader = new CustGLTFLoader(args)
    gltfLoader.load(
      loadUrl,
      async gltf => {
        const { scene: modelGroup } = gltf
        //
        // 主模型则要释放所有的url
        fileURLMap.forEach(url => URL.revokeObjectURL(url))
        //释放内存
        fileURLMap = null
        //释放内存
        blobs = null
        resolve(modelGroup)
      },
      progress => {
        if (progress.total !== 0 && progress.total >= progress.loaded) {
          //单个文件加载使用这个方法回调计算进度
          if (!isBulked) {
            postMessage({
              percent: Math.floor((100 * progress.loaded) / progress.total) - 3,
              model_id,
              type: 2,
            })
          }
        }
      },
      error => {
        blobs = null //释放内存
        postMessage({
          model_id,
          type: 3,
          error: `CustGLTFLoader onError => ${error}`,
        })
        reject(error)
      }
    )
  })
}

Rebuild

public async loadModelFromFile(args: ILoadModel) {
    const { model_id, files, onProgress, isMainModel = false } = args
    // let loaderWorker = null
    window.three = this
    let loaderWorker = new myworker()
    this.loadingModel = true
    if (loaderWorker) {
      const filesArrayBuffer = await Promise.all(files.map(file => file.arrayBuffer()))
      const msg = { model_id, isMainModel, files }
      loaderWorker.postMessage(msg, filesArrayBuffer)
      onProgress &&
        onProgress({
          model_id,
          percent: 3,
          type: 2,
        })
      return new Promise<THREE.Group | void>((resolve, reject) => {
        loaderWorker.onmessage = e => {
          const { type } = e.data
          switch (type) {
            case 1:
              this.loadingModel = true
              break
            case 2: // 配景模型加载进度
              onProgress && onProgress(e.data)
              break
            case 3: // 模型加载错误
              console.error(e.data.error)
              message.error('【gltf】解析模型错误', {
                duration: 0,
                closable: true,
              })
              this.loadingModel = false
              loaderWorker.terminate()
              loaderWorker = null
              reject(e.data.error)
              break
            case 4: // 主模型加载完成
              resolve(this.reBuildMesh(e.data))
              onProgress &&
                onProgress({
                  model_id,
                  percent: 101,
                  type: 2,
                })
              this.loadingModel = false
              loaderWorker.terminate()
              loaderWorker = null
              break
          }
        }
      })
    }
  }

  public reBuildMesh(data: { geometryData: Record<string, IGeometryData> }) {
    const modelGroup = new THREE.Group()
    const { geometryData } = data
    for (const key in geometryData) {
      const { attributes, index, materialProps, originalMeshes, groups } = geometryData[key]
      // Recreate the geometry
      const geometry = new THREE.BufferGeometry()
      for (const attrName in attributes) {
        const attr = attributes[attrName]
        geometry.setAttribute(attrName, new THREE.BufferAttribute(attr.array, attr.itemSize, attr.normalized))
      }
      if (index) {
        geometry.setIndex(new THREE.BufferAttribute(index.array, 1))
      }

      // Recreate the material
      let material: THREE.MeshBasicMaterial | THREE.MeshStandardMaterial = null
      const { jsons } = materialProps
      if (materialProps.type === 'MeshStandardMaterial') {
        // 标准材质,如有金属贴图和粗糙度贴图,则需要单独处理,否则直接使用
        material = new THREE.MeshStandardMaterial(jsons)
        if (materialProps.metalnessMap) {
          material.metalnessMap = new THREE.Texture(materialProps.metalnessMap)
          material.metalnessMap.needsUpdate = true
        }
        if (materialProps.roughnessMap) {
          material.roughnessMap = new THREE.Texture(materialProps.roughnessMap)
          material.roughnessMap.needsUpdate = true
        }
        // material.envMap = this.envMap
      } else {
        // Default to MeshBasicMaterial if type is not recognized
        material = new THREE.MeshBasicMaterial(jsons)
      }
      material.onBeforeCompile = shader => {
        // 添加 uniform
        shader.uniforms.uvScale = { value: new THREE.Vector2(1.0, 1.0) }
        // 添加 uniform 声明
        const uv_vertex_glsl = uv_vertex.replace(
          'vMapUv = ( mapTransform * vec3( MAP_UV, 1 ) ).xy;',
          'vMapUv = ( mapTransform * vec3( MAP_UV * uvScale, 1 ) ).xy;'
        )
        // 添加 uniform 声明
        shader.vertexShader = shader.vertexShader.replace(
          '#include <common>',
          `
#include <common>
uniform vec2 uvScale;
          `
        )
        // 修改 MAP_UV 的使用方式
        shader.vertexShader = shader.vertexShader.replace(
          '#include <uv_vertex>',
          `
${uv_vertex_glsl}
          `
        )
        // 保存 shader 引用以便后续更新
        material.userData.shader = shader
      }

      // Set the texture if it exists
      if (materialProps.map) {
        const texture = new THREE.Texture(materialProps.map)
        texture.name = materialProps.jsons.name
        texture.needsUpdate = true
        texture.flipY = false
        texture.wrapS = texture.wrapT = THREE.RepeatWrapping // 相当于 x 轴的平铺方式为镜像平铺
        texture.colorSpace = THREE.SRGBColorSpace // 纹理编码方式

        material.map = texture
      }

      material.name = materialProps.jsons.name
      if (groups) {
        geometry.groups = groups
      }
      // 保存材质
      this.mtlsMap[materialProps.jsons.name] = material
      // Create the mesh and add it to the group
      const mesh = new MergeMesh(geometry, material, originalMeshes)
      mesh.castShadow = true // 对象是否被渲染到阴影贴图中
      mesh.receiveShadow = true
      // 保存uuid与原始材质的映射关系
      this.initMtlIdNameMaps[material.uuid] = materialProps.jsons.name
      // 保存原始材质uuid与mesh的映射关系
      this.initMtlIdMeshMaps[material.uuid] = mesh
      // 保存原始材质uuid
      mesh.userData['muuid'] = material.uuid
      // 保存uuid与mesh的映射关系
      this.meshesMap[mesh.uuid] = mesh

      modelGroup.add(mesh)
    }
    return modelGroup
  }