@three.ez/batched-mesh-extensions
Version:
Utility extension methods for BatchedMesh
399 lines (398 loc) • 16.5 kB
JavaScript
import { BVH as nt, HybridBuilder as ot, WebGLCoordinateSystem as st, WebGPUCoordinateSystem as rt, vec3ToArray as Y, box3ToArray as N } from "bvh.js";
import { Box3 as K, Matrix4 as z, Frustum as it, Vector3 as T, Sphere as J, Mesh as ct, Ray as at, BatchedMesh as p } from "three";
import { radixSort as ut } from "three/addons/utils/SortUtils.js";
class lt {
/**
* @param target The target `BatchedMesh`.
* @param margin The margin applied for bounding box calculations (default is 0).
* @param accurateCulling Flag to enable accurate frustum culling without considering margin (default is true).
*/
constructor(t, n, e = 0, o = !0) {
this.nodesMap = /* @__PURE__ */ new Map(), this._origin = new Float32Array(3), this._dir = new Float32Array(3), this._cameraPos = new Float32Array(3), this._boxArray = new Float32Array(6), this.target = t, this.accurateCulling = o, this._margin = e, this.bvh = new nt(new ot(), n === 2e3 ? st : rt);
}
/**
* Builds the BVH from the target mesh's instances using a top-down construction method.
* This approach is more efficient and accurate compared to incremental methods, which add one instance at a time.
*/
create() {
const t = this.target.instanceCount, n = this.target._instanceInfo.length, e = this.target._instanceInfo, o = new Array(t), r = new Uint32Array(t);
let i = 0;
this.clear();
for (let c = 0; c < n; c++)
e[c].active && (o[i] = this.getBox(c, new Float32Array(6)), r[i] = c, i++);
this.bvh.createFromArray(r, o, (c) => {
this.nodesMap.set(c.object, c);
}, this._margin);
}
/**
* Inserts an instance into the BVH.
* @param id The id of the instance to insert.
*/
insert(t) {
const n = this.bvh.insert(t, this.getBox(t, new Float32Array(6)), this._margin);
this.nodesMap.set(t, n);
}
/**
* Inserts a range of instances into the BVH.
* @param ids An array of ids to insert.
*/
insertRange(t) {
const n = t.length, e = new Array(n);
for (let o = 0; o < n; o++)
e[o] = this.getBox(t[o], new Float32Array(6));
this.bvh.insertRange(t, e, this._margin, (o) => {
this.nodesMap.set(o.object, o);
});
}
/**
* Moves an instance within the BVH.
* @param id The id of the instance to move.
*/
move(t) {
const n = this.nodesMap.get(t);
n && (this.getBox(t, n.box), this.bvh.move(n, this._margin));
}
/**
* Deletes an instance from the BVH.
* @param id The id of the instance to delete.
*/
delete(t) {
const n = this.nodesMap.get(t);
n && (this.bvh.delete(n), this.nodesMap.delete(t));
}
/**
* Clears the BVH.
*/
clear() {
this.bvh.clear(), this.nodesMap.clear();
}
/**
* Performs frustum culling to determine which instances are visible based on the provided projection matrix.
* @param projScreenMatrix The projection screen matrix for frustum culling.
* @param onFrustumIntersection Callback function invoked when an instance intersects the frustum.
*/
frustumCulling(t, n) {
this._margin > 0 && this.accurateCulling ? this.bvh.frustumCulling(t.elements, (e, o, r) => {
o.isIntersectedMargin(e.box, r, this._margin) && n(e);
}) : this.bvh.frustumCulling(t.elements, n);
}
/**
* Performs raycasting to check if a ray intersects any instances.
* @param raycaster The raycaster used for raycasting.
* @param onIntersection Callback function invoked when a ray intersects an instance.
*/
raycast(t, n) {
const e = t.ray, o = this._origin, r = this._dir;
Y(e.origin, o), Y(e.direction, r), this.bvh.rayIntersections(r, o, n, t.near, t.far);
}
/**
* Checks if a given box intersects with any instance bounding box.
* @param target The target bounding box.
* @param onIntersection Callback function invoked when an intersection occurs.
* @returns `True` if there is an intersection, otherwise `false`.
*/
intersectBox(t, n) {
const e = this._boxArray;
return N(t, e), this.bvh.intersectsBox(e, n);
}
getBox(t, n) {
const e = this.target, o = e._instanceInfo[t].geometryIndex;
return e.getBoundingBoxAt(o, U).applyMatrix4(e.getMatrixAt(t, ht)), N(U, n), n;
}
}
const U = new K(), ht = new z();
class mt {
constructor() {
this.array = [], this.pool = [];
}
push(t, n, e, o) {
const r = this.pool, i = this.array, c = i.length;
c >= r.length && r.push({ start: null, count: null, z: null, zSort: null, index: null });
const a = r[c];
a.index = t, a.start = e, a.count = o, a.z = n, i.push(a);
}
reset() {
this.array.length = 0;
}
}
function dt(s, t = {}) {
this.bvh = new lt(this, s, t.margin, t.accurateCulling), this.bvh.create();
}
function zt(s) {
const t = {
get: (n) => n.zSort,
aux: new Array(s.maxInstanceCount),
reversed: null
};
return function(e) {
t.reversed = s.material.transparent, s.maxInstanceCount > t.aux.length && (t.aux.length = s.maxInstanceCount);
let o = 1 / 0, r = -1 / 0;
for (const { z: a } of e)
a > r && (r = a), a < o && (o = a);
const i = r - o, c = (2 ** 32 - 1) / i;
for (const a of e)
a.zSort = (a.z - o) * c;
ut(e, t);
};
}
function ft(s, t) {
return s.z - t.z;
}
function gt(s, t) {
return t.z - s.z;
}
const k = new it(), S = new mt(), V = new z(), F = new z(), j = new T(), R = new T(), H = new T(), yt = new T(), w = new J();
function pt(s, t, n, e, o, r) {
var i;
this.frustumCulling(n), (i = this.uniformsTexture) == null || i.update(s);
}
function xt(s, t = s) {
if (!this._visibilityChanged && !this.perObjectFrustumCulled && !this.sortObjects)
return;
this._indirectTexture.needsUpdate = !0, this._visibilityChanged = !1;
const n = this.sortObjects, e = this.perObjectFrustumCulled;
if (!e && !n) {
this.updateIndexArray();
return;
}
if (F.copy(this.matrixWorld).invert(), R.setFromMatrixPosition(s.matrixWorld).applyMatrix4(F), H.setFromMatrixPosition(t.matrixWorld).applyMatrix4(F), j.set(0, 0, -1).transformDirection(s.matrixWorld).transformDirection(F), e ? (V.multiplyMatrices(s.projectionMatrix, s.matrixWorldInverse).multiply(this.matrixWorld), this.bvh ? this.BVHCulling(s, t) : this.linearCulling(s, t)) : this.updateRenderList(), n) {
const o = this.geometry.getIndex(), r = o === null ? 1 : o.array.BYTES_PER_ELEMENT, i = this._multiDrawStarts, c = this._multiDrawCounts, a = this._indirectTexture.image.data, m = this.customSort;
m === null ? S.array.sort(this.material.transparent ? gt : ft) : m(S.array);
const u = S.array, h = u.length;
for (let l = 0; l < h; l++) {
const d = u[l];
i[l] = d.start * r, c[l] = d.count, a[l] = d.index;
}
S.reset();
}
}
function It() {
if (!this._visibilityChanged) return;
const s = this.geometry.getIndex(), t = s === null ? 1 : s.array.BYTES_PER_ELEMENT, n = this._instanceInfo, e = this._geometryInfo, o = this._multiDrawStarts, r = this._multiDrawCounts, i = this._indirectTexture.image.data;
let c = 0;
for (let a = 0, m = n.length; a < m; a++) {
const u = n[a];
if (u.visible && u.active) {
const h = u.geometryIndex, l = e[h];
o[c] = l.start * t, r[c] = l.count, i[c] = a, c++;
}
}
this._multiDrawCount = c;
}
function _t() {
const s = this._instanceInfo, t = this._geometryInfo;
for (let n = 0, e = s.length; n < e; n++) {
const o = s[n];
if (o.visible && o.active) {
const r = o.geometryIndex, i = t[r], c = this.getPositionAt(n).sub(R).dot(j);
S.push(n, c, i.start, i.count);
}
}
this._multiDrawCount = S.array.length;
}
function bt(s, t) {
const n = this.geometry.getIndex(), e = n === null ? 1 : n.array.BYTES_PER_ELEMENT, o = this._instanceInfo, r = this._geometryInfo, i = this.sortObjects, c = this._multiDrawStarts, a = this._multiDrawCounts, m = this._indirectTexture.image.data, u = this.onFrustumEnter;
let h = 0;
const l = s, d = (l.top - l.bottom) / l.zoom, _ = s, D = Math.tan(_.fov * 0.5 * (Math.PI / 180)) ** 2, M = this.useDistanceForLOD, O = _.isPerspectiveCamera;
this.bvh.frustumCulling(V, (x) => {
const v = x.object, A = o[v];
if (!A.visible) return;
const E = A.geometryIndex, I = r[E], f = I.LOD;
let g, y;
if (f) {
w.radius = I.boundingSphere.radius;
let b;
if (O) {
const L = this.getPositionAt(v).distanceToSquared(H);
b = Q(M, w, D, L);
} else
b = tt(M, w, d);
const B = this.getLODIndex(f, b, O);
if (u && !u(v, s, t, B)) return;
g = f[B].start, y = f[B].count;
} else {
if (u && !u(v, s)) return;
g = I.start, y = I.count;
}
if (i) {
const b = this.getPositionAt(v).sub(R).dot(j);
S.push(v, b, g, y);
} else
c[h] = g * e, a[h] = y, m[h] = v, h++;
}), this._multiDrawCount = i ? S.array.length : h;
}
function Q(s, t, n, e) {
return s ? e : t.radius ** 2 / (e * n);
}
function tt(s, t, n) {
if (s) throw new Error("BatchedMesh: useDistanceForLOD cannot be used with orthographic camera.");
return t.radius * 2 / n;
}
function vt(s, t) {
const n = this.geometry.getIndex(), e = n === null ? 1 : n.array.BYTES_PER_ELEMENT, o = this._instanceInfo, r = this._geometryInfo, i = this.sortObjects, c = this._multiDrawStarts, a = this._multiDrawCounts, m = this._indirectTexture.image.data, u = this.onFrustumEnter;
let h = 0;
k.setFromProjectionMatrix(V);
const l = s, d = (l.top - l.bottom) / l.zoom, _ = s, D = Math.tan(_.fov * 0.5 * (Math.PI / 180)) ** 2, M = this.useDistanceForLOD, O = _.isPerspectiveCamera;
for (let x = 0, v = o.length; x < v; x++) {
const A = o[x];
if (!A.visible || !A.active) continue;
const E = A.geometryIndex, I = r[E], f = I.LOD;
let g, y;
const b = I.boundingSphere, B = b.radius, L = b.center;
if (L.x === 0 && L.y === 0 && L.z === 0) {
const P = this.getPositionAndMaxScaleOnAxisAt(x, w.center);
w.radius = B * P;
} else
this.applyMatrixAtToSphere(x, w, L, B);
if (k.intersectsSphere(w)) {
if (f) {
let P;
if (O) {
const et = w.center.distanceToSquared(H);
P = Q(M, w, D, et);
} else
P = tt(M, w, d);
const q = this.getLODIndex(f, P, O);
if (u && !u(x, s, t, q)) continue;
g = f[q].start, y = f[q].count;
} else {
if (u && !u(x, s)) continue;
g = I.start, y = I.count;
}
if (i) {
const P = yt.subVectors(w.center, R).dot(j);
S.push(x, P, g, y);
} else
c[h] = g * e, a[h] = y, m[h] = x, h++;
}
}
this._multiDrawCount = i ? S.array.length : h;
}
const wt = new T();
function Ct(s, t = wt) {
const n = s * 16, e = this._matricesTexture.image.data;
return t.x = e[n + 12], t.y = e[n + 13], t.z = e[n + 14], t;
}
function St(s, t) {
const n = s * 16, e = this._matricesTexture.image.data, o = e[n + 0], r = e[n + 1], i = e[n + 2], c = o * o + r * r + i * i, a = e[n + 4], m = e[n + 5], u = e[n + 6], h = a * a + m * m + u * u, l = e[n + 8], d = e[n + 9], _ = e[n + 10], D = l * l + d * d + _ * _;
return t.x = e[n + 12], t.y = e[n + 13], t.z = e[n + 14], Math.sqrt(Math.max(c, h, D));
}
function Dt(s, t, n, e) {
const o = s * 16, r = this._matricesTexture.image.data, i = r[o + 0], c = r[o + 1], a = r[o + 2], m = r[o + 3], u = r[o + 4], h = r[o + 5], l = r[o + 6], d = r[o + 7], _ = r[o + 8], D = r[o + 9], M = r[o + 10], O = r[o + 11], x = r[o + 12], v = r[o + 13], A = r[o + 14], E = r[o + 15], I = t.center, f = n.x, g = n.y, y = n.z, b = 1 / (m * f + d * g + O * y + E);
I.x = (i * f + u * g + _ * y + x) * b, I.y = (c * f + h * g + D * y + v) * b, I.z = (a * f + l * g + M * y + A) * b;
const B = i * i + c * c + a * a, L = u * u + h * h + l * l, G = _ * _ + D * D + M * M;
t.radius = e * Math.sqrt(Math.max(B, L, G));
}
function Mt(s, t, n) {
const e = this._geometryInfo[s], o = t.isBufferGeometry ? t.index.array : t, r = n ** 2;
e.LOD ?? (e.LOD = [{ start: e.start, count: e.count, metric: 1 / 0, metricSquared: 1 / 0 }]);
const i = e.LOD, c = i[i.length - 1], a = c.start + c.count, m = o.length;
if (a - e.start + m > e.reservedIndexCount)
throw new Error("BatchedMesh LOD: Reserved space request exceeds the maximum buffer size.");
i.push({ start: a, count: m, metric: n, metricSquared: r });
const u = this.geometry.getIndex(), h = u.array, l = e.vertexStart;
for (let d = 0; d < m; d++)
h[a + d] = o[d] + l;
u.needsUpdate = !0;
}
function At(s, t, n = !1) {
const e = n ? "metricSquared" : "metric";
if (this.useDistanceForLOD) {
for (let o = s.length - 1; o > 0; o--) {
const i = s[o][e];
if (t >= i) return o;
}
return 0;
}
for (let o = s.length - 1; o > 0; o--) {
const i = s[o][e];
if (t <= i) return o;
}
return 0;
}
const W = [], C = new ct(), Bt = new at(), Z = new T(), X = new T(), $ = new z();
function Lt(s, t) {
var i, c;
if (!this.material || this.instanceCount === 0) return;
C.geometry = this.geometry, C.material = this.material, (i = C.geometry).boundingBox ?? (i.boundingBox = new K()), (c = C.geometry).boundingSphere ?? (c.boundingSphere = new J());
const n = s.ray, e = s.near, o = s.far;
$.copy(this.matrixWorld).invert(), X.setFromMatrixScale(this.matrixWorld), Z.copy(s.ray.direction).multiply(X);
const r = Z.length();
if (s.ray = Bt.copy(s.ray).applyMatrix4($), s.near /= r, s.far /= r, this.bvh)
this.bvh.raycast(s, (a) => this.checkInstanceIntersection(s, a, t));
else if (this.boundingSphere === null && this.computeBoundingSphere(), s.ray.intersectsSphere(this.boundingSphere))
for (let a = 0, m = this._instanceInfo.length; a < m; a++)
this.checkInstanceIntersection(s, a, t);
s.ray = n, s.near = e, s.far = o;
}
function Ot(s, t, n) {
const e = this._instanceInfo[t];
if (!e.active || !e.visible) return;
const o = e.geometryIndex, r = this._geometryInfo[o];
this.getMatrixAt(t, C.matrixWorld), C.geometry.boundsTree = this.boundsTrees ? this.boundsTrees[o] : void 0, C.geometry.boundsTree || (this.getBoundingBoxAt(o, C.geometry.boundingBox), this.getBoundingSphereAt(o, C.geometry.boundingSphere), C.geometry.setDrawRange(r.start, r.count)), C.raycast(s, W);
for (const i of W)
i.batchId = t, i.object = this, n.push(i);
W.length = 0;
}
function Pt() {
p.prototype.computeBVH = dt, p.prototype.onBeforeRender = pt, p.prototype.frustumCulling = xt, p.prototype.updateIndexArray = It, p.prototype.updateRenderList = _t, p.prototype.BVHCulling = bt, p.prototype.linearCulling = vt, p.prototype.getPositionAt = Ct, p.prototype.getPositionAndMaxScaleOnAxisAt = St, p.prototype.applyMatrixAtToSphere = Dt, p.prototype.addGeometryLOD = Mt, p.prototype.getLODIndex = At, p.prototype.raycast = Lt, p.prototype.checkInstanceIntersection = Ot;
}
function jt() {
Pt();
}
function Rt(s) {
const t = s.material, n = t.onBeforeCompile.bind(t);
t.onBeforeCompile = (e, o) => {
if (s.uniformsTexture) {
e.uniforms.uniformsTexture = { value: s.uniformsTexture };
const { vertex: r, fragment: i } = s.uniformsTexture.getUniformsGLSL("uniformsTexture", "batchIndex", "float");
e.vertexShader = e.vertexShader.replace("void main() {", r), e.fragmentShader = e.fragmentShader.replace("void main() {", i), e.vertexShader = e.vertexShader.replace("void main() {", "void main() { float batchIndex = getIndirectIndex( gl_DrawID );");
}
n(e, o);
};
}
function qt(s) {
let t = 0, n = 0;
for (const e of s)
t += e.attributes.position.count, n += e.index.count;
return { vertexCount: t, indexCount: n };
}
function Wt(s) {
const t = [];
let n = 0, e = 0;
for (const o of s) {
let r = 0;
for (const i of o) {
const c = i.index.count;
e += c, r += c, n += i.attributes.position.count;
}
t.push(r);
}
return { vertexCount: n, indexCount: e, LODIndexCount: t };
}
export {
bt as BVHCulling,
lt as BatchedMeshBVH,
mt as MultiDrawRenderList,
Mt as addGeometryLOD,
Dt as applyMatrixAtToSphere,
Ot as checkInstanceIntersection,
dt as computeBVH,
zt as createRadixSort,
jt as extendBatchedMeshPrototype,
xt as frustumCulling,
qt as getBatchedMeshCount,
Wt as getBatchedMeshLODCount,
At as getLODIndex,
St as getPositionAndMaxScaleOnAxisAt,
Ct as getPositionAt,
vt as linearCulling,
pt as onBeforeRender,
Rt as patchBatchedMeshMaterial,
Lt as raycast,
ft as sortOpaque,
gt as sortTransparent,
It as updateIndexArray,
_t as updateRenderList
};
//# sourceMappingURL=webgpu.js.map