更新:
经过一番尝试发现了这种方式的局限
模型太大构建的八叉树结构也非常大
一个10万个点的模型构建的八叉树在控制台内存中居然有150M 而主线程在接受大量数据的时候又产生了堵塞💢
此方法适用模型稍小的场景 另外在优化这一方案的时候 我修改了八叉树的源码使其在接受worker传递的结构化数据不需要转换成octreeVector等Three变量也可正常进行八叉树检测 不过只写了capture 胶囊体碰撞 在文章最下面
最后:为了解决上述问题 采用了​​在web woker中进行八叉树碰撞检测​​ 这样主线程就不会阻塞 也不需要接收八叉树结构了


八叉树用来碰撞检测非常便捷 但是对于点多的模型在构建八叉树结构时会消耗大量时间 并且会阻塞js线程,既然​​模型可以在webWorker中解析​​ 那么八叉树亦然

原理 同解析模型相同 都是在worker中处理完传给主线程 主线程解析成真正有效的数据

实现
主线程加载模型 模型添加到环境中 处理好需要参与碰撞检测的世界环境
将这一环境转换成普通对象传入worker中
worker将传入对象还原成Three物体结构 并构建八叉树
将八叉树返回主线程
主线程接收到的是普通对象 再将普通对象转换成真是的八叉树结构 至此就可以使用这一八叉树进行碰撞检测了

优化:
这次使用web worker 发现从worker中向外发送内容 可以自动过滤掉函数 之前是不行的 带有函数向外传递会导致报错
处理数据转换时使用的代码还是之前的提取不是方法的属性传到主线程主线程在生成真实数据
此处有待优化

代码部分:

主线程加载模型 模型添加到环境中 处理好需要参与碰撞检测的世界环境
将这一环境转换成普通对象传入worker中

/** 构建八叉树的web worker */
const OctreeBuildWorker = new Worker(
new URL("../../src/ThreeHelper/worker/OctreeBuild.ts", import.meta.url)
);

OctreeBuildWorker.onmessage = (e) => {
if (e.data.type === "graphNodeBuildComplete") {
octreeControls.updateGraphNode(e.data.graphNode, () => {
console.log("解析八叉树完成");
});
}
};

OctreeBuildWorker.onerror = (err) => {
console.error("work出错:", err, err.message);
};

// 加载模型
const gltf = await helper.loadGltf("/models/scene.glb");
group.add(gltf.scene);

console.log(gltf);
const modelStruct = ModelTranslate.generateWorkerStruct(group);
OctreeBuildWorker.postMessage({ type: "build", modelStruct });

worker将传入对象还原成Three物体结构 并构建八叉树
将八叉树返回主线程

/*
* @Author: hongbin
* @Date: 2023-02-25 12:06:52
* @LastEditors: hongbin
* @LastEditTime: 2023-02-25 16:36:06
* @Description: 八叉树构建worker
*/
import { Octree } from "../expand/Octree";
import { ModelTranslate } from "./ModelTranslate";

class BuildFromGraphNode {
worldOctree!: Octree;

constructor() {
this.init();
}

init() {
this.worldOctree = new Octree();
}

fromGraphNode(obj: Object3D) {
const start = performance.now();
this.worldOctree.fromGraphNode(obj);
postMessage({
type: "graphNodeBuildComplete",
msg: `构建八叉树结构成功 用时 ${performance.now() - start}`,
graphNode: this.worldOctree,
});
}
}

const buildFromGraphNode = new BuildFromGraphNode();

/**
* 监听主线程发来的数信息
*/
onmessage = function (e) {
switch (e.data.type) {
case "build":
const model = ModelTranslate.parseWorkerStruct(e.data.modelStruct);
buildFromGraphNode.fromGraphNode(model);

break;
}
};

export {};

主线程接收到的是普通对象 再将普通对象转换成真是的八叉树结构

