OpenGL 与 3D精校教程
Web Worker 模型加载器
把 glTF 解析、几何合并和可转移对象搬到 Worker,降低大模型处理对主线程的阻塞。
worker
重写了加载过程
import CustGLTFLoader from '@/workers/gltf-loader.js'
import { LoadingManager, FrontSide, DoubleSide, Mesh } from 'three'
import { mergeGeometries } from './BufferGeometryUtils.js'
self.onmessage = async function (e) {
const { isMainModel, files, model_id } = e.data
postMessage({ type: 1 })
const modelGroup = await loadAndMergeGLTF(model_id, files, isMainModel)
if (modelGroup) {
genMainModelMsg(modelGroup)
}
}
async function genMainModelMsg(modelGroup) {
const materialMaps = {}
modelGroup.updateMatrixWorld()
modelGroup.traverse(mesh => {
if (mesh instanceof Mesh) {
if (mesh.material.uuid in materialMaps) {
materialMaps[mesh.material.uuid].meshes.push(mesh)
} else {
materialMaps[mesh.material.uuid] = {
material: mesh.material,
meshes: [mesh],
}
}
}
})
const geometryMaps = {}
for (const key in materialMaps) {
const { material, meshes } = materialMaps[key]
const geometryList = meshes.map((mesh, index) => {
const geometry = mesh.geometry.clone() //没必要克隆
geometry.applyMatrix4(mesh.matrixWorld)
mesh.geometry.dispose()
return geometry
})
const mergedGeometry = mergeGeometries(geometryList, true)
if(mergedGeometry){
mergedGeometry.computeVertexNormals()
} else {
// 合并失败,则不处理
continue
}
geometryMaps[key] = {
material,
geometry: mergedGeometry,
groups: mergedGeometry.groups,
originalMeshes: meshes.map(mesh => {
const originalGeometry = mesh.geometry
return {
boundingBox: originalGeometry.boundingBox.clone().applyMatrix4(mesh.matrixWorld),
boundingSphere: originalGeometry.boundingSphere.clone().applyMatrix4(mesh.matrixWorld),
assimpMeshName: mesh.userData.assimpMeshName,
}
}),
}
}
const transferableObjects= []
const geometryData = {}
for (const key in geometryMaps) {
const { material, geometry, originalMeshes } = geometryMaps[key]
const buffers = {}
for (const attributeName in geometry.attributes) {
const attribute = geometry.attributes[attributeName]
buffers[attributeName] = attribute.array.buffer
transferableObjects.push(attribute.array.buffer)
}
if (geometry.index) {
buffers['index'] = geometry.index.array.buffer
transferableObjects.push(geometry.index.array.buffer)
}
const jsons = material.toJSON()
//删除无用的属性
for (const key of ['images', 'metadata', 'textures', 'envMapRotation']) {
if (Reflect.has(jsons, key)) delete jsons[key]
}
geometryData[key] = {
attributes: Object.keys(geometry.attributes).reduce((acc, name) => {
const attr = geometry.attributes[name]
acc[name] = {
itemSize: attr.itemSize,
normalized: attr.normalized,
array: attr.array,
}
return acc
}, {}),
index: geometry.index
? {
array: geometry.index.array,
}
: null,
materialProps: {
jsons,
type: material.type,
map: material.map?.source?.data instanceof ImageBitmap ? material.map.source.data : null,
metalnessMap: material.metalnessMap?.source?.data instanceof ImageBitmap ? material.metalnessMap.source.data : null,
roughnessMap: material.roughnessMap?.source?.data instanceof ImageBitmap ? material.roughnessMap.source.data : null,
},
originalMeshes: originalMeshes.map(mesh => ({
boundingBox: {
min: { x: mesh.boundingBox.min.x, y: mesh.boundingBox.min.y, z: mesh.boundingBox.min.z },
max: { x: mesh.boundingBox.max.x, y: mesh.boundingBox.max.y, z: mesh.boundingBox.max.z },
},
boundingSphere: {
center: { x: mesh.boundingSphere.center.x, y: mesh.boundingSphere.center.y, z: mesh.boundingSphere.center.z },
radius: mesh.boundingSphere.radius,
},
assimpMeshName: mesh.assimpMeshName,
})),
groups: geometry.groups,
}
if (geometryData[key].materialProps.map) {
transferableObjects.push(geometryData[key].materialProps.map)
}
material.dispose()
}
self.postMessage(
{
type: 4,
geometryData,
},
transferableObjects
)
}
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
}
mergeMesh
重写了mesh的实现,拾取交点的判定方法
import { Vector3, Vector2, Sphere, Matrix4, Ray, Mesh, Triangle, BackSide, FrontSide, MeshBasicMaterial, BufferGeometry } from 'three'
const _inverseMatrix = /*@__PURE__*/ new Matrix4()
const _ray = /*@__PURE__*/ new Ray()
const _sphere = /*@__PURE__*/ new Sphere()
const _sphereHitAt = /*@__PURE__*/ new Vector3()
const _vA = /*@__PURE__*/ new Vector3()
const _vB = /*@__PURE__*/ new Vector3()
const _vC = /*@__PURE__*/ new Vector3()
const _tempA = /*@__PURE__*/ new Vector3()
const _morphA = /*@__PURE__*/ new Vector3()
const _uvA = /*@__PURE__*/ new Vector2()
const _uvB = /*@__PURE__*/ new Vector2()
const _uvC = /*@__PURE__*/ new Vector2()
const _normalA = /*@__PURE__*/ new Vector3()
const _normalB = /*@__PURE__*/ new Vector3()
const _normalC = /*@__PURE__*/ new Vector3()
const _intersectionPoint = /*@__PURE__*/ new Vector3()
const _intersectionPointWorld = /*@__PURE__*/ new Vector3()
merge-raycast
class MergeMesh extends Mesh {
constructor(geometry = new BufferGeometry(), material = new MeshBasicMaterial(), originalMeshes = []) {
super()
this.isMesh = true
this.type = 'Mesh'
this.geometry = geometry
this.material = material
// 原始的mesh
this.originalMeshes = originalMeshes ?? []
this.updateMorphTargets()
}
raycast(raycaster, intersects) {
// console.log('raycast :>> ')
const geometry = this.geometry
const material = this.material
const matrixWorld = this.matrixWorld
if (material === undefined) return
// test with bounding sphere in world space
if (geometry.boundingSphere === null) geometry.computeBoundingSphere()
_sphere.copy(geometry.boundingSphere)
_sphere.applyMatrix4(matrixWorld)
// check distance from ray origin to bounding sphere
_ray.copy(raycaster.ray).recast(raycaster.near)
if (_sphere.containsPoint(_ray.origin) === false) {
if (_ray.intersectSphere(_sphere, _sphereHitAt) === null) return
if (_ray.origin.distanceToSquared(_sphereHitAt) > (raycaster.far - raycaster.near) ** 2) return
}
// console.log('outer sphere')
// convert ray to local space of mesh
_inverseMatrix.copy(matrixWorld).invert()
_ray.copy(raycaster.ray).applyMatrix4(_inverseMatrix)
// test with bounding box in local space
//
if (geometry.boundingBox !== null) {
if (_ray.intersectsBox(geometry.boundingBox) === false) return
}
// 遍历每个合并前的原始mesh
if (this.originalMeshes.length > 0){
for (let i = 0, il = this.originalMeshes.length; i < il; i++) {
const { boundingBox, boundingSphere } = this.originalMeshes[i]
const { start, count } = this.geometry.groups[i]
// 射线检测boundingSphere
if (boundingSphere !== null) {
_sphere.copy(boundingSphere)
_sphere.applyMatrix4(matrixWorld)
_ray.copy(raycaster.ray).recast(raycaster.near)
if (_sphere.containsPoint(_ray.origin) === false) {
if (_ray.intersectSphere(_sphere, _sphereHitAt) === null) continue
if (_ray.origin.distanceToSquared(_sphereHitAt) > (raycaster.far - raycaster.near) ** 2) continue
}
}
// 射线检测boundingBox
_inverseMatrix.copy(matrixWorld).invert()
_ray.copy(raycaster.ray).applyMatrix4(_inverseMatrix)
// test with bounding box in local space
if (boundingBox !== null) {
if (_ray.intersectsBox(boundingBox) === false) continue
}
// console.log('inner box')
// 射线检测每个原始mesh的三角面
this._computeIntersections(raycaster, intersects, _ray, start, count)
}
} else {
this._computeIntersections(raycaster, intersects, _ray)
}
// test for intersections with geometry
}
/**
* 主要改写这个方法,实现合并mesh的射线检测,首先检测每个子mesh的射线检测,
* 然后合并结果,子mesh的射线检测,需要根据顶点的偏移量,来计算射线检测
* 也是需要计算距离的
* 1. 其实每个子mesh的计算过程,都是一样的,都是通过射线检测,然后计算距离,然后判断是否在射线检测的范围内
* 2. 子mesh的射线检测,需要根据顶点的偏移量,来计算射线检测
*/
_computeIntersections(raycaster, intersects, rayLocalSpace, start = 0, count = Infinity) {
let intersection
const geometry = this.geometry
const material = this.material
const index = geometry.index
const position = geometry.attributes.position
const uv = geometry.attributes.uv
const uv1 = geometry.attributes.uv1
const normal = geometry.attributes.normal
const groups = geometry.groups
const drawRange = geometry.drawRange
const startIndex = Math.max(start, drawRange.start)
const endIndex = Math.min(start + count, drawRange.start + drawRange.count)
if (index !== null) {
// indexed buffer geometry
if (Array.isArray(material)) {
for (let i = 0, il = groups.length; i < il; i++) {
const group = groups[i]
const groupMaterial = material[group.materialIndex]
const groupStart = Math.max(group.start, startIndex)
const groupEnd = Math.min(index.count, Math.min(group.start + group.count, endIndex))
for (let j = groupStart, jl = groupEnd; j < jl; j += 3) {
const a = index.getX(j)
const b = index.getX(j + 1)
const c = index.getX(j + 2)
intersection = checkGeometryIntersection(this, groupMaterial, raycaster, rayLocalSpace, uv, uv1, normal, a, b, c)
if (intersection) {
intersection.faceIndex = Math.floor(j / 3) // triangle number in indexed buffer semantics
intersection.face.materialIndex = group.materialIndex
intersects.push(intersection)
}
}
}
} else {
for (let i = startIndex, il = endIndex; i < il; i += 3) {
const a = index.getX(i)
const b = index.getX(i + 1)
const c = index.getX(i + 2)
intersection = checkGeometryIntersection(this, material, raycaster, rayLocalSpace, uv, uv1, normal, a, b, c)
if (intersection) {
intersection.faceIndex = Math.floor(i / 3) // triangle number in indexed buffer semantics
intersects.push(intersection)
}
}
}
} else if (position !== undefined) {
// non-indexed buffer geometry
if (Array.isArray(material)) {
for (let i = 0, il = groups.length; i < il; i++) {
const group = groups[i]
const groupMaterial = material[group.materialIndex]
const groupStart = Math.max(group.start, startIndex)
const groupEnd = Math.min(position.count, Math.min(group.start + group.count, endIndex))
for (let j = groupStart, jl = groupEnd; j < jl; j += 3) {
const a = j
const b = j + 1
const c = j + 2
intersection = checkGeometryIntersection(this, groupMaterial, raycaster, rayLocalSpace, uv, uv1, normal, a, b, c)
if (intersection) {
intersection.faceIndex = Math.floor(j / 3) // triangle number in non-indexed buffer semantics
intersection.face.materialIndex = group.materialIndex
intersects.push(intersection)
}
}
}
} else {
for (let i = startIndex, il = endIndex; i < il; i += 3) {
const a = i
const b = i + 1
const c = i + 2
intersection = checkGeometryIntersection(this, material, raycaster, rayLocalSpace, uv, uv1, normal, a, b, c)
if (intersection) {
intersection.faceIndex = Math.floor(i / 3) // triangle number in non-indexed buffer semantics
intersects.push(intersection)
}
}
}
}
}
}
function checkIntersection(object, material, raycaster, ray, pA, pB, pC, point) {
let intersect
if (material.side === BackSide) {
intersect = ray.intersectTriangle(pC, pB, pA, true, point)
} else {
intersect = ray.intersectTriangle(pA, pB, pC, material.side === FrontSide, point)
}
if (intersect === null) return null
_intersectionPointWorld.copy(point)
_intersectionPointWorld.applyMatrix4(object.matrixWorld)
const distance = raycaster.ray.origin.distanceTo(_intersectionPointWorld)
if (distance < raycaster.near || distance > raycaster.far) return null
return {
distance: distance,
point: _intersectionPointWorld.clone(),
object: object,
}
}
function checkGeometryIntersection(object, material, raycaster, ray, uv, uv1, normal, a, b, c) {
object.getVertexPosition(a, _vA)
object.getVertexPosition(b, _vB)
object.getVertexPosition(c, _vC)
const intersection = checkIntersection(object, material, raycaster, ray, _vA, _vB, _vC, _intersectionPoint)
if (intersection) {
if (uv) {
_uvA.fromBufferAttribute(uv, a)
_uvB.fromBufferAttribute(uv, b)
_uvC.fromBufferAttribute(uv, c)
intersection.uv = Triangle.getInterpolation(_intersectionPoint, _vA, _vB, _vC, _uvA, _uvB, _uvC, new Vector2())
}
if (uv1) {
_uvA.fromBufferAttribute(uv1, a)
_uvB.fromBufferAttribute(uv1, b)
_uvC.fromBufferAttribute(uv1, c)
intersection.uv1 = Triangle.getInterpolation(_intersectionPoint, _vA, _vB, _vC, _uvA, _uvB, _uvC, new Vector2())
}
if (normal) {
_normalA.fromBufferAttribute(normal, a)
_normalB.fromBufferAttribute(normal, b)
_normalC.fromBufferAttribute(normal, c)
intersection.normal = Triangle.getInterpolation(_intersectionPoint, _vA, _vB, _vC, _normalA, _normalB, _normalC, new Vector3())
if (intersection.normal.dot(ray.direction) > 0) {
intersection.normal.multiplyScalar(-1)
}
}
const face = {
a: a,
b: b,
c: c,
normal: new Vector3(),
materialIndex: 0,
}
Triangle.getNormal(_vA, _vB, _vC, face.normal)
intersection.face = face
}
return intersection
}
export { MergeMesh }