OpenGL 与 3D第 2 / 4 章
Worker 端:glTF 加载流程
处理文件映射、LoadingManager、加载进度和资源释放。
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
}