/**
* 格式化从web worker中拿到的八叉树结构
*/
formatSubTrees(subTree: IWorkerSubTree) {
const octree = new Octree();
const min = new THREE.Vector3().copy(subTree.box.min as Vector3);
const max = new THREE.Vector3().copy(subTree.box.max as Vector3);
octree["box"] = new THREE.Box3(min, max);
octree["triangles"] = subTree.triangles.map((triangle) => {
const a = new THREE.Vector3().copy(triangle.a as Vector3);
const b = new THREE.Vector3().copy(triangle.b as Vector3);
const c = new THREE.Vector3().copy(triangle.c as Vector3);
return new THREE.Triangle(a, b, c);
});
octree.subTrees = subTree.subTrees.map((subTree) =>
this.formatSubTrees(subTree)
);
return octree;
}

/**
* 使用从web worker 构建的八叉树结构
*/
updateGraphNode(subTrees: IWorkerSubTree, call?: VoidFunction) {
const Octree = this.formatSubTrees(subTrees);
this.worldOctree = Octree;
this.buildComplete = true;
//@ts-ignore
Octree._id = Math.random();
console.log(Octree);
call && call();
}
/*
* @Author: hongbin
* @Date: 2023-02-25 13:11:07
* @LastEditors: hongbin
* @LastEditTime: 2023-02-25 14:30:37
* @Description: 模型在web worker 和主线程之间转换
*/
import * as THREE from "three";
import {
IBaseProps,
IGroupParams,
THREEMaterialType,
IMeshParams,
IParams,
} from "../types/worker";

/**
******* 解析结构生成模型 代码*******
*/

/**
* 通过设置attributes index来复刻一个集合体
*/
const genGeometry = (geometry: IParams["geometry"]) => {
const geom = new THREE.BufferGeometry();
const {
attributes: { position, uv, normal },
index,
} = geometry;

//处理几何坐标
const attributes = {
position: new THREE.BufferAttribute(
position.array,
position.itemSize,
position.normalized
),
} as THREE.BufferGeometry["attributes"];

// 导出模型可能不带uv或法线
if (uv) {
attributes["uv"] = new THREE.BufferAttribute(
uv.array,
uv.itemSize,
uv.normalized
);
}
if (normal) {
attributes["normal"] = new THREE.BufferAttribute(
normal.array,
normal.itemSize,
normal.normalized
);
}

geom.attributes = attributes;
geom.index = index
? new THREE.BufferAttribute(
index.array,
index.itemSize,
index.normalized
)
: null;
return geom;
};

/**
* 根据传入纹理的参数生成真正有效的Material类型数据
*/
const genMaterial = (mate: IParams["material"]) => {
if (!mate) return undefined;
const multipleMaterial = Array.isArray(mate);
const material = multipleMaterial
? ([] as THREE.Material[])
: new THREE[mate.type as THREEMaterialType]();
//处理材质
//多个材质
if (multipleMaterial && Array.isArray(material)) {
for (const m of mate) {
const im = new THREE[m.type as THREEMaterialType]();
material.push(im);
}
} else if (mate) {
//单个材质
Object.assign(material, mate);
}
// console.log(mate, material);
return material;
};

/**
* 处理变换 matrix scale rotate translate position
*/
const setTransform = (params: IBaseProps, object: THREE.Object3D) => {
const matrix = new THREE.Matrix4();
matrix.elements = params.matrix.elements;
object.name = params.name;
object.matrix = matrix;
object.rotation.set(...params.rotation);
object.position.set(...params.position);
object.scale.set(...params.scale);
object.quaternion.set(...params.quaternion);
object.up.set(...params.up);
object.userData = params.userData;
object.visible = params.visible;
};

const pressMesh = (meshParams: IMeshParams) => {
const geometry = genGeometry(meshParams.geometry);
const material = genMaterial(meshParams.material);

const mesh = new THREE.Mesh(geometry, material);
setTransform(meshParams, mesh);
meshParams.children.length &&
mesh.add(...pressChildren(meshParams.children));
meshParams.animations.length &&
(mesh.animations = genAnimations(meshParams.animations));
return mesh;
};

const pressGroup = (groupParams: IGroupParams) => {
const group = new THREE.Group();
setTransform(groupParams, group);
groupParams.children.length &&
group.add(...pressChildren(groupParams.children));
groupParams.animations.length &&
(group.animations = genAnimations(groupParams.animations));
return group;
};

