基於three.js的Instanced Draw+LOD+Frustum Cull的改進實現

杨元超發表於2024-05-24

大家好,本文在上文的基礎上,最佳化了Instanced Draw+LOD+Frustum Cull的效能,效能提升了3倍以上

關鍵詞:three.js、Instanced Draw、大場景、LOD、Frustum Cull、最佳化、Web3D、WebGL、開源

上文:
three.js使用Instanced Draw+Frustum Cull+LOD來渲染大場景(開源)

相對於上文的改進點

相對於上文的Octree,本文的Octree直接遍歷世界矩陣而不是Mesh,從而提高了效能

相對於上文的Instanced LOD,本文的Instanced LOD簡化了資料結構,並且不再透過交換來實現cull,從而提高了效能

本文改進的程式碼

呼叫程式碼:

let first = new THREE.Group()
...
first.add(mesh)

let details = [
	//第一級LOD
	{
		group: first,
		level: "l0",
		distance: 800,
	},
	//第二級LOD...
	{
		group: second,
		level: "l1",
		distance: 1000,
	},
	...
]

let octree = new Octree(boundingBox, 5, 0)


let camera = 獲得當前相機

let instancedlod = new InstancedLOD(staticGroup, camera, "lod")

instancedlod.setOctree(octree);
instancedlod.setLevels(details, true);
instancedlod.setPopulation();

...


在主迴圈中呼叫:
instancedlod.update()

Octree

import * as THREE from "three";

class Octree {
	public box
	public capacity
	public divided
	public transforms
	public children
	public depth

	constructor(box3, n, depth) {
		this.box = box3;
		this.capacity = n;
		this.divided = false;
		this.transforms = [];
		this.children = [];
		this.depth = depth;
	}

	subdivide() {
		const { box, capacity, depth } = this;
		let size = new THREE.Vector3().subVectors(box.max, box.min).divideScalar(2);
		let arr = [
			[0, 0, 0],
			[size.x, 0, 0],
			[0, 0, size.z],
			[size.x, 0, size.z],
			[0, size.y, 0],
			[size.x, size.y, 0],
			[0, size.y, size.z],
			[size.x, size.y, size.z],
		];
		for (let i = 0; i < 8; i++) {
			let min = new THREE.Vector3(
				box.min.x + arr[i][0],
				box.min.y + arr[i][1],
				box.min.z + arr[i][2]
			);
			let max = new THREE.Vector3().addVectors(min, size);
			let newbox = new THREE.Box3(min, max);
			this.children.push(new Octree(newbox, capacity, depth + 1));
		}
		this.divided = true;
	}

	insert(transform) {
		const { box, transforms, capacity, divided, children } = this;
		if (
			!box.containsPoint(new THREE.Vector3().setFromMatrixPosition(transform))
		)
			return false;
		if (transforms.length < capacity) {
			transforms.push(transform);
			return true;
		} else {
			if (!divided) this.subdivide();
			for (let i = 0; i < children.length; i++) {
				if (children[i].insert(transform)) return true;
			}
		}
	}

	queryByBox(boxRange, found = []) {
		if (!this.box.intersectsBox(boxRange)) {
			return found;
		} else {
			for (let transform of this.transforms) {
				if (
					boxRange.containsPoint(
						new THREE.Vector3().setFromMatrixPosition(transform)
					)
				) {
					found.push(transform);
				}
			}
			if (this.divided) {
				this.children.forEach((child) => {
					child.queryByBox(boxRange, found);
				});
			}
			return found;
		}
	}

	queryBySphere(
		sphereRange,
		boundingBox = sphereRange.getBoundingBox(new THREE.Box3()),
		found = []
	) {
		if (!this.box.intersectsBox(boundingBox)) {
			return found;
		} else {
			for (let transform of this.transforms) {
				if (
					sphereRange.containsPoint(
						new THREE.Vector3().setFromMatrixPosition(transform)
					)
				) {
					found.push(transform);
				}
			}
			if (this.divided) {
				this.children.forEach((child) => {
					child.queryBySphere(sphereRange, boundingBox, found);
				});
			}
			return found;
		}
	}

	queryByFrustum(frustum, found = []) {
		if (!frustum.intersectsBox(this.box)) {
			return found;
		} else {
			for (let transform of this.transforms) {
				if (
					frustum.containsPoint(
						new THREE.Vector3().setFromMatrixPosition(transform)
					)
				) {
					found.push(transform);
				}
			}
			if (this.divided) {
				this.children.forEach((child) => {
					child.queryByFrustum(frustum, found);
				});
			}
			return found;
		}
	}

