@openhps/core
Version:
Open Hybrid Positioning System - Core component
1,230 lines (1,145 loc) • 47.8 kB
JavaScript
import { BufferAttribute } from '../core/BufferAttribute.js';
import { BufferGeometry } from '../core/BufferGeometry.js';
import { DataTexture } from '../textures/DataTexture.js';
import { FloatType, RedIntegerFormat, UnsignedIntType, RGBAFormat } from '../constants.js';
import { Matrix4 } from '../math/Matrix4.js';
import { Mesh } from './Mesh.js';
import { ColorManagement } from '../math/ColorManagement.js';
import { Box3 } from '../math/Box3.js';
import { Sphere } from '../math/Sphere.js';
import { Frustum } from '../math/Frustum.js';
import { Vector3 } from '../math/Vector3.js';
import { Color } from '../math/Color.js';
function ascIdSort(a, b) {
return a - b;
}
function sortOpaque(a, b) {
return a.z - b.z;
}
function sortTransparent(a, b) {
return b.z - a.z;
}
class MultiDrawRenderList {
constructor() {
this.index = 0;
this.pool = [];
this.list = [];
}
push(start, count, z, index) {
const pool = this.pool;
const list = this.list;
if (this.index >= pool.length) {
pool.push({
start: -1,
count: -1,
z: -1,
index: -1
});
}
const item = pool[this.index];
list.push(item);
this.index++;
item.start = start;
item.count = count;
item.z = z;
item.index = index;
}
reset() {
this.list.length = 0;
this.index = 0;
}
}
const _matrix = /*@__PURE__*/new Matrix4();
const _whiteColor = /*@__PURE__*/new Color(1, 1, 1);
const _frustum = /*@__PURE__*/new Frustum();
const _box = /*@__PURE__*/new Box3();
const _sphere = /*@__PURE__*/new Sphere();
const _vector = /*@__PURE__*/new Vector3();
const _forward = /*@__PURE__*/new Vector3();
const _temp = /*@__PURE__*/new Vector3();
const _renderList = /*@__PURE__*/new MultiDrawRenderList();
const _mesh = /*@__PURE__*/new Mesh();
const _batchIntersects = [];
// copies data from attribute "src" into "target" starting at "targetOffset"
function copyAttributeData(src, target, targetOffset = 0) {
const itemSize = target.itemSize;
if (src.isInterleavedBufferAttribute || src.array.constructor !== target.array.constructor) {
// use the component getters and setters if the array data cannot
// be copied directly
const vertexCount = src.count;
for (let i = 0; i < vertexCount; i++) {
for (let c = 0; c < itemSize; c++) {
target.setComponent(i + targetOffset, c, src.getComponent(i, c));
}
}
} else {
// faster copy approach using typed array set function
target.array.set(src.array, targetOffset * itemSize);
}
target.needsUpdate = true;
}
// safely copies array contents to a potentially smaller array
function copyArrayContents(src, target) {
if (src.constructor !== target.constructor) {
// if arrays are of a different type (eg due to index size increasing) then data must be per-element copied
const len = Math.min(src.length, target.length);
for (let i = 0; i < len; i++) {
target[i] = src[i];
}
} else {
// if the arrays use the same data layout we can use a fast block copy
const len = Math.min(src.length, target.length);
target.set(new src.constructor(src.buffer, 0, len));
}
}
/**
* A special version of a mesh with multi draw batch rendering support. Use
* this class if you have to render a large number of objects with the same
* material but with different geometries or world transformations. The usage of
* `BatchedMesh` will help you to reduce the number of draw calls and thus improve the overall
* rendering performance in your application.
*
* ```js
* const box = new THREE.BoxGeometry( 1, 1, 1 );
* const sphere = new THREE.SphereGeometry( 1, 12, 12 );
* const material = new THREE.MeshBasicMaterial( { color: 0x00ff00 } );
*
* // initialize and add geometries into the batched mesh
* const batchedMesh = new BatchedMesh( 10, 5000, 10000, material );
* const boxGeometryId = batchedMesh.addGeometry( box );
* const sphereGeometryId = batchedMesh.addGeometry( sphere );
*
* // create instances of those geometries
* const boxInstancedId1 = batchedMesh.addInstance( boxGeometryId );
* const boxInstancedId2 = batchedMesh.addInstance( boxGeometryId );
*
* const sphereInstancedId1 = batchedMesh.addInstance( sphereGeometryId );
* const sphereInstancedId2 = batchedMesh.addInstance( sphereGeometryId );
*
* // position the geometries
* batchedMesh.setMatrixAt( boxInstancedId1, boxMatrix1 );
* batchedMesh.setMatrixAt( boxInstancedId2, boxMatrix2 );
*
* batchedMesh.setMatrixAt( sphereInstancedId1, sphereMatrix1 );
* batchedMesh.setMatrixAt( sphereInstancedId2, sphereMatrix2 );
*
* scene.add( batchedMesh );
* ```
*
* @augments Mesh
*/
class BatchedMesh extends Mesh {
/**
* Constructs a new batched mesh.
*
* @param {number} maxInstanceCount - The maximum number of individual instances planned to be added and rendered.
* @param {number} maxVertexCount - The maximum number of vertices to be used by all unique geometries.
* @param {number} [maxIndexCount=maxVertexCount*2] - The maximum number of indices to be used by all unique geometries
* @param {Material|Array<Material>} [material] - The mesh material.
*/
constructor(maxInstanceCount, maxVertexCount, maxIndexCount = maxVertexCount * 2, material) {
super(new BufferGeometry(), material);
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isBatchedMesh = true;
/**
* When set ot `true`, the individual objects of a batch are frustum culled.
*
* @type {boolean}
* @default true
*/
this.perObjectFrustumCulled = true;
/**
* When set to `true`, the individual objects of a batch are sorted to improve overdraw-related artifacts.
* If the material is marked as "transparent" objects are rendered back to front and if not then they are
* rendered front to back.
*
* @type {boolean}
* @default true
*/
this.sortObjects = true;
/**
* The bounding box of the batched mesh. Can be computed via {@link BatchedMesh#computeBoundingBox}.
*
* @type {?Box3}
* @default null
*/
this.boundingBox = null;
/**
* The bounding sphere of the batched mesh. Can be computed via {@link BatchedMesh#computeBoundingSphere}.
*
* @type {?Sphere}
* @default null
*/
this.boundingSphere = null;
/**
* Takes a sort a function that is run before render. The function takes a list of instances to
* sort and a camera. The objects in the list include a "z" field to perform a depth-ordered
* sort with.
*
* @type {?Function}
* @default null
*/
this.customSort = null;
// stores visible, active, and geometry id per instance and reserved buffer ranges for geometries
this._instanceInfo = [];
this._geometryInfo = [];
// instance, geometry ids that have been set as inactive, and are available to be overwritten
this._availableInstanceIds = [];
this._availableGeometryIds = [];
// used to track where the next point is that geometry should be inserted
this._nextIndexStart = 0;
this._nextVertexStart = 0;
this._geometryCount = 0;
// flags
this._visibilityChanged = true;
this._geometryInitialized = false;
// cached user options
this._maxInstanceCount = maxInstanceCount;
this._maxVertexCount = maxVertexCount;
this._maxIndexCount = maxIndexCount;
// buffers for multi draw
this._multiDrawCounts = new Int32Array(maxInstanceCount);
this._multiDrawStarts = new Int32Array(maxInstanceCount);
this._multiDrawCount = 0;
this._multiDrawInstances = null;
// Local matrix per geometry by using data texture
this._matricesTexture = null;
this._indirectTexture = null;
this._colorsTexture = null;
this._initMatricesTexture();
this._initIndirectTexture();
}
/**
* The maximum number of individual instances that can be stored in the batch.
*
* @type {number}
* @readonly
*/
get maxInstanceCount() {
return this._maxInstanceCount;
}
/**
* The instance count.
*
* @type {number}
* @readonly
*/
get instanceCount() {
return this._instanceInfo.length - this._availableInstanceIds.length;
}
/**
* The number of unused vertices.
*
* @type {number}
* @readonly
*/
get unusedVertexCount() {
return this._maxVertexCount - this._nextVertexStart;
}
/**
* The number of unused indices.
*
* @type {number}
* @readonly
*/
get unusedIndexCount() {
return this._maxIndexCount - this._nextIndexStart;
}
_initMatricesTexture() {
// layout (1 matrix = 4 pixels)
// RGBA RGBA RGBA RGBA (=> column1, column2, column3, column4)
// with 8x8 pixel texture max 16 matrices * 4 pixels = (8 * 8)
// 16x16 pixel texture max 64 matrices * 4 pixels = (16 * 16)
// 32x32 pixel texture max 256 matrices * 4 pixels = (32 * 32)
// 64x64 pixel texture max 1024 matrices * 4 pixels = (64 * 64)
let size = Math.sqrt(this._maxInstanceCount * 4); // 4 pixels needed for 1 matrix
size = Math.ceil(size / 4) * 4;
size = Math.max(size, 4);
const matricesArray = new Float32Array(size * size * 4); // 4 floats per RGBA pixel
const matricesTexture = new DataTexture(matricesArray, size, size, RGBAFormat, FloatType);
this._matricesTexture = matricesTexture;
}
_initIndirectTexture() {
let size = Math.sqrt(this._maxInstanceCount);
size = Math.ceil(size);
const indirectArray = new Uint32Array(size * size);
const indirectTexture = new DataTexture(indirectArray, size, size, RedIntegerFormat, UnsignedIntType);
this._indirectTexture = indirectTexture;
}
_initColorsTexture() {
let size = Math.sqrt(this._maxInstanceCount);
size = Math.ceil(size);
// 4 floats per RGBA pixel initialized to white
const colorsArray = new Float32Array(size * size * 4).fill(1);
const colorsTexture = new DataTexture(colorsArray, size, size, RGBAFormat, FloatType);
colorsTexture.colorSpace = ColorManagement.workingColorSpace;
this._colorsTexture = colorsTexture;
}
_initializeGeometry(reference) {
const geometry = this.geometry;
const maxVertexCount = this._maxVertexCount;
const maxIndexCount = this._maxIndexCount;
if (this._geometryInitialized === false) {
for (const attributeName in reference.attributes) {
const srcAttribute = reference.getAttribute(attributeName);
const {
array,
itemSize,
normalized
} = srcAttribute;
const dstArray = new array.constructor(maxVertexCount * itemSize);
const dstAttribute = new BufferAttribute(dstArray, itemSize, normalized);
geometry.setAttribute(attributeName, dstAttribute);
}
if (reference.getIndex() !== null) {
// Reserve last u16 index for primitive restart.
const indexArray = maxVertexCount > 65535 ? new Uint32Array(maxIndexCount) : new Uint16Array(maxIndexCount);
geometry.setIndex(new BufferAttribute(indexArray, 1));
}
this._geometryInitialized = true;
}
}
// Make sure the geometry is compatible with the existing combined geometry attributes
_validateGeometry(geometry) {
// check to ensure the geometries are using consistent attributes and indices
const batchGeometry = this.geometry;
if (Boolean(geometry.getIndex()) !== Boolean(batchGeometry.getIndex())) {
throw new Error('THREE.BatchedMesh: All geometries must consistently have "index".');
}
for (const attributeName in batchGeometry.attributes) {
if (!geometry.hasAttribute(attributeName)) {
throw new Error(`THREE.BatchedMesh: Added geometry missing "${attributeName}". All geometries must have consistent attributes.`);
}
const srcAttribute = geometry.getAttribute(attributeName);
const dstAttribute = batchGeometry.getAttribute(attributeName);
if (srcAttribute.itemSize !== dstAttribute.itemSize || srcAttribute.normalized !== dstAttribute.normalized) {
throw new Error('THREE.BatchedMesh: All attributes must have a consistent itemSize and normalized value.');
}
}
}
/**
* Validates the instance defined by the given ID.
*
* @param {number} instanceId - The instance to validate.
*/
validateInstanceId(instanceId) {
const instanceInfo = this._instanceInfo;
if (instanceId < 0 || instanceId >= instanceInfo.length || instanceInfo[instanceId].active === false) {
throw new Error(`THREE.BatchedMesh: Invalid instanceId ${instanceId}. Instance is either out of range or has been deleted.`);
}
}
/**
* Validates the geometry defined by the given ID.
*
* @param {number} geometryId - The geometry to validate.
*/
validateGeometryId(geometryId) {
const geometryInfoList = this._geometryInfo;
if (geometryId < 0 || geometryId >= geometryInfoList.length || geometryInfoList[geometryId].active === false) {
throw new Error(`THREE.BatchedMesh: Invalid geometryId ${geometryId}. Geometry is either out of range or has been deleted.`);
}
}
/**
* Takes a sort a function that is run before render. The function takes a list of instances to
* sort and a camera. The objects in the list include a "z" field to perform a depth-ordered sort with.
*
* @param {Function} func - The custom sort function.
* @return {BatchedMesh} A reference to this batched mesh.
*/
setCustomSort(func) {
this.customSort = func;
return this;
}
/**
* Computes the bounding box, updating {@link BatchedMesh#boundingBox}.
* Bounding boxes aren't computed by default. They need to be explicitly computed,
* otherwise they are `null`.
*/
computeBoundingBox() {
if (this.boundingBox === null) {
this.boundingBox = new Box3();
}
const boundingBox = this.boundingBox;
const instanceInfo = this._instanceInfo;
boundingBox.makeEmpty();
for (let i = 0, l = instanceInfo.length; i < l; i++) {
if (instanceInfo[i].active === false) continue;
const geometryId = instanceInfo[i].geometryIndex;
this.getMatrixAt(i, _matrix);
this.getBoundingBoxAt(geometryId, _box).applyMatrix4(_matrix);
boundingBox.union(_box);
}
}
/**
* Computes the bounding sphere, updating {@link BatchedMesh#boundingSphere}.
* Bounding spheres aren't computed by default. They need to be explicitly computed,
* otherwise they are `null`.
*/
computeBoundingSphere() {
if (this.boundingSphere === null) {
this.boundingSphere = new Sphere();
}
const boundingSphere = this.boundingSphere;
const instanceInfo = this._instanceInfo;
boundingSphere.makeEmpty();
for (let i = 0, l = instanceInfo.length; i < l; i++) {
if (instanceInfo[i].active === false) continue;
const geometryId = instanceInfo[i].geometryIndex;
this.getMatrixAt(i, _matrix);
this.getBoundingSphereAt(geometryId, _sphere).applyMatrix4(_matrix);
boundingSphere.union(_sphere);
}
}
/**
* Adds a new instance to the batch using the geometry of the given ID and returns
* a new id referring to the new instance to be used by other functions.
*
* @param {number} geometryId - The ID of a previously added geometry via {@link BatchedMesh#addGeometry}.
* @return {number} The instance ID.
*/
addInstance(geometryId) {
const atCapacity = this._instanceInfo.length >= this.maxInstanceCount;
// ensure we're not over geometry
if (atCapacity && this._availableInstanceIds.length === 0) {
throw new Error('THREE.BatchedMesh: Maximum item count reached.');
}
const instanceInfo = {
visible: true,
active: true,
geometryIndex: geometryId
};
let drawId = null;
// Prioritize using previously freed instance ids
if (this._availableInstanceIds.length > 0) {
this._availableInstanceIds.sort(ascIdSort);
drawId = this._availableInstanceIds.shift();
this._instanceInfo[drawId] = instanceInfo;
} else {
drawId = this._instanceInfo.length;
this._instanceInfo.push(instanceInfo);
}
const matricesTexture = this._matricesTexture;
_matrix.identity().toArray(matricesTexture.image.data, drawId * 16);
matricesTexture.needsUpdate = true;
const colorsTexture = this._colorsTexture;
if (colorsTexture) {
_whiteColor.toArray(colorsTexture.image.data, drawId * 4);
colorsTexture.needsUpdate = true;
}
this._visibilityChanged = true;
return drawId;
}
/**
* Adds the given geometry to the batch and returns the associated
* geometry id referring to it to be used in other functions.
*
* @param {BufferGeometry} geometry - The geometry to add.
* @param {number} [reservedVertexCount=-1] - Optional parameter specifying the amount of
* vertex buffer space to reserve for the added geometry. This is necessary if it is planned
* to set a new geometry at this index at a later time that is larger than the original geometry.
* Defaults to the length of the given geometry vertex buffer.
* @param {number} [reservedIndexCount=-1] - Optional parameter specifying the amount of index
* buffer space to reserve for the added geometry. This is necessary if it is planned to set a
* new geometry at this index at a later time that is larger than the original geometry. Defaults to
* the length of the given geometry index buffer.
* @return {number} The geometry ID.
*/
addGeometry(geometry, reservedVertexCount = -1, reservedIndexCount = -1) {
this._initializeGeometry(geometry);
this._validateGeometry(geometry);
const geometryInfo = {
// geometry information
vertexStart: -1,
vertexCount: -1,
reservedVertexCount: -1,
indexStart: -1,
indexCount: -1,
reservedIndexCount: -1,
// draw range information
start: -1,
count: -1,
// state
boundingBox: null,
boundingSphere: null,
active: true
};
const geometryInfoList = this._geometryInfo;
geometryInfo.vertexStart = this._nextVertexStart;
geometryInfo.reservedVertexCount = reservedVertexCount === -1 ? geometry.getAttribute('position').count : reservedVertexCount;
const index = geometry.getIndex();
const hasIndex = index !== null;
if (hasIndex) {
geometryInfo.indexStart = this._nextIndexStart;
geometryInfo.reservedIndexCount = reservedIndexCount === -1 ? index.count : reservedIndexCount;
}
if (geometryInfo.indexStart !== -1 && geometryInfo.indexStart + geometryInfo.reservedIndexCount > this._maxIndexCount || geometryInfo.vertexStart + geometryInfo.reservedVertexCount > this._maxVertexCount) {
throw new Error('THREE.BatchedMesh: Reserved space request exceeds the maximum buffer size.');
}
// update id
let geometryId;
if (this._availableGeometryIds.length > 0) {
this._availableGeometryIds.sort(ascIdSort);
geometryId = this._availableGeometryIds.shift();
geometryInfoList[geometryId] = geometryInfo;
} else {
geometryId = this._geometryCount;
this._geometryCount++;
geometryInfoList.push(geometryInfo);
}
// update the geometry
this.setGeometryAt(geometryId, geometry);
// increment the next geometry position
this._nextIndexStart = geometryInfo.indexStart + geometryInfo.reservedIndexCount;
this._nextVertexStart = geometryInfo.vertexStart + geometryInfo.reservedVertexCount;
return geometryId;
}
/**
* Replaces the geometry at the given ID with the provided geometry. Throws an error if there
* is not enough space reserved for geometry. Calling this will change all instances that are
* rendering that geometry.
*
* @param {number} geometryId - The ID of the geometry that should be replaced with the given geometry.
* @param {BufferGeometry} geometry - The new geometry.
* @return {number} The geometry ID.
*/
setGeometryAt(geometryId, geometry) {
if (geometryId >= this._geometryCount) {
throw new Error('THREE.BatchedMesh: Maximum geometry count reached.');
}
this._validateGeometry(geometry);
const batchGeometry = this.geometry;
const hasIndex = batchGeometry.getIndex() !== null;
const dstIndex = batchGeometry.getIndex();
const srcIndex = geometry.getIndex();
const geometryInfo = this._geometryInfo[geometryId];
if (hasIndex && srcIndex.count > geometryInfo.reservedIndexCount || geometry.attributes.position.count > geometryInfo.reservedVertexCount) {
throw new Error('THREE.BatchedMesh: Reserved space not large enough for provided geometry.');
}
// copy geometry buffer data over
const vertexStart = geometryInfo.vertexStart;
const reservedVertexCount = geometryInfo.reservedVertexCount;
geometryInfo.vertexCount = geometry.getAttribute('position').count;
for (const attributeName in batchGeometry.attributes) {
// copy attribute data
const srcAttribute = geometry.getAttribute(attributeName);
const dstAttribute = batchGeometry.getAttribute(attributeName);
copyAttributeData(srcAttribute, dstAttribute, vertexStart);
// fill the rest in with zeroes
const itemSize = srcAttribute.itemSize;
for (let i = srcAttribute.count, l = reservedVertexCount; i < l; i++) {
const index = vertexStart + i;
for (let c = 0; c < itemSize; c++) {
dstAttribute.setComponent(index, c, 0);
}
}
dstAttribute.needsUpdate = true;
dstAttribute.addUpdateRange(vertexStart * itemSize, reservedVertexCount * itemSize);
}
// copy index
if (hasIndex) {
const indexStart = geometryInfo.indexStart;
const reservedIndexCount = geometryInfo.reservedIndexCount;
geometryInfo.indexCount = geometry.getIndex().count;
// copy index data over
for (let i = 0; i < srcIndex.count; i++) {
dstIndex.setX(indexStart + i, vertexStart + srcIndex.getX(i));
}
// fill the rest in with zeroes
for (let i = srcIndex.count, l = reservedIndexCount; i < l; i++) {
dstIndex.setX(indexStart + i, vertexStart);
}
dstIndex.needsUpdate = true;
dstIndex.addUpdateRange(indexStart, geometryInfo.reservedIndexCount);
}
// update the draw range
geometryInfo.start = hasIndex ? geometryInfo.indexStart : geometryInfo.vertexStart;
geometryInfo.count = hasIndex ? geometryInfo.indexCount : geometryInfo.vertexCount;
// store the bounding boxes
geometryInfo.boundingBox = null;
if (geometry.boundingBox !== null) {
geometryInfo.boundingBox = geometry.boundingBox.clone();
}
geometryInfo.boundingSphere = null;
if (geometry.boundingSphere !== null) {
geometryInfo.boundingSphere = geometry.boundingSphere.clone();
}
this._visibilityChanged = true;
return geometryId;
}
/**
* Deletes the geometry defined by the given ID from this batch. Any instances referencing
* this geometry will also be removed as a side effect.
*
* @param {number} geometryId - The ID of the geometry to remove from the batch.
* @return {BatchedMesh} A reference to this batched mesh.
*/
deleteGeometry(geometryId) {
const geometryInfoList = this._geometryInfo;
if (geometryId >= geometryInfoList.length || geometryInfoList[geometryId].active === false) {
return this;
}
// delete any instances associated with this geometry
const instanceInfo = this._instanceInfo;
for (let i = 0, l = instanceInfo.length; i < l; i++) {
if (instanceInfo[i].active && instanceInfo[i].geometryIndex === geometryId) {
this.deleteInstance(i);
}
}
geometryInfoList[geometryId].active = false;
this._availableGeometryIds.push(geometryId);
this._visibilityChanged = true;
return this;
}
/**
* Deletes an existing instance from the batch using the given ID.
*
* @param {number} instanceId - The ID of the instance to remove from the batch.
* @return {BatchedMesh} A reference to this batched mesh.
*/
deleteInstance(instanceId) {
this.validateInstanceId(instanceId);
this._instanceInfo[instanceId].active = false;
this._availableInstanceIds.push(instanceId);
this._visibilityChanged = true;
return this;
}
/**
* Repacks the sub geometries in [name] to remove any unused space remaining from
* previously deleted geometry, freeing up space to add new geometry.
*
* @param {number} instanceId - The ID of the instance to remove from the batch.
* @return {BatchedMesh} A reference to this batched mesh.
*/
optimize() {
// track the next indices to copy data to
let nextVertexStart = 0;
let nextIndexStart = 0;
// Iterate over all geometry ranges in order sorted from earliest in the geometry buffer to latest
// in the geometry buffer. Because draw range objects can be reused there is no guarantee of their order.
const geometryInfoList = this._geometryInfo;
const indices = geometryInfoList.map((e, i) => i).sort((a, b) => {
return geometryInfoList[a].vertexStart - geometryInfoList[b].vertexStart;
});
const geometry = this.geometry;
for (let i = 0, l = geometryInfoList.length; i < l; i++) {
// if a geometry range is inactive then don't copy anything
const index = indices[i];
const geometryInfo = geometryInfoList[index];
if (geometryInfo.active === false) {
continue;
}
// if a geometry contains an index buffer then shift it, as well
if (geometry.index !== null) {
if (geometryInfo.indexStart !== nextIndexStart) {
const {
indexStart,
vertexStart,
reservedIndexCount
} = geometryInfo;
const index = geometry.index;
const array = index.array;
// shift the index pointers based on how the vertex data will shift
// adjusting the index must happen first so the original vertex start value is available
const elementDelta = nextVertexStart - vertexStart;
for (let j = indexStart; j < indexStart + reservedIndexCount; j++) {
array[j] = array[j] + elementDelta;
}
index.array.copyWithin(nextIndexStart, indexStart, indexStart + reservedIndexCount);
index.addUpdateRange(nextIndexStart, reservedIndexCount);
geometryInfo.indexStart = nextIndexStart;
}
nextIndexStart += geometryInfo.reservedIndexCount;
}
// if a geometry needs to be moved then copy attribute data to overwrite unused space
if (geometryInfo.vertexStart !== nextVertexStart) {
const {
vertexStart,
reservedVertexCount
} = geometryInfo;
const attributes = geometry.attributes;
for (const key in attributes) {
const attribute = attributes[key];
const {
array,
itemSize
} = attribute;
array.copyWithin(nextVertexStart * itemSize, vertexStart * itemSize, (vertexStart + reservedVertexCount) * itemSize);
attribute.addUpdateRange(nextVertexStart * itemSize, reservedVertexCount * itemSize);
}
geometryInfo.vertexStart = nextVertexStart;
}
nextVertexStart += geometryInfo.reservedVertexCount;
geometryInfo.start = geometry.index ? geometryInfo.indexStart : geometryInfo.vertexStart;
// step the next geometry points to the shifted position
this._nextIndexStart = geometry.index ? geometryInfo.indexStart + geometryInfo.reservedIndexCount : 0;
this._nextVertexStart = geometryInfo.vertexStart + geometryInfo.reservedVertexCount;
}
return this;
}
/**
* Returns the bounding box for the given geometry.
*
* @param {number} geometryId - The ID of the geometry to return the bounding box for.
* @param {Box3} target - The target object that is used to store the method's result.
* @return {Box3|null} The geometry's bounding box. Returns `null` if no geometry has been found for the given ID.
*/
getBoundingBoxAt(geometryId, target) {
if (geometryId >= this._geometryCount) {
return null;
}
// compute bounding box
const geometry = this.geometry;
const geometryInfo = this._geometryInfo[geometryId];
if (geometryInfo.boundingBox === null) {
const box = new Box3();
const index = geometry.index;
const position = geometry.attributes.position;
for (let i = geometryInfo.start, l = geometryInfo.start + geometryInfo.count; i < l; i++) {
let iv = i;
if (index) {
iv = index.getX(iv);
}
box.expandByPoint(_vector.fromBufferAttribute(position, iv));
}
geometryInfo.boundingBox = box;
}
target.copy(geometryInfo.boundingBox);
return target;
}
/**
* Returns the bounding sphere for the given geometry.
*
* @param {number} geometryId - The ID of the geometry to return the bounding sphere for.
* @param {Sphere} target - The target object that is used to store the method's result.
* @return {Sphere|null} The geometry's bounding sphere. Returns `null` if no geometry has been found for the given ID.
*/
getBoundingSphereAt(geometryId, target) {
if (geometryId >= this._geometryCount) {
return null;
}
// compute bounding sphere
const geometry = this.geometry;
const geometryInfo = this._geometryInfo[geometryId];
if (geometryInfo.boundingSphere === null) {
const sphere = new Sphere();
this.getBoundingBoxAt(geometryId, _box);
_box.getCenter(sphere.center);
const index = geometry.index;
const position = geometry.attributes.position;
let maxRadiusSq = 0;
for (let i = geometryInfo.start, l = geometryInfo.start + geometryInfo.count; i < l; i++) {
let iv = i;
if (index) {
iv = index.getX(iv);
}
_vector.fromBufferAttribute(position, iv);
maxRadiusSq = Math.max(maxRadiusSq, sphere.center.distanceToSquared(_vector));
}
sphere.radius = Math.sqrt(maxRadiusSq);
geometryInfo.boundingSphere = sphere;
}
target.copy(geometryInfo.boundingSphere);
return target;
}
/**
* Sets the given local transformation matrix to the defined instance.
* Negatively scaled matrices are not supported.
*
* @param {number} instanceId - The ID of an instance to set the matrix of.
* @param {Matrix4} matrix - A 4x4 matrix representing the local transformation of a single instance.
* @return {BatchedMesh} A reference to this batched mesh.
*/
setMatrixAt(instanceId, matrix) {
this.validateInstanceId(instanceId);
const matricesTexture = this._matricesTexture;
const matricesArray = this._matricesTexture.image.data;
matrix.toArray(matricesArray, instanceId * 16);
matricesTexture.needsUpdate = true;
return this;
}
/**
* Returns the local transformation matrix of the defined instance.
*
* @param {number} instanceId - The ID of an instance to get the matrix of.
* @param {Matrix4} matrix - The target object that is used to store the method's result.
* @return {Matrix4} The instance's local transformation matrix.
*/
getMatrixAt(instanceId, matrix) {
this.validateInstanceId(instanceId);
return matrix.fromArray(this._matricesTexture.image.data, instanceId * 16);
}
/**
* Sets the given color to the defined instance.
*
* @param {number} instanceId - The ID of an instance to set the color of.
* @param {Color} color - The color to set the instance to.
* @return {BatchedMesh} A reference to this batched mesh.
*/
setColorAt(instanceId, color) {
this.validateInstanceId(instanceId);
if (this._colorsTexture === null) {
this._initColorsTexture();
}
color.toArray(this._colorsTexture.image.data, instanceId * 4);
this._colorsTexture.needsUpdate = true;
return this;
}
/**
* Returns the color of the defined instance.
*
* @param {number} instanceId - The ID of an instance to get the color of.
* @param {Color} color - The target object that is used to store the method's result.
* @return {Color} The instance's color.
*/
getColorAt(instanceId, color) {
this.validateInstanceId(instanceId);
return color.fromArray(this._colorsTexture.image.data, instanceId * 4);
}
/**
* Sets the visibility of the instance.
*
* @param {number} instanceId - The id of the instance to set the visibility of.
* @param {boolean} visible - Whether the instance is visible or not.
* @return {BatchedMesh} A reference to this batched mesh.
*/
setVisibleAt(instanceId, visible) {
this.validateInstanceId(instanceId);
if (this._instanceInfo[instanceId].visible === visible) {
return this;
}
this._instanceInfo[instanceId].visible = visible;
this._visibilityChanged = true;
return this;
}
/**
* Returns the visibility state of the defined instance.
*
* @param {number} instanceId - The ID of an instance to get the visibility state of.
* @return {boolean} Whether the instance is visible or not.
*/
getVisibleAt(instanceId) {
this.validateInstanceId(instanceId);
return this._instanceInfo[instanceId].visible;
}
/**
* Sets the geometry ID of the instance at the given index.
*
* @param {number} instanceId - The ID of the instance to set the geometry ID of.
* @param {number} geometryId - The geometry ID to be use by the instance.
* @return {BatchedMesh} A reference to this batched mesh.
*/
setGeometryIdAt(instanceId, geometryId) {
this.validateInstanceId(instanceId);
this.validateGeometryId(geometryId);
this._instanceInfo[instanceId].geometryIndex = geometryId;
return this;
}
/**
* Returns the geometry ID of the defined instance.
*
* @param {number} instanceId - The ID of an instance to get the geometry ID of.
* @return {number} The instance's geometry ID.
*/
getGeometryIdAt(instanceId) {
this.validateInstanceId(instanceId);
return this._instanceInfo[instanceId].geometryIndex;
}
/**
* Get the range representing the subset of triangles related to the attached geometry,
* indicating the starting offset and count, or `null` if invalid.
*
* @param {number} geometryId - The id of the geometry to get the range of.
* @param {Object} [target] - The target object that is used to store the method's result.
* @return {{
* vertexStart:number,vertexCount:number,reservedVertexCount:number,
* indexStart:number,indexCount:number,reservedIndexCount:number,
* start:number,count:number
* }} The result object with range data.
*/
getGeometryRangeAt(geometryId, target = {}) {
this.validateGeometryId(geometryId);
const geometryInfo = this._geometryInfo[geometryId];
target.vertexStart = geometryInfo.vertexStart;
target.vertexCount = geometryInfo.vertexCount;
target.reservedVertexCount = geometryInfo.reservedVertexCount;
target.indexStart = geometryInfo.indexStart;
target.indexCount = geometryInfo.indexCount;
target.reservedIndexCount = geometryInfo.reservedIndexCount;
target.start = geometryInfo.start;
target.count = geometryInfo.count;
return target;
}
/**
* Resizes the necessary buffers to support the provided number of instances.
* If the provided arguments shrink the number of instances but there are not enough
* unused Ids at the end of the list then an error is thrown.
*
* @param {number} maxInstanceCount - The max number of individual instances that can be added and rendered by the batch.
*/
setInstanceCount(maxInstanceCount) {
// shrink the available instances as much as possible
const availableInstanceIds = this._availableInstanceIds;
const instanceInfo = this._instanceInfo;
availableInstanceIds.sort(ascIdSort);
while (availableInstanceIds[availableInstanceIds.length - 1] === instanceInfo.length) {
instanceInfo.pop();
availableInstanceIds.pop();
}
// throw an error if it can't be shrunk to the desired size
if (maxInstanceCount < instanceInfo.length) {
throw new Error(`BatchedMesh: Instance ids outside the range ${maxInstanceCount} are being used. Cannot shrink instance count.`);
}
// copy the multi draw counts
const multiDrawCounts = new Int32Array(maxInstanceCount);
const multiDrawStarts = new Int32Array(maxInstanceCount);
copyArrayContents(this._multiDrawCounts, multiDrawCounts);
copyArrayContents(this._multiDrawStarts, multiDrawStarts);
this._multiDrawCounts = multiDrawCounts;
this._multiDrawStarts = multiDrawStarts;
this._maxInstanceCount = maxInstanceCount;
// update texture data for instance sampling
const indirectTexture = this._indirectTexture;
const matricesTexture = this._matricesTexture;
const colorsTexture = this._colorsTexture;
indirectTexture.dispose();
this._initIndirectTexture();
copyArrayContents(indirectTexture.image.data, this._indirectTexture.image.data);
matricesTexture.dispose();
this._initMatricesTexture();
copyArrayContents(matricesTexture.image.data, this._matricesTexture.image.data);
if (colorsTexture) {
colorsTexture.dispose();
this._initColorsTexture();
copyArrayContents(colorsTexture.image.data, this._colorsTexture.image.data);
}
}
/**
* Resizes the available space in the batch's vertex and index buffer attributes to the provided sizes.
* If the provided arguments shrink the geometry buffers but there is not enough unused space at the
* end of the geometry attributes then an error is thrown.
*
* @param {number} maxVertexCount - The maximum number of vertices to be used by all unique geometries to resize to.
* @param {number} maxIndexCount - The maximum number of indices to be used by all unique geometries to resize to.
*/
setGeometrySize(maxVertexCount, maxIndexCount) {
// Check if we can shrink to the requested vertex attribute size
const validRanges = [...this._geometryInfo].filter(info => info.active);
const requiredVertexLength = Math.max(...validRanges.map(range => range.vertexStart + range.reservedVertexCount));
if (requiredVertexLength > maxVertexCount) {
throw new Error(`BatchedMesh: Geometry vertex values are being used outside the range ${maxIndexCount}. Cannot shrink further.`);
}
// Check if we can shrink to the requested index attribute size
if (this.geometry.index) {
const requiredIndexLength = Math.max(...validRanges.map(range => range.indexStart + range.reservedIndexCount));
if (requiredIndexLength > maxIndexCount) {
throw new Error(`BatchedMesh: Geometry index values are being used outside the range ${maxIndexCount}. Cannot shrink further.`);
}
}
//
// dispose of the previous geometry
const oldGeometry = this.geometry;
oldGeometry.dispose();
// recreate the geometry needed based on the previous variant
this._maxVertexCount = maxVertexCount;
this._maxIndexCount = maxIndexCount;
if (this._geometryInitialized) {
this._geometryInitialized = false;
this.geometry = new BufferGeometry();
this._initializeGeometry(oldGeometry);
}
// copy data from the previous geometry
const geometry = this.geometry;
if (oldGeometry.index) {
copyArrayContents(oldGeometry.index.array, geometry.index.array);
}
for (const key in oldGeometry.attributes) {
copyArrayContents(oldGeometry.attributes[key].array, geometry.attributes[key].array);
}
}
raycast(raycaster, intersects) {
const instanceInfo = this._instanceInfo;
const geometryInfoList = this._geometryInfo;
const matrixWorld = this.matrixWorld;
const batchGeometry = this.geometry;
// iterate over each geometry
_mesh.material = this.material;
_mesh.geometry.index = batchGeometry.index;
_mesh.geometry.attributes = batchGeometry.attributes;
if (_mesh.geometry.boundingBox === null) {
_mesh.geometry.boundingBox = new Box3();
}
if (_mesh.geometry.boundingSphere === null) {
_mesh.geometry.boundingSphere = new Sphere();
}
for (let i = 0, l = instanceInfo.length; i < l; i++) {
if (!instanceInfo[i].visible || !instanceInfo[i].active) {
continue;
}
const geometryId = instanceInfo[i].geometryIndex;
const geometryInfo = geometryInfoList[geometryId];
_mesh.geometry.setDrawRange(geometryInfo.start, geometryInfo.count);
// get the intersects
this.getMatrixAt(i, _mesh.matrixWorld).premultiply(matrixWorld);
this.getBoundingBoxAt(geometryId, _mesh.geometry.boundingBox);
this.getBoundingSphereAt(geometryId, _mesh.geometry.boundingSphere);
_mesh.raycast(raycaster, _batchIntersects);
// add batch id to the intersects
for (let j = 0, l = _batchIntersects.length; j < l; j++) {
const intersect = _batchIntersects[j];
intersect.object = this;
intersect.batchId = i;
intersects.push(intersect);
}
_batchIntersects.length = 0;
}
_mesh.material = null;
_mesh.geometry.index = null;
_mesh.geometry.attributes = {};
_mesh.geometry.setDrawRange(0, Infinity);
}
copy(source) {
super.copy(source);
this.geometry = source.geometry.clone();
this.perObjectFrustumCulled = source.perObjectFrustumCulled;
this.sortObjects = source.sortObjects;
this.boundingBox = source.boundingBox !== null ? source.boundingBox.clone() : null;
this.boundingSphere = source.boundingSphere !== null ? source.boundingSphere.clone() : null;
this._geometryInfo = source._geometryInfo.map(info => ({
...info,
boundingBox: info.boundingBox !== null ? info.boundingBox.clone() : null,
boundingSphere: info.boundingSphere !== null ? info.boundingSphere.clone() : null
}));
this._instanceInfo = source._instanceInfo.map(info => ({
...info
}));
this._maxInstanceCount = source._maxInstanceCount;
this._maxVertexCount = source._maxVertexCount;
this._maxIndexCount = source._maxIndexCount;
this._geometryInitialized = source._geometryInitialized;
this._geometryCount = source._geometryCount;
this._multiDrawCounts = source._multiDrawCounts.slice();
this._multiDrawStarts = source._multiDrawStarts.slice();
this._matricesTexture = source._matricesTexture.clone();
this._matricesTexture.image.data = this._matricesTexture.image.data.slice();
if (this._colorsTexture !== null) {
this._colorsTexture = source._colorsTexture.clone();
this._colorsTexture.image.data = this._colorsTexture.image.data.slice();
}
return this;
}
/**
* Frees the GPU-related resources allocated by this instance. Call this
* method whenever this instance is no longer used in your app.
*/
dispose() {
// Assuming the geometry is not shared with other meshes
this.geometry.dispose();
this._matricesTexture.dispose();
this._matricesTexture = null;
this._indirectTexture.dispose();
this._indirectTexture = null;
if (this._colorsTexture !== null) {
this._colorsTexture.dispose();
this._colorsTexture = null;
}
}
onBeforeRender(renderer, scene, camera, geometry, material /*, _group*/) {
// if visibility has not changed and frustum culling and object sorting is not required
// then skip iterating over all items
if (!this._visibilityChanged && !this.perObjectFrustumCulled && !this.sortObjects) {
return;
}
// the indexed version of the multi draw function requires specifying the start
// offset in bytes.
const index = geometry.getIndex();
const bytesPerElement = index === null ? 1 : index.array.BYTES_PER_ELEMENT;
const instanceInfo = this._instanceInfo;
const multiDrawStarts = this._multiDrawStarts;
const multiDrawCounts = this._multiDrawCounts;
const geometryInfoList = this._geometryInfo;
const perObjectFrustumCulled = this.perObjectFrustumCulled;
const indirectTexture = this._indirectTexture;
const indirectArray = indirectTexture.image.data;
// prepare the frustum in the local frame
if (perObjectFrustumCulled) {
_matrix.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse).multiply(this.matrixWorld);
_frustum.setFromProjectionMatrix(_matrix, renderer.coordinateSystem);
}
let multiDrawCount = 0;
if (this.sortObjects) {
// get the camera position in the local frame
_matrix.copy(this.matrixWorld).invert();
_vector.setFromMatrixPosition(camera.matrixWorld).applyMatrix4(_matrix);
_forward.set(0, 0, -1).transformDirection(camera.matrixWorld).transformDirection(_matrix);
for (let i = 0, l = instanceInfo.length; i < l; i++) {
if (instanceInfo[i].visible && instanceInfo[i].active) {
const geometryId = instanceInfo[i].geometryIndex;
// get the bounds in world space
this.getMatrixAt(i, _matrix);
this.getBoundingSphereAt(geometryId, _sphere).applyMatrix4(_matrix);
// determine whether the batched geometry is within the frustum
let culled = false;
if (perObjectFrustumCulled) {
culled = !_frustum.intersectsSphere(_sphere);
}
if (!culled) {
// get the distance from camera used for sorting
const geometryInfo = geometryInfoList[geometryId];
const z = _temp.subVectors(_sphere.center, _vector).dot(_forward);
_renderList.push(geometryInfo.start, geometryInfo.count, z, i);
}
}
}
// Sort the draw ranges and prep for rendering
const list = _renderList.list;
const customSort = this.customSort;
if (customSort === null) {
list.sort(material.transparent ? sortTransparent : sortOpaque);
} else {
customSort.call(this, list, camera);
}
for (let i = 0, l = list.length; i < l; i++) {
const item = list[i];
multiDrawStarts[multiDrawCount] = item.start * bytesPerElement;
multiDrawCounts[multiDrawCount] = item.count;
indirectArray[multiDrawCount] = item.index;
multiDrawCount++;
}
_renderList.reset();
} else {
for (let i = 0, l = instanceInfo.length; i < l; i++) {
if (instanceInfo[i].visible && instanceInfo[i].active) {
const geometryId = instanceInfo[i].geometryIndex;
// determine whether the batched geometry is within the frustum
let culled = false;
if (perObjectFrustumCulled) {
// get the bounds in world space
this.getMatrixAt(i, _matrix);
this.getBoundingSphereAt(geometryId, _sphere).applyMatrix4(_matrix);
culled = !_frustum.intersectsSphere(_sphere);
}
if (!culled) {
const geometryInfo = geometryInfoList[geometryId];
multiDrawStarts[multiDrawCount] = geometryInfo.start * bytesPerElement;
multiDrawCounts[multiDrawCount] = geometryInfo.count;
indirectArray[multiDrawCount] = i;
multiDrawCount++;
}
}
}
}
indirectTexture.needsUpdate = true;
this._multiDrawCount = multiDrawCount;
this._visibilityChanged = false;
}
onBeforeShadow(renderer, object, camera, shadowCamera, geometry, depthMaterial /* , group */) {
this.onBeforeRender(renderer, null, shadowCamera, geometry, depthMaterial);
}
}
export { BatchedMesh };