const pressChildren = (children: (IGroupParams | IMeshParams)[]) => {
const objectList: THREE.Object3D[] = [];
for (const child of children) {
if (child.hasOwnProperty("geometry")) {
objectList.push(pressMesh(child as IMeshParams));
} else {
objectList.push(pressGroup(child));
}
}
return objectList;
};

/**
* 生成动画
*/
const genAnimations = (sceneAnimations: IGroupParams["sceneAnimations"]) => {
const animations: THREE.AnimationClip[] = [];

for (const animation of sceneAnimations!) {
const clip = new THREE.AnimationClip(
animation.name,
animation.duration,
[],
animation.blendMode
);

for (const { name, times, values } of animation.tracks) {
const nreTrack = new THREE.QuaternionKeyframeTrack(
name,
times as any,
values as any
);
clip.tracks.push(nreTrack);
}

animations.push(clip);
}

return animations;
};

/**
* 解析传入的模型参数生成有效的three.js物体
*/
export const pressModel = (params: IGroupParams) => {
const model = pressGroup(params);
params.sceneAnimations &&
(model.animations = genAnimations(params.sceneAnimations));
return model;
};

/**
******* 解析模型 代码 *******
*/

/**
* 生成动画结构
*/
const genAnimationsStruct = (animations: THREE.AnimationClip[]) =>
animations.map((animation) => {
//删除这个方法就可以传递过去了
//@ts-ignore
animation["tracks"].forEach((t) => delete t["createInterpolant"]);
return animation;
});

/**
* 生成基本参数 旋转 位移 缩放等属性
*/
const genBaseStruct = (obj: THREE.Object3D): IBaseProps => {
const {
type,
name,
quaternion: q,
position: p,
rotation: r,
scale: s,
up: u,
userData,
visible,
matrix,
} = obj;
const quaternion: IBaseProps["quaternion"] = [q.x, q.y, q.z, q.w];
const position: IBaseProps["position"] = [p.x, p.y, p.z];
const rotation: IBaseProps["rotation"] = [r.x, r.y, r.z, r.order];
const scale: IBaseProps["scale"] = [s.x, s.y, s.z];
const up: IBaseProps["up"] = [u.x, u.y, u.z];

return {
type,
name,
quaternion,
position,
rotation,
scale,
up,
matrix,
userData,
visible,
children: genObject3DChildren(obj.children),
animations: genAnimationsStruct(obj.animations),
};
};

/**
* 生成物体参数
*/
const genMeshStruct = (mesh: THREE.Mesh) => {
const { geometry, material } = mesh;

return {
geometry,
material,
...genBaseStruct(mesh),
};
};

/**
* 生成子元素结构
*/
const genObject3DChildren = (children: THREE.Object3D[]) => {
const childStruct: IGroupParams["children"] = [];
for (const child of children) {
if (child.type === "Mesh") {
childStruct.push(genMeshStruct(child as THREE.Mesh));
} else if (child.type === "Group") {
childStruct.push(genGroupStruct(child as THREE.Group));
}
}
return childStruct;
};

/**
* 生成物体组结构
*/
const genGroupStruct = (group: THREE.Group) => {
const struct: IGroupParams = { ...genBaseStruct(group) };
return struct;
};

/**
* 模型转换
* 模型 -> web worker
* web worker -> 模型
*/
export class ModelTranslate {
/**
* 将模型解析成能传进 web worker的结构
* 最后返回一个对应模型结构的对象
*/
static generateWorkerStruct(group: THREE.Group) {
return genGroupStruct(group);
}
/**
* 解析模型结构 生成有效的3D模型返回一个Group
*/
static parseWorkerStruct(params: IGroupParams) {
return pressModel(params);
}
}

修改八叉树代码 可以直接使用worker传递过来的结构

/*
* @Author: hongbin
* @Date: 2023-02-26 11:09:40
* @LastEditors: hongbin
* @LastEditTime: 2023-02-26 12:51:48
* @Description: 修改THREE的八叉树 与webWorker传递的没有方法的八叉树结构进行检测 - 使用胶囊体检测
*/

import { Box3, Line3, Plane, Triangle, Vector3 } from "three";
import { Capsule } from "./Capsule";

const _v1 = new Vector3();
const _plane = new Plane();
const _line1 = new Line3();
const _line2 = new Line3();
const _capsule = new Capsule();