	display(scene) {
		// 葉子結點
		if (!this.divided && this.transforms.length > 0) {
			scene.add(new THREE.Box3Helper(this.box, 0x00ff00));
			return;
		}
		this.children.forEach((child) => {
			child.display(scene);
		});
	}
}

export { Octree };

Contract(用於契約檢查)

export let buildAssetMessage = (expect:string, actual = "not as expect") => {
    return `expect ${expect}, but actual ${actual}`;
}

export let test = (message: string, func: () => boolean): void => {
    if (func() !== true) {
        throw new Error(message);
    }
}

export let requireCheck = (func: () => void, isTest: boolean): void => {
    if (!isTest) {
        return;
    }

    func();
}

export function ensureCheck<T extends any>(returnVal: T, func: (returnVal: T) => void, isTest: boolean): T {
    if (!isTest) {
        return returnVal;
    }

    func(returnVal);

    return returnVal;
}

export function assertPass() {
    return true;
}

export function assertTrue(source: boolean) {
    return source === true;
}

export function assertFalse(source: boolean) {
    return source === false;
}

function _isNullableExist<T extends any>(source: T): T extends null ? never : T extends undefined ? never : boolean;
function _isNullableExist(source:any) {
    return source !== undefined && source !== null;
};

export let assertNullableExist = _isNullableExist;

// export function assertEqual<S extends any, T extends any>(source: S, target: T): S extends T ? true : false;
export function assertEqual<S extends number, T extends number>(source: S, target: T): S extends T ? true : false;
export function assertEqual<S extends string, T extends string>(source: S, target: T): S extends T ? true : false;
export function assertEqual<S extends boolean, T extends boolean>(source: S, target: T): S extends T ? true : false;
export function assertEqual<S extends number | string | boolean, T extends number | string | boolean>(source: S, target: T): false;
export function assertEqual(source:any, target:any) {
    return source == target;
}

export function assertNotEqual<S extends number, T extends number>(source: S, target: T): S extends T ? false : true;
export function assertNotEqual<S extends string, T extends string>(source: S, target: T): S extends T ? false : true;
export function assertNotEqual<S extends boolean, T extends boolean>(source: S, target: T): S extends T ? false : true;
export function assertNotEqual<S extends number | string | boolean, T extends number | string | boolean>(source: S, target: T): true;
export function assertNotEqual(source:any, target:any) {
    return source != target;
}

export function assertGt(source: number, target: number) {
    return source > target;
}

export function assertGte(source: number, target: number) {
    return source >= target;
}

export function assertLt(source: number, target: number) {
    return source < target;
}

export function assertLte(source: number, target: number) {
    return source <= target;
}

InstancedLOD

import * as THREE from "three";
import { requireCheck, test } from "./Contract";

let count = 0
class InstancedLOD {
	public treeSpecies
	public numOfLevel
	public scene
	public camera
	public levels
	public instancedMeshOfAllLevel: Array<
		{
			meshes: Array<THREE.Mesh>,
			count: number,
			matrix4: Array<THREE.Matrix4>,
			castShadow: boolean,
			receiveShadow: boolean
		}>
	public groupOfInstances


	public octree

	public frustum
	public worldProjectionMatrix
	public obj_position
	public cur_dist
	public cur_level

	constructor(scene, camera, treeSpecies) {
		this.treeSpecies = treeSpecies;
		this.numOfLevel = 0;
		this.scene = scene;
		this.camera = camera;
		this.levels;
		this.instancedMeshOfAllLevel;
		this.groupOfInstances;

		this.frustum = new THREE.Frustum();
		this.worldProjectionMatrix = new THREE.Matrix4();
		this.obj_position = new THREE.Vector3();
		this.cur_dist = 0;
		this.cur_level = 0;
	}

	setOctree(octree) {
		this.octree = octree;
	}

	extractMeshes(group) {
		return group.children
	}