type TResult = { normal: Vector3; depth: number } | boolean;

interface VVector3 {
x: number;
y: number;
z: number;
}

interface IWorkerSubTree {
box: {
isBox3: true;
max: VVector3;
min: VVector3;
};
triangles: { a: VVector3; b: VVector3; c: VVector3 }[];
subTrees: IWorkerSubTree[];
}

class Octree {
triangles: Triangle[];
box = new Box3();
tempBox = new Box3();
tempTriangle = new Triangle();
subTrees: IWorkerSubTree[];
root: IWorkerSubTree;

constructor(tree: IWorkerSubTree) {
this.root = tree;
this.copyBox(tree.box, this.box);
this.triangles = [];
this.subTrees = tree.subTrees;
}

copyBox(treeBox: IWorkerSubTree["box"], box: Box3) {
const min = new Vector3().copy(treeBox.min as Vector3);
const max = new Vector3().copy(treeBox.max as Vector3);
box.set(min, max);
return box;
}

setTempTriangle(triangle: IWorkerSubTree["triangles"][number]) {
const a = new Vector3().copy(triangle.a as Vector3);
const b = new Vector3().copy(triangle.b as Vector3);
const c = new Vector3().copy(triangle.c as Vector3);
this.tempTriangle.set(a, b, c);
}

triangleCapsuleIntersect(
capsule: Capsule,
triangle: IWorkerSubTree["triangles"][number]
) {
this.setTempTriangle(triangle);
this.tempTriangle.getPlane(_plane);

const d1 = _plane.distanceToPoint(capsule.start) - capsule.radius;
const d2 = _plane.distanceToPoint(capsule.end) - capsule.radius;

if (
(d1 > 0 && d2 > 0) ||
(d1 < -capsule.radius && d2 < -capsule.radius)
) {
return false;
}

const delta = Math.abs(d1 / (Math.abs(d1) + Math.abs(d2)));
const intersectPoint = _v1.copy(capsule.start).lerp(capsule.end, delta);

if (this.tempTriangle.containsPoint(intersectPoint)) {
return {
normal: _plane.normal.clone(),
point: intersectPoint.clone(),
depth: Math.abs(Math.min(d1, d2)),
};
}
return false;
}

getCapsuleTriangles(
capsule: Capsule,
triangles: IWorkerSubTree["triangles"],
root: IWorkerSubTree
) {
for (let i = 0; i < root.subTrees.length; i++) {
const subTree = root.subTrees[i];
this.copyBox(subTree.box, this.tempBox);
if (!capsule.intersectsBox(this.tempBox)) continue;

if (subTree.triangles.length > 0) {
for (let j = 0; j < subTree.triangles.length; j++) {
// this.setTempTriangle(subTree.triangles[j]);
if (triangles.indexOf(subTree.triangles[j]) === -1)
triangles.push(subTree.triangles[j]);
}
} else {
this.getCapsuleTriangles(capsule, triangles, subTree);
}
}
}

capsuleIntersect(capsule: Capsule) {
_capsule.copy(capsule);

const triangles: IWorkerSubTree["triangles"] = [];
let result: TResult,
hit = false;

this.getCapsuleTriangles(_capsule, triangles, this.root);

for (let i = 0; i < triangles.length; i++) {
if (
(result = this.triangleCapsuleIntersect(_capsule, triangles[i]))
) {
hit = true;

_capsule.translate(result.normal.multiplyScalar(result.depth));
}
}

if (hit) {
const collisionVector = _capsule
.getCenter(new Vector3())
.sub(capsule.getCenter(_v1));
const depth = collisionVector.length();

return { normal: collisionVector.normalize(), depth: depth };
}

return false;
}
}

export { Octree };

使用

import { Octree as WorkerOctree } from "../expand/WorkerOctree";
//...

const result = this.worldWorldOctree.capsuleIntersect(this.playerCollider);
//...

/**
* 使用从web worker 构建的八叉树结构
*/
updateGraphNode(subTree: IWorkerSubTree, call?: VoidFunction) {
console.time();
console.log(subTree);
this.worldWorldOctree = new WorkerOctree(subTree);
console.timeEnd();
call && call();
}