	setLevels(array, isDebug) {
		requireCheck(() => {
			let group = array[0].group

			test("meshs should be first level children", () => {
				return group.children.reduce((result, child: THREE.Mesh) => {
					if (!result) {
						return result
					}

					return child.isMesh && child.children.length == 0
				}, true)
			})
			test("transform should be default", () => {
				return group.children.reduce((result, child: THREE.Mesh) => {
					if (!result) {
						return result
					}

					return child.position.equals(new THREE.Vector3(0, 0, 0)) && child.rotation.equals(new THREE.Euler(0, 0, 0)) && child.scale.equals(new THREE.Vector3(1, 1, 1))
				}, true)
			})
		}, isDebug)

		this.numOfLevel = array.length;
		this.levels = new Array(this.numOfLevel);
		this.instancedMeshOfAllLevel = new Array(this.numOfLevel); // array of { mesh:[], count, matrix4:[] }
		this.groupOfInstances = new Array(this.numOfLevel); // array of THREE.Group(), each Group -> tree meshes in each level
		for (let i = 0; i < this.numOfLevel; i++) {
			this.levels[i] = array[i].distance;
			let group = array[i].group
			this.instancedMeshOfAllLevel[i] = {
				meshes: this.extractMeshes(group),
				count: 0,
				matrix4: [],
				castShadow: group.castShadow,
				receiveShadow: group.receiveShadow,
			};
		}
	}

	setPopulation() {
		for (let i = 0; i < this.numOfLevel; i++) {
			const group = new THREE.Group();

			let { meshes, castShadow, receiveShadow } = this.instancedMeshOfAllLevel[i]

			meshes.forEach((m) => {
				const instancedMesh = new THREE.InstancedMesh(
					m.geometry,
					m.material,
					15000
				);
				instancedMesh.castShadow = castShadow;
				instancedMesh.receiveShadow = receiveShadow;

				group.add(instancedMesh);
			});
			this.groupOfInstances[i] = group;
			this.scene.add(group);
		}
	}

	getDistanceLevel(dist) {
		const { levels } = this;
		const length = levels.length;
		for (let i = 0; i < length; i++) {
			if (dist <= levels[i]) {
				return i;
			}
		}
		return -1
	}

	getLastLevel() {
		return this.levels.length - 1;
	}

	getSpecies() {
		return this.treeSpecies;
	}

	expandFrustum(frustum, offset) {
		frustum.planes.forEach((plane) => {
			plane.constant += offset;
		});
	}

	/* update函式每幀都要進行,記憶體交換越少越好,計算時間越短越好 */
	// render() {
	update() {
		count++
		let {
			instancedMeshOfAllLevel,
			groupOfInstances,
			numOfLevel,
			camera,
			frustum,
			octree,
			worldProjectionMatrix,
			obj_position,
			cur_dist,
			cur_level,
		} = this;
		// clear
		for (let i = 0; i < numOfLevel; i++) {
			instancedMeshOfAllLevel[i].count = 0;
			instancedMeshOfAllLevel[i].matrix4 = [];
		}
		// update camera frustum
		worldProjectionMatrix.identity(); // reset as identity matrix
		frustum.setFromProjectionMatrix(
			worldProjectionMatrix.multiplyMatrices(
				camera.projectionMatrix,
				camera.matrixWorldInverse
			)
		);

		this.expandFrustum(frustum, 25);
		let found = octree.queryByFrustum(frustum);
		found.forEach((matrix) => {
			obj_position.setFromMatrixPosition(matrix);
			cur_dist = obj_position.distanceTo(camera.position);
			cur_level = this.getDistanceLevel(cur_dist);
			if (cur_level != -1) {
				instancedMeshOfAllLevel[cur_level].count++;
				instancedMeshOfAllLevel[cur_level].matrix4.push(matrix); // column-major list of a matrix
			}
		});

		for (let i = 0; i < numOfLevel; i++) {
			const obj = instancedMeshOfAllLevel[i]; // obj: { meshes:[], count, matrix4:[] }
			for (let j = 0; j < groupOfInstances[i].children.length; j++) {
				let instancedMesh = groupOfInstances[i].children[j];

				if (instancedMesh.count >= obj.count) {
					instancedMesh.count = obj.count;
					for (let k = 0; k < obj.count; k++) {
						instancedMesh.instanceMatrix.needsUpdate = true;
						instancedMesh.setMatrixAt(k, obj.matrix4[k]);
					}
				} else {
					let new_instancedMesh = new THREE.InstancedMesh(
						obj.meshes[j].geometry,
						obj.meshes[j].material,
						obj.count
					);
					for (let k = 0; k < obj.count; k++) {
						new_instancedMesh.setMatrixAt(k, obj.matrix4[k]);
					}
					new_instancedMesh.castShadow = obj.castShadow;
					new_instancedMesh.receiveShadow = obj.receiveShadow;
					groupOfInstances[i].children[j] = new_instancedMesh;
				}
			}
		}
	}
}

export { InstancedLOD };

相關文章