@three.ez/batched-mesh-extensions
Version:
Utility extension methods for BatchedMesh
1 lines • 87.4 kB
Source Map (JSON)
{"version":3,"file":"webgl.cjs","sources":["../../src/core/BatchedMeshBVH.ts","../../src/core/MultiDrawRenderList.ts","../../src/core/SquareDataTexture.ts","../../src/core/feature/ComputeBVH.ts","../../src/utils/SortingUtils.ts","../../src/core/feature/FrustumCulling.ts","../../src/core/feature/GetPositionAt.ts","../../src/core/feature/LOD.ts","../../src/core/feature/Raycasting.ts","../../src/patch/PatchBatchedMeshMaterial.ts","../../src/core/feature/Uniforms.ts","../../src/patch/ExtendBatchedMeshPrototype.common.ts","../../src/patch/ExtendBatchedMeshPrototype.webgl.ts","../../src/utils/CountUtils.ts"],"sourcesContent":["import { box3ToArray, BVH, BVHNode, HybridBuilder, onFrustumIntersectionCallback, onIntersectionCallback, onIntersectionRayCallback, vec3ToArray, WebGLCoordinateSystem, WebGPUCoordinateSystem } from 'bvh.js';\r\nimport { BatchedMesh, Box3, CoordinateSystem, Matrix4, Raycaster } from 'three';\r\n\r\n// TODO implement getBBoxFromBSphere (add property to geometryInfo)\r\n// TODO implement frustumCullingLOD?\r\n\r\n/**\r\n * Class to manage BVH (Bounding Volume Hierarchy) for `BatchedMesh`.\r\n * Provides methods for managing bounding volumes, frustum culling, raycasting, and bounding box computation.\r\n */\r\nexport class BatchedMeshBVH {\r\n /**\r\n * The target `BatchedMesh` object that the BVH is managing.\r\n */\r\n public target: BatchedMesh;\r\n /**\r\n * The BVH instance used to organize bounding volumes.\r\n */\r\n public bvh: BVH<{}, number>;\r\n /**\r\n * A map that stores the BVH nodes for each instance.\r\n */\r\n public nodesMap = new Map<number, BVHNode<{}, number>>();\r\n /**\r\n * Enables accurate frustum culling by checking intersections without applying margin to the bounding box.\r\n */\r\n public accurateCulling: boolean;\r\n protected _margin: number;\r\n protected _origin = new Float32Array(3);\r\n protected _dir = new Float32Array(3);\r\n protected _cameraPos = new Float32Array(3);\r\n protected _boxArray = new Float32Array(6);\r\n\r\n /**\r\n * @param target The target `BatchedMesh`.\r\n * @param margin The margin applied for bounding box calculations (default is 0).\r\n * @param accurateCulling Flag to enable accurate frustum culling without considering margin (default is true).\r\n */\r\n constructor(target: BatchedMesh, coordinateSystem: CoordinateSystem, margin = 0, accurateCulling = true) {\r\n this.target = target;\r\n this.accurateCulling = accurateCulling;\r\n this._margin = margin;\r\n\r\n this.bvh = new BVH(new HybridBuilder(), coordinateSystem === 2000 ? WebGLCoordinateSystem : WebGPUCoordinateSystem); // TODO fix in BVH.js\r\n }\r\n\r\n /**\r\n * Builds the BVH from the target mesh's instances using a top-down construction method.\r\n * This approach is more efficient and accurate compared to incremental methods, which add one instance at a time.\r\n */\r\n public create(): void {\r\n const count = this.target.instanceCount;\r\n const instancesArrayCount = this.target._instanceInfo.length; // TODO this may change.. don't like it too much\r\n const instancesInfo = this.target._instanceInfo;\r\n const boxes: Float32Array[] = new Array(count); // test if single array and recreation inside node creation is faster due to memory location\r\n const objects = new Uint32Array(count);\r\n let index = 0;\r\n\r\n this.clear();\r\n\r\n for (let i = 0; i < instancesArrayCount; i++) {\r\n if (!instancesInfo[i].active) continue;\r\n boxes[index] = this.getBox(i, new Float32Array(6));\r\n objects[index] = i;\r\n index++;\r\n }\r\n\r\n this.bvh.createFromArray(objects as unknown as number[], boxes, (node) => {\r\n this.nodesMap.set(node.object, node);\r\n }, this._margin);\r\n }\r\n\r\n /**\r\n * Inserts an instance into the BVH.\r\n * @param id The id of the instance to insert.\r\n */\r\n public insert(id: number): void {\r\n const node = this.bvh.insert(id, this.getBox(id, new Float32Array(6)), this._margin);\r\n this.nodesMap.set(id, node);\r\n }\r\n\r\n /**\r\n * Inserts a range of instances into the BVH.\r\n * @param ids An array of ids to insert.\r\n */\r\n public insertRange(ids: number[]): void {\r\n const count = ids.length;\r\n const boxes: Float32Array[] = new Array(count);\r\n\r\n for (let i = 0; i < count; i++) {\r\n boxes[i] = this.getBox(ids[i], new Float32Array(6));\r\n }\r\n\r\n this.bvh.insertRange(ids, boxes, this._margin, (node) => {\r\n this.nodesMap.set(node.object, node);\r\n });\r\n }\r\n\r\n /**\r\n * Moves an instance within the BVH.\r\n * @param id The id of the instance to move.\r\n */\r\n public move(id: number): void {\r\n const node = this.nodesMap.get(id);\r\n if (!node) return;\r\n this.getBox(id, node.box as Float32Array); // this also updates box\r\n this.bvh.move(node, this._margin);\r\n }\r\n\r\n /**\r\n * Deletes an instance from the BVH.\r\n * @param id The id of the instance to delete.\r\n */\r\n public delete(id: number): void {\r\n const node = this.nodesMap.get(id);\r\n if (!node) return;\r\n this.bvh.delete(node);\r\n this.nodesMap.delete(id);\r\n }\r\n\r\n /**\r\n * Clears the BVH.\r\n */\r\n public clear(): void {\r\n this.bvh.clear();\r\n this.nodesMap.clear();\r\n }\r\n\r\n /**\r\n * Performs frustum culling to determine which instances are visible based on the provided projection matrix.\r\n * @param projScreenMatrix The projection screen matrix for frustum culling.\r\n * @param onFrustumIntersection Callback function invoked when an instance intersects the frustum.\r\n */\r\n public frustumCulling(projScreenMatrix: Matrix4, onFrustumIntersection: onFrustumIntersectionCallback<{}, number>): void {\r\n if (this._margin > 0 && this.accurateCulling) {\r\n this.bvh.frustumCulling(projScreenMatrix.elements, (node, frustum, mask) => {\r\n if (frustum.isIntersectedMargin(node.box, mask, this._margin)) {\r\n onFrustumIntersection(node);\r\n }\r\n });\r\n } else {\r\n this.bvh.frustumCulling(projScreenMatrix.elements, onFrustumIntersection);\r\n }\r\n }\r\n\r\n /**\r\n * Performs raycasting to check if a ray intersects any instances.\r\n * @param raycaster The raycaster used for raycasting.\r\n * @param onIntersection Callback function invoked when a ray intersects an instance.\r\n */\r\n public raycast(raycaster: Raycaster, onIntersection: onIntersectionRayCallback<number>): void {\r\n const ray = raycaster.ray;\r\n const origin = this._origin;\r\n const dir = this._dir;\r\n\r\n vec3ToArray(ray.origin, origin);\r\n vec3ToArray(ray.direction, dir);\r\n\r\n // TODO should we add margin check? maybe is not worth it\r\n this.bvh.rayIntersections(dir, origin, onIntersection, raycaster.near, raycaster.far);\r\n }\r\n\r\n /**\r\n * Checks if a given box intersects with any instance bounding box.\r\n * @param target The target bounding box.\r\n * @param onIntersection Callback function invoked when an intersection occurs.\r\n * @returns `True` if there is an intersection, otherwise `false`.\r\n */\r\n public intersectBox(target: Box3, onIntersection: onIntersectionCallback<number>): boolean {\r\n const array = this._boxArray;\r\n box3ToArray(target, array);\r\n return this.bvh.intersectsBox(array, onIntersection);\r\n }\r\n\r\n protected getBox(id: number, array: Float32Array): Float32Array {\r\n const target = this.target;\r\n const geometryId = target._instanceInfo[id].geometryIndex;\r\n target.getBoundingBoxAt(geometryId, _box3).applyMatrix4(target.getMatrixAt(id, _matrix));\r\n box3ToArray(_box3, array);\r\n return array;\r\n }\r\n}\r\n\r\nconst _box3 = new Box3();\r\nconst _matrix = new Matrix4();\r\n","export type MultiDrawRenderItem = { start: number; count: number; z: number; zSort?: number; index?: number };\r\n\r\n/**\r\n * A class that creates and manages a list of render items, used to determine the rendering order based on depth.\r\n * @internal\r\n */\r\nexport class MultiDrawRenderList {\r\n public array: MultiDrawRenderItem[] = [];\r\n protected pool: MultiDrawRenderItem[] = [];\r\n\r\n public push(instanceId: number, depth: number, start: number, count: number): void {\r\n const pool = this.pool;\r\n const list = this.array;\r\n const index = list.length;\r\n\r\n if (index >= pool.length) {\r\n pool.push({ start: null, count: null, z: null, zSort: null, index: null });\r\n }\r\n\r\n const item = pool[index];\r\n item.index = instanceId;\r\n item.start = start;\r\n item.count = count;\r\n item.z = depth;\r\n\r\n list.push(item);\r\n }\r\n\r\n public reset(): void {\r\n this.array.length = 0;\r\n }\r\n}\r\n","import { Color, ColorManagement, DataTexture, FloatType, IntType, Matrix3, Matrix4, NoColorSpace, PixelFormat, RedFormat, RedIntegerFormat, RGBAFormat, RGBAIntegerFormat, RGFormat, RGIntegerFormat, TextureDataType, TypedArray, UnsignedIntType, Vector2, Vector3, Vector4, WebGLRenderer, WebGLUtils } from 'three';\r\n\r\n/**\r\n * Represents the number of elements per pixel.\r\n */\r\nexport type ChannelSize = 1 | 2 | 3 | 4;\r\n/**\r\n * A constructor signature for creating TypedArray.\r\n */\r\nexport type TypedArrayConstructor = new (count: number) => TypedArray;\r\n/**\r\n * Represents the texture information including its data, size, format, and data type.\r\n */\r\nexport type TextureInfo = { array: TypedArray; size: number; format: PixelFormat; type: TextureDataType };\r\n/**\r\n * Represents information for updating rows in the texture, including the row index and number of rows.\r\n */\r\nexport type UpdateRowInfo = { row: number; count: number };\r\n/**\r\n * Defines the possible types of uniforms that can be used in shaders.\r\n */\r\nexport type UniformType = 'float' | 'vec2' | 'vec3' | 'vec4' | 'mat3' | 'mat4';\r\n/**\r\n * Represents a value that can be used as a uniform.\r\n */\r\nexport type UniformValueObj = Vector2 | Vector3 | Vector4 | Matrix3 | Matrix4 | Color;\r\n/**\r\n * Defines a uniform value as either a number or a compatible Three.js object.\r\n */\r\nexport type UniformValue = number | UniformValueObj;\r\n/**\r\n * Represents the schema for a uniform, defining its offset, size, and type.\r\n */\r\nexport type UniformMapType = { offset: number; size: number; type: UniformType };\r\n/**\r\n * Represents a map of uniform names to their schema definitions.\r\n */\r\nexport type UniformMap = Map<string, UniformMapType>;\r\n\r\n/**\r\n * Calculates the square texture size based on the capacity and pixels per instance.\r\n * This ensures the texture is large enough to store all instances in a square layout.\r\n * @param capacity The maximum number of instances allowed in the texture.\r\n * @param pixelsPerInstance The number of pixels required for each instance.\r\n * @returns The size of the square texture needed to store all the instances.\r\n */\r\nexport function getSquareTextureSize(capacity: number, pixelsPerInstance: number): number {\r\n return Math.max(pixelsPerInstance, Math.ceil(Math.sqrt(capacity / pixelsPerInstance)) * pixelsPerInstance);\r\n}\r\n\r\n/**\r\n * Generates texture information (size, format, type) for a square texture based on the provided parameters.\r\n * @param arrayType The constructor for the TypedArray.\r\n * @param channels The number of channels in the texture.\r\n * @param pixelsPerInstance The number of pixels required for each instance.\r\n * @param capacity The maximum number of instances allowed in the texture.\r\n * @returns An object containing the texture's array, size, format, and data type.\r\n */\r\nexport function getSquareTextureInfo(arrayType: TypedArrayConstructor, channels: ChannelSize, pixelsPerInstance: number, capacity: number): TextureInfo {\r\n if (channels === 3) {\r\n console.warn('\"channels\" cannot be 3. Set to 4. More info: https://github.com/mrdoob/three.js/pull/23228');\r\n channels = 4;\r\n }\r\n\r\n const size = getSquareTextureSize(capacity, pixelsPerInstance);\r\n const array = new arrayType(size * size * channels);\r\n const isFloat = arrayType.name.includes('Float');\r\n const isUnsignedInt = arrayType.name.includes('Uint');\r\n const type: TextureDataType = isFloat ? FloatType : (isUnsignedInt ? UnsignedIntType : IntType);\r\n let format: PixelFormat;\r\n\r\n switch (channels) {\r\n case 1:\r\n format = isFloat ? RedFormat : RedIntegerFormat;\r\n break;\r\n case 2:\r\n format = isFloat ? RGFormat : RGIntegerFormat;\r\n break;\r\n case 4:\r\n format = isFloat ? RGBAFormat : RGBAIntegerFormat;\r\n break;\r\n }\r\n\r\n return { array, size, type, format };\r\n}\r\n\r\n/**\r\n * A class that extends `DataTexture` to manage a square texture optimized for instances rendering.\r\n * It supports dynamic resizing, partial update based on rows, and allows setting/getting uniforms per instance.\r\n */\r\nexport class SquareDataTexture extends DataTexture {\r\n /**\r\n * Whether to enable partial texture updates by row. If `false`, the entire texture will be updated.\r\n * @default true.\r\n */\r\n public partialUpdate = true;\r\n /**\r\n * The maximum number of update calls per frame.\r\n * @default Infinity\r\n */\r\n public maxUpdateCalls = Infinity;\r\n /** @internal */ _data: TypedArray; // TODO make it public or remove it?\r\n protected _channels: ChannelSize;\r\n protected _pixelsPerInstance: number;\r\n protected _stride: number;\r\n protected _rowToUpdate: boolean[];\r\n protected _uniformMap: UniformMap;\r\n protected _fetchUniformsInFragmentShader: boolean;\r\n protected _utils: WebGLUtils = null; // TODO add it to renderer instead of creating for each texture\r\n protected _needsUpdate: boolean = false;\r\n protected _lastWidth: number = null;\r\n\r\n /**\r\n * @param arrayType The constructor for the TypedArray.\r\n * @param channels The number of channels in the texture.\r\n * @param pixelsPerInstance The number of pixels required for each instance.\r\n * @param capacity The total number of instances.\r\n * @param uniformMap Optional map for handling uniform values.\r\n * @param fetchInFragmentShader Optional flag that determines if uniform values should be fetched in the fragment shader instead of the vertex shader.\r\n */\r\n constructor(arrayType: TypedArrayConstructor, channels: ChannelSize, pixelsPerInstance: number, capacity: number, uniformMap?: UniformMap, fetchInFragmentShader?: boolean) {\r\n if (channels === 3) channels = 4;\r\n const { array, format, size, type } = getSquareTextureInfo(arrayType, channels, pixelsPerInstance, capacity);\r\n super(array, size, size, format, type);\r\n this._data = array;\r\n this._channels = channels;\r\n this._pixelsPerInstance = pixelsPerInstance;\r\n this._stride = pixelsPerInstance * channels;\r\n this._rowToUpdate = new Array(size);\r\n this._uniformMap = uniformMap;\r\n this._fetchUniformsInFragmentShader = fetchInFragmentShader;\r\n this.needsUpdate = true; // necessary to init texture\r\n }\r\n\r\n /**\r\n * Resizes the texture to accommodate a new number of instances.\r\n * @param count The new total number of instances.\r\n */\r\n public resize(count: number): void {\r\n const size = getSquareTextureSize(count, this._pixelsPerInstance);\r\n if (size === this.image.width) return;\r\n\r\n const currentData = this._data;\r\n const channels = this._channels;\r\n this._rowToUpdate.length = size;\r\n const arrayType = (currentData as any).constructor;\r\n\r\n const data = new arrayType(size * size * channels);\r\n const minLength = Math.min(currentData.length, data.length);\r\n data.set(new arrayType(currentData.buffer, 0, minLength));\r\n\r\n this.dispose();\r\n this.image = { data, height: size, width: size };\r\n this._data = data;\r\n }\r\n\r\n /**\r\n * Marks a row of the texture for update during the next render cycle.\r\n * This helps in optimizing texture updates by only modifying the rows that have changed.\r\n * @param index The index of the instance to update.\r\n */\r\n public enqueueUpdate(index: number): void {\r\n this._needsUpdate = true;\r\n if (!this.partialUpdate) return;\r\n\r\n const elementsPerRow = this.image.width / this._pixelsPerInstance;\r\n const rowIndex = Math.floor(index / elementsPerRow);\r\n this._rowToUpdate[rowIndex] = true;\r\n }\r\n\r\n /**\r\n * Updates the texture data based on the rows that need updating.\r\n * This method is optimized to only update the rows that have changed, improving performance.\r\n * @param renderer The WebGLRenderer used for rendering.\r\n */\r\n public update(renderer: WebGLRenderer): void {\r\n const textureProperties: any = renderer.properties.get(this);\r\n const versionChanged = this.version > 0 && textureProperties.__version !== this.version;\r\n const sizeChanged = this._lastWidth !== null && this._lastWidth !== this.image.width;\r\n if (!this._needsUpdate || !textureProperties.__webglTexture || versionChanged || sizeChanged) {\r\n this._lastWidth = this.image.width;\r\n this._needsUpdate = false;\r\n return;\r\n }\r\n\r\n this._needsUpdate = false;\r\n\r\n if (!this.partialUpdate) {\r\n this.needsUpdate = true; // three.js will update the whole texture\r\n return;\r\n }\r\n\r\n const rowsInfo = this.getUpdateRowsInfo();\r\n if (rowsInfo.length === 0) return;\r\n\r\n if (rowsInfo.length > this.maxUpdateCalls) {\r\n this.needsUpdate = true; // three.js will update the whole texture\r\n } else {\r\n this.updateRows(textureProperties, renderer, rowsInfo);\r\n }\r\n\r\n this._rowToUpdate.fill(false);\r\n }\r\n\r\n // TODO reuse same objects to prevent memory leak\r\n protected getUpdateRowsInfo(): UpdateRowInfo[] {\r\n const rowsToUpdate = this._rowToUpdate;\r\n const result: UpdateRowInfo[] = [];\r\n\r\n for (let i = 0, l = rowsToUpdate.length; i < l; i++) {\r\n if (rowsToUpdate[i]) {\r\n const row = i;\r\n for (; i < l; i++) {\r\n if (!rowsToUpdate[i]) break;\r\n }\r\n result.push({ row, count: i - row });\r\n }\r\n }\r\n\r\n return result;\r\n }\r\n\r\n protected updateRows(textureProperties: any, renderer: WebGLRenderer, info: UpdateRowInfo[]): void {\r\n const state = renderer.state;\r\n const gl = renderer.getContext() as WebGL2RenderingContext;\r\n // @ts-expect-error Expected 2 arguments, but got 3.\r\n this._utils ??= new WebGLUtils(gl, renderer.extensions, renderer.capabilities); // third argument is necessary for older three versions\r\n const glFormat = this._utils.convert(this.format);\r\n const glType = this._utils.convert(this.type);\r\n const { data, width } = this.image;\r\n const channels = this._channels;\r\n\r\n state.bindTexture(gl.TEXTURE_2D, textureProperties.__webglTexture);\r\n\r\n const workingPrimaries = ColorManagement.getPrimaries(ColorManagement.workingColorSpace);\r\n const texturePrimaries = this.colorSpace === NoColorSpace ? null : ColorManagement.getPrimaries(this.colorSpace);\r\n const unpackConversion = this.colorSpace === NoColorSpace || workingPrimaries === texturePrimaries ? gl.NONE : gl.BROWSER_DEFAULT_WEBGL;\r\n\r\n gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, this.flipY);\r\n gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, this.premultiplyAlpha);\r\n gl.pixelStorei(gl.UNPACK_ALIGNMENT, this.unpackAlignment);\r\n gl.pixelStorei(gl.UNPACK_COLORSPACE_CONVERSION_WEBGL, unpackConversion);\r\n\r\n for (const { count, row } of info) {\r\n gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, row, width, count, glFormat, glType, data, row * width * channels);\r\n }\r\n\r\n if (this.onUpdate) this.onUpdate(this);\r\n }\r\n\r\n /**\r\n * Sets a uniform value at the specified instance ID in the texture.\r\n * @param id The instance ID to set the uniform for.\r\n * @param name The name of the uniform.\r\n * @param value The value to set for the uniform.\r\n */\r\n public setUniformAt(id: number, name: string, value: UniformValue): void {\r\n const { offset, size } = this._uniformMap.get(name);\r\n const stride = this._stride;\r\n\r\n if (size === 1) {\r\n this._data[id * stride + offset] = value as number;\r\n } else {\r\n (value as UniformValueObj).toArray(this._data, id * stride + offset);\r\n }\r\n }\r\n\r\n /**\r\n * Retrieves a uniform value at the specified instance ID from the texture.\r\n * @param id The instance ID to retrieve the uniform from.\r\n * @param name The name of the uniform.\r\n * @param target Optional target object to store the uniform value.\r\n * @returns The uniform value for the specified instance.\r\n */\r\n public getUniformAt(id: number, name: string, target?: UniformValueObj): UniformValue {\r\n const { offset, size } = this._uniformMap.get(name);\r\n const stride = this._stride;\r\n\r\n if (size === 1) {\r\n return this._data[id * stride + offset];\r\n }\r\n\r\n return target.fromArray(this._data, id * stride + offset);\r\n }\r\n\r\n /**\r\n * Generates the GLSL code for accessing the uniform data stored in the texture.\r\n * @param textureName The name of the texture in the GLSL shader.\r\n * @param indexName The name of the index in the GLSL shader.\r\n * @param indexType The type of the index in the GLSL shader.\r\n * @returns An object containing the GLSL code for the vertex and fragment shaders.\r\n */\r\n public getUniformsGLSL(textureName: string, indexName: string, indexType: string): { vertex: string; fragment: string } {\r\n const vertex = this.getUniformsVertexGLSL(textureName, indexName, indexType);\r\n const fragment = this.getUniformsFragmentGLSL(textureName, indexName, indexType);\r\n return { vertex, fragment };\r\n }\r\n\r\n protected getUniformsVertexGLSL(textureName: string, indexName: string, indexType: string): string {\r\n if (this._fetchUniformsInFragmentShader) {\r\n return `\r\n flat varying ${indexType} ez_v${indexName}; \r\n void main() {\r\n ez_v${indexName} = ${indexName};`;\r\n }\r\n\r\n const texelsFetch = this.texelsFetchGLSL(textureName, indexName);\r\n const getFromTexels = this.getFromTexelsGLSL();\r\n const { assignVarying, declareVarying } = this.getVarying();\r\n\r\n return `\r\n uniform highp sampler2D ${textureName}; \r\n ${declareVarying}\r\n void main() {\r\n ${texelsFetch}\r\n ${getFromTexels}\r\n ${assignVarying}`;\r\n }\r\n\r\n protected getUniformsFragmentGLSL(textureName: string, indexName: string, indexType: string): string {\r\n if (!this._fetchUniformsInFragmentShader) {\r\n const { declareVarying, getVarying } = this.getVarying();\r\n\r\n return `\r\n ${declareVarying}\r\n void main() {\r\n ${getVarying}`;\r\n }\r\n\r\n const texelsFetch = this.texelsFetchGLSL(textureName, `ez_v${indexName}`);\r\n const getFromTexels = this.getFromTexelsGLSL();\r\n\r\n return `\r\n uniform highp sampler2D ${textureName}; \r\n flat varying ${indexType} ez_v${indexName};\r\n void main() {\r\n ${texelsFetch}\r\n ${getFromTexels}`;\r\n }\r\n\r\n protected texelsFetchGLSL(textureName: string, indexName: string): string {\r\n const pixelsPerInstance = this._pixelsPerInstance;\r\n\r\n let texelsFetch = `\r\n int size = textureSize(${textureName}, 0).x;\r\n int j = int(${indexName}) * ${pixelsPerInstance};\r\n int x = j % size;\r\n int y = j / size;\r\n `;\r\n\r\n for (let i = 0; i < pixelsPerInstance; i++) {\r\n texelsFetch += `vec4 ez_texel${i} = texelFetch(${textureName}, ivec2(x + ${i}, y), 0);\\n`;\r\n }\r\n\r\n return texelsFetch;\r\n }\r\n\r\n protected getFromTexelsGLSL(): string {\r\n const uniforms = this._uniformMap;\r\n let getFromTexels = '';\r\n\r\n for (const [name, { type, offset, size }] of uniforms) {\r\n const tId = Math.floor(offset / this._channels);\r\n\r\n if (type === 'mat3') {\r\n getFromTexels += `mat3 ${name} = mat3(ez_texel${tId}.rgb, vec3(ez_texel${tId}.a, ez_texel${tId + 1}.rg), vec3(ez_texel${tId + 1}.ba, ez_texel${tId + 2}.r));\\n`;\r\n } else if (type === 'mat4') {\r\n getFromTexels += `mat4 ${name} = mat4(ez_texel${tId}, ez_texel${tId + 1}, ez_texel${tId + 2}, ez_texel${tId + 3});\\n`;\r\n } else {\r\n const components = this.getUniformComponents(offset, size);\r\n getFromTexels += `${type} ${name} = ez_texel${tId}.${components};\\n`;\r\n }\r\n }\r\n\r\n return getFromTexels;\r\n }\r\n\r\n protected getVarying(): { declareVarying: string; assignVarying: string; getVarying: string } {\r\n const uniforms = this._uniformMap;\r\n let declareVarying = '';\r\n let assignVarying = '';\r\n let getVarying = '';\r\n\r\n for (const [name, { type }] of uniforms) {\r\n declareVarying += `flat varying ${type} ez_v${name};\\n`;\r\n assignVarying += `ez_v${name} = ${name};\\n`;\r\n getVarying += `${type} ${name} = ez_v${name};\\n`;\r\n }\r\n\r\n return { declareVarying, assignVarying, getVarying };\r\n }\r\n\r\n protected getUniformComponents(offset: number, size: number): string {\r\n const startIndex = offset % this._channels;\r\n let components = '';\r\n\r\n for (let i = 0; i < size; i++) {\r\n components += componentsArray[startIndex + i];\r\n }\r\n\r\n return components;\r\n }\r\n\r\n public override copy(source: SquareDataTexture): this {\r\n super.copy(source);\r\n\r\n this.partialUpdate = source.partialUpdate;\r\n this.maxUpdateCalls = source.maxUpdateCalls;\r\n this._channels = source._channels;\r\n this._pixelsPerInstance = source._pixelsPerInstance;\r\n this._stride = source._stride;\r\n this._rowToUpdate = source._rowToUpdate;\r\n this._uniformMap = source._uniformMap;\r\n this._fetchUniformsInFragmentShader = source._fetchUniformsInFragmentShader;\r\n\r\n return this;\r\n }\r\n}\r\n\r\nconst componentsArray = ['r', 'g', 'b', 'a'];\r\n","import { BatchedMesh, CoordinateSystem } from 'three';\r\nimport { BatchedMeshBVH } from '../BatchedMeshBVH.js';\r\n\r\n/**\r\n * Parameters for configuring the BVH (Bounding Volume Hierarchy).\r\n */\r\nexport interface BVHParams {\r\n /**\r\n * Margin applied to accommodate animated or moving objects.\r\n * Improves BVH update performance but slows down frustum culling and raycasting.\r\n * For static objects, set to 0 to optimize culling and raycasting efficiency.\r\n * @default 0\r\n */\r\n margin?: number;\r\n /**\r\n * Enables accurate frustum culling by checking intersections without applying margin to the bounding box.\r\n * @default true\r\n */\r\n accurateCulling?: boolean;\r\n}\r\n\r\ndeclare module 'three' {\r\n interface BatchedMesh {\r\n /**\r\n * BVH structure for optimized culling and intersection testing.\r\n * It's possible to create the BVH using the `computeBVH` method. Once created it will be updated automatically.\r\n */\r\n bvh?: BatchedMeshBVH;\r\n /**\r\n * Creates and computes the BVH (Bounding Volume Hierarchy) for the instances.\r\n * It's recommended to create it when all the instance matrices have been assigned.\r\n * Once created it will be updated automatically.\r\n * @param coordinateSystem The coordinate system (webgl or webgpu) in which the BVH is computed.\r\n * @param config Optional configuration parameters object. See `BVHParams` for details.\r\n */\r\n computeBVH(coordinateSystem: CoordinateSystem, config?: BVHParams): void;\r\n }\r\n}\r\n\r\nexport function computeBVH(this: BatchedMesh, coordinateSystem: CoordinateSystem, config: BVHParams = {}): void {\r\n this.bvh = new BatchedMeshBVH(this, coordinateSystem, config.margin, config.accurateCulling);\r\n this.bvh.create();\r\n}\r\n","import { BatchedMesh } from 'three';\r\nimport { radixSort, RadixSortOptions } from 'three/addons/utils/SortUtils.js';\r\nimport { MultiDrawRenderItem } from '../core/MultiDrawRenderList.js';\r\n\r\ntype radixSortCallback = (list: MultiDrawRenderItem[]) => void;\r\n\r\n/**\r\n * Creates a radix sort function specifically for sorting `BatchedMesh` instances.\r\n * The sorting is based on the `depth` property of each `MultiDrawRenderItem`.\r\n * This function dynamically adjusts for transparent materials by reversing the sort order if necessary.\r\n * @param target The `BatchedMesh` instance that contains the instances to be sorted.\r\n * @returns A radix sort function.\r\n * @reference https://github.com/mrdoob/three.js/blob/master/examples/webgl_mesh_batch.html#L291\r\n */\r\nexport function createRadixSort(target: BatchedMesh): radixSortCallback {\r\n const options: RadixSortOptions<MultiDrawRenderItem> = {\r\n get: (el) => el.zSort,\r\n aux: new Array(target.maxInstanceCount),\r\n reversed: null\r\n };\r\n\r\n return function sortFunction(list: MultiDrawRenderItem[]): void {\r\n options.reversed = target.material.transparent;\r\n\r\n if (target.maxInstanceCount > options.aux.length) {\r\n options.aux.length = target.maxInstanceCount;\r\n }\r\n\r\n let minZ = Infinity;\r\n let maxZ = -Infinity;\r\n\r\n for (const { z } of list) {\r\n if (z > maxZ) maxZ = z;\r\n if (z < minZ) minZ = z;\r\n }\r\n\r\n const depthDelta = maxZ - minZ;\r\n const factor = (2 ** 32 - 1) / depthDelta;\r\n\r\n for (const item of list) {\r\n item.zSort = (item.z - minZ) * factor;\r\n }\r\n\r\n radixSort(list, options);\r\n };\r\n}\r\n\r\n/** @internal */\r\nexport function sortOpaque(a: MultiDrawRenderItem, b: MultiDrawRenderItem): number {\r\n return a.z - b.z;\r\n}\r\n\r\n/** @internal */\r\nexport function sortTransparent(a: MultiDrawRenderItem, b: MultiDrawRenderItem): number {\r\n return b.z - a.z;\r\n}\r\n","import { BVHNode } from 'bvh.js';\r\nimport { BatchedMesh, BufferGeometry, Camera, Frustum, Material, Matrix4, Scene, Sphere, Vector3, WebGLRenderer } from 'three';\r\nimport { MultiDrawRenderItem, MultiDrawRenderList } from '../MultiDrawRenderList.js';\r\nimport { sortOpaque, sortTransparent } from '../../utils/SortingUtils.js';\r\n\r\n// TODO: fix LOD if no sorting and no culling\r\n\r\n/**\r\n * A custom sorting callback for render items.\r\n */\r\nexport type CustomSortCallback = (list: MultiDrawRenderItem[]) => void;\r\n\r\n/**\r\n * Callback invoked when an instance is within the frustum.\r\n * @param index The index of the instance.\r\n * @param camera The camera used for rendering.\r\n * @param cameraLOD The camera used for LOD calculations (provided only if LODs are initialized).\r\n * @param LODIndex The LOD level of the instance (provided only if LODs are initialized and `sortObjects` is false).\r\n * @returns True if the instance should be rendered, false otherwise.\r\n */\r\nexport type OnFrustumEnterCallback = (index: number, camera: Camera, cameraLOD?: Camera, LODIndex?: number) => boolean;\r\n\r\ndeclare module 'three' {\r\n interface BatchedMesh {\r\n /**\r\n * Callback function called if an instance is inside the frustum.\r\n */\r\n onFrustumEnter?: OnFrustumEnterCallback;\r\n\r\n /**\r\n * Performs frustum culling and sorting.\r\n * @param camera The main camera used for rendering.\r\n * @param cameraLOD The camera used for LOD calculations (provided only if LODs are initialized).\r\n */\r\n frustumCulling(camera: Camera, cameraLOD?: Camera): void;\r\n /**\r\n * Updates the index array for indirect rendering.\r\n */\r\n updateIndexArray(): void;\r\n /**\r\n * Updates the render list based on the current visibility and sorting settings.\r\n */\r\n updateRenderList(): void;\r\n /**\r\n * Performs BVH frustum culling.\r\n * @param camera The main camera used for rendering.\r\n * @param cameraLOD The camera used for LOD calculations (provided only if LODs are initialized).\r\n */\r\n BVHCulling(camera: Camera, cameraLOD: Camera): void;\r\n /**\r\n * Performs linear frustum culling.\r\n * @param camera The main camera used for rendering.\r\n * @param cameraLOD The camera used for LOD calculations (provided only if LODs are initialized).\r\n */\r\n linearCulling(camera: Camera, cameraLOD: Camera): void;\r\n }\r\n}\r\n\r\nconst _frustum = new Frustum();\r\nconst _renderList = new MultiDrawRenderList();\r\nconst _projScreenMatrix = new Matrix4();\r\nconst _invMatrixWorld = new Matrix4();\r\nconst _forward = new Vector3();\r\nconst _cameraPos = new Vector3();\r\nconst _cameraLODPos = new Vector3();\r\nconst _position = new Vector3();\r\nconst _sphere = new Sphere();\r\n\r\nexport function onBeforeRender(this: BatchedMesh, renderer: WebGLRenderer, scene: Scene, camera: Camera, geometry: BufferGeometry, material: Material, group: any): void {\r\n // TODO check if nothing changed\r\n this.frustumCulling(camera);\r\n}\r\n\r\nexport function frustumCulling(this: BatchedMesh, camera: Camera, cameraLOD = camera): void {\r\n if (!this._visibilityChanged && !this.perObjectFrustumCulled && !this.sortObjects) {\r\n return;\r\n }\r\n\r\n this._indirectTexture.needsUpdate = true;\r\n this._visibilityChanged = false;\r\n\r\n const sortObjects = this.sortObjects;\r\n const perObjectFrustumCulled = this.perObjectFrustumCulled;\r\n\r\n if (!perObjectFrustumCulled && !sortObjects) {\r\n this.updateIndexArray();\r\n return;\r\n }\r\n\r\n _invMatrixWorld.copy(this.matrixWorld).invert();\r\n _cameraPos.setFromMatrixPosition(camera.matrixWorld).applyMatrix4(_invMatrixWorld);\r\n _cameraLODPos.setFromMatrixPosition(cameraLOD.matrixWorld).applyMatrix4(_invMatrixWorld);\r\n _forward.set(0, 0, -1).transformDirection(camera.matrixWorld).transformDirection(_invMatrixWorld);\r\n\r\n if (!perObjectFrustumCulled) {\r\n this.updateRenderList();\r\n } else {\r\n _projScreenMatrix.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse).multiply(this.matrixWorld);\r\n\r\n if (this.bvh) this.BVHCulling(camera, cameraLOD);\r\n else this.linearCulling(camera, cameraLOD);\r\n }\r\n\r\n if (sortObjects) {\r\n const index = this.geometry.getIndex();\r\n const bytesPerElement = index === null ? 1 : index.array.BYTES_PER_ELEMENT;\r\n const multiDrawStarts = this._multiDrawStarts;\r\n const multiDrawCounts = this._multiDrawCounts;\r\n const indirectArray = this._indirectTexture.image.data as unknown as number[];\r\n const customSort = this.customSort as unknown as CustomSortCallback;\r\n\r\n if (customSort === null) {\r\n _renderList.array.sort(!this.material.transparent ? sortOpaque : sortTransparent);\r\n } else {\r\n customSort(_renderList.array); // TODO fix and remove second useless parameter... make a PR on main repo\r\n }\r\n\r\n const list = _renderList.array;\r\n const count = list.length;\r\n for (let i = 0; i < count; i++) {\r\n const item = list[i];\r\n multiDrawStarts[i] = item.start * bytesPerElement; // TODO multiply bytesPerElement in the renderList?\r\n multiDrawCounts[i] = item.count;\r\n indirectArray[i] = item.index;\r\n }\r\n\r\n _renderList.reset();\r\n }\r\n}\r\n\r\nexport function updateIndexArray(this: BatchedMesh): void {\r\n if (!this._visibilityChanged) return;\r\n\r\n const index = this.geometry.getIndex();\r\n const bytesPerElement = index === null ? 1 : index.array.BYTES_PER_ELEMENT;\r\n const instanceInfo = this._instanceInfo;\r\n const geometryInfoList = this._geometryInfo;\r\n const multiDrawStarts = this._multiDrawStarts;\r\n const multiDrawCounts = this._multiDrawCounts;\r\n const indirectArray = this._indirectTexture.image.data as unknown as number[];\r\n let count = 0;\r\n\r\n for (let i = 0, l = instanceInfo.length; i < l; i++) {\r\n const instance = instanceInfo[i];\r\n if (instance.visible && instance.active) {\r\n const geometryId = instance.geometryIndex;\r\n const geometryInfo = geometryInfoList[geometryId];\r\n\r\n multiDrawStarts[count] = geometryInfo.start * bytesPerElement;\r\n multiDrawCounts[count] = geometryInfo.count;\r\n indirectArray[count] = i;\r\n count++;\r\n }\r\n }\r\n\r\n this._multiDrawCount = count;\r\n}\r\n\r\nexport function updateRenderList(this: BatchedMesh): void {\r\n const instanceInfo = this._instanceInfo;\r\n const geometryInfoList = this._geometryInfo;\r\n\r\n for (let i = 0, l = instanceInfo.length; i < l; i++) {\r\n const instance = instanceInfo[i];\r\n if (instance.visible && instance.active) {\r\n const geometryId = instance.geometryIndex;\r\n const geometryInfo = geometryInfoList[geometryId];\r\n const depth = this.getPositionAt(i).sub(_cameraPos).dot(_forward); // getPosition instead of _sphere.center\r\n _renderList.push(i, depth, geometryInfo.start, geometryInfo.count);\r\n }\r\n }\r\n\r\n this._multiDrawCount = _renderList.array.length;\r\n}\r\n\r\nexport function BVHCulling(this: BatchedMesh, camera: Camera, cameraLOD: Camera): void {\r\n const index = this.geometry.getIndex();\r\n const bytesPerElement = index === null ? 1 : index.array.BYTES_PER_ELEMENT;\r\n const instanceInfo = this._instanceInfo;\r\n const geometryInfoList = this._geometryInfo;\r\n const sortObjects = this.sortObjects;\r\n const multiDrawStarts = this._multiDrawStarts;\r\n const multiDrawCounts = this._multiDrawCounts;\r\n const indirectArray = this._indirectTexture.image.data as unknown as number[];\r\n const onFrustumEnter = this.onFrustumEnter;\r\n let instancesCount = 0;\r\n\r\n this.bvh.frustumCulling(_projScreenMatrix, (node: BVHNode<{}, number>) => {\r\n const index = node.object;\r\n const instance = instanceInfo[index];\r\n\r\n // we don't check if active because we remove inactive instances from BVH\r\n if (!instance.visible) return;\r\n\r\n const geometryId = instance.geometryIndex;\r\n const geometryInfo = geometryInfoList[geometryId];\r\n const LOD = geometryInfo.LOD;\r\n let start: number;\r\n let count: number;\r\n\r\n if (LOD) {\r\n const distance = this.getPositionAt(index).distanceToSquared(_cameraLODPos);\r\n const LODIndex = this.getLODIndex(LOD, distance);\r\n if (onFrustumEnter && !onFrustumEnter(index, camera, cameraLOD, LODIndex)) return;\r\n start = LOD[LODIndex].start;\r\n count = LOD[LODIndex].count;\r\n } else {\r\n if (onFrustumEnter && !onFrustumEnter(index, camera)) return;\r\n start = geometryInfo.start;\r\n count = geometryInfo.count;\r\n }\r\n\r\n // TODO don't reuse getPositionAt for sort\r\n // TODO LOD optimized if bvh and sort?\r\n\r\n if (sortObjects) {\r\n const depth = this.getPositionAt(index).sub(_cameraPos).dot(_forward);\r\n _renderList.push(index, depth, start, count);\r\n } else {\r\n multiDrawStarts[instancesCount] = start * bytesPerElement;\r\n multiDrawCounts[instancesCount] = count;\r\n indirectArray[instancesCount] = index;\r\n instancesCount++;\r\n }\r\n });\r\n\r\n this._multiDrawCount = sortObjects ? _renderList.array.length : instancesCount;\r\n}\r\n\r\nexport function linearCulling(this: BatchedMesh, camera: Camera, cameraLOD: Camera): void {\r\n const index = this.geometry.getIndex();\r\n const bytesPerElement = index === null ? 1 : index.array.BYTES_PER_ELEMENT;\r\n const instanceInfo = this._instanceInfo;\r\n const geometryInfoList = this._geometryInfo;\r\n const sortObjects = this.sortObjects;\r\n const multiDrawStarts = this._multiDrawStarts;\r\n const multiDrawCounts = this._multiDrawCounts;\r\n const indirectArray = this._indirectTexture.image.data as unknown as number[];\r\n const onFrustumEnter = this.onFrustumEnter;\r\n let instancesCount = 0;\r\n\r\n _frustum.setFromProjectionMatrix(_projScreenMatrix);\r\n\r\n for (let i = 0, l = instanceInfo.length; i < l; i++) {\r\n const instance = instanceInfo[i];\r\n if (!instance.visible || !instance.active) continue;\r\n\r\n const geometryId = instance.geometryIndex;\r\n const geometryInfo = geometryInfoList[geometryId];\r\n const LOD = geometryInfo.LOD;\r\n let start: number;\r\n let count: number;\r\n\r\n const bSphere = geometryInfo.boundingSphere;\r\n const radius = bSphere.radius;\r\n const center = bSphere.center;\r\n const geometryCentered = center.x === 0 && center.y === 0 && center.z === 0; // TODO add to geometryInfo?\r\n\r\n if (geometryCentered) {\r\n const maxScale = this.getPositionAndMaxScaleOnAxisAt(i, _sphere.center);\r\n _sphere.radius = radius * maxScale;\r\n } else {\r\n this.applyMatrixAtToSphere(i, _sphere, center, radius);\r\n }\r\n\r\n if (!_frustum.intersectsSphere(_sphere)) continue;\r\n\r\n if (LOD) {\r\n const distance = _sphere.center.distanceToSquared(_cameraLODPos);\r\n const LODIndex = this.getLODIndex(LOD, distance);\r\n if (onFrustumEnter && !onFrustumEnter(i, camera, cameraLOD, LODIndex)) continue;\r\n start = LOD[LODIndex].start;\r\n count = LOD[LODIndex].count;\r\n } else {\r\n if (onFrustumEnter && !onFrustumEnter(i, camera)) continue;\r\n start = geometryInfo.start;\r\n count = geometryInfo.count;\r\n }\r\n\r\n // TODO LOD optimized if sort?\r\n\r\n if (sortObjects) {\r\n const depth = _position.subVectors(_sphere.center, _cameraPos).dot(_forward);\r\n _renderList.push(i, depth, start, count);\r\n } else {\r\n multiDrawStarts[instancesCount] = start * bytesPerElement;\r\n multiDrawCounts[instancesCount] = count;\r\n indirectArray[instancesCount] = i;\r\n instancesCount++;\r\n }\r\n }\r\n\r\n this._multiDrawCount = sortObjects ? _renderList.array.length : instancesCount;\r\n}\r\n","import { BatchedMesh, Sphere, Vector3 } from 'three';\r\n\r\ndeclare module 'three' {\r\n interface BatchedMesh {\r\n /**\r\n * Retrieves the position of a specific instance.\r\n * @param index The index of the instance.\r\n * @param target Optional `Vector3` to store the result.\r\n * @returns The position of the instance as a `Vector3`.\r\n */\r\n getPositionAt(index: number, target?: Vector3): Vector3;\r\n /**\r\n * Retrieves the position and maximum scale on any axis of a specific instance.\r\n * @param index The index of the instance.\r\n * @param position Optional `Vector3` to store the position result.\r\n * @returns The maximum scale on any axis.\r\n */\r\n getPositionAndMaxScaleOnAxisAt(index: number, position: Vector3): number;\r\n /**\r\n * Applies the transformation matrix of a specific instance to a sphere.\r\n * @param index The index of the instance.\r\n * @param sphere The sphere to transform.\r\n * @param center TODO\r\n * @param radius TODO\r\n */\r\n applyMatrixAtToSphere(index: number, sphere: Sphere, center: Vector3, radius: number): void;\r\n }\r\n}\r\n\r\nconst _position = new Vector3();\r\n\r\nexport function getPositionAt(this: BatchedMesh, index: number, target = _position): Vector3 {\r\n const offset = index * 16;\r\n const array = this._matricesTexture.image.data as unknown as number[];\r\n\r\n target.x = array[offset + 12];\r\n target.y = array[offset + 13];\r\n target.z = array[offset + 14];\r\n\r\n return target;\r\n}\r\n\r\nexport function getPositionAndMaxScaleOnAxisAt(this: BatchedMesh, index: number, position: Vector3): number {\r\n const offset = index * 16;\r\n const array = this._matricesTexture.image.data as unknown as number[];\r\n\r\n const te0 = array[offset + 0];\r\n const te1 = array[offset + 1];\r\n const te2 = array[offset + 2];\r\n const scaleXSq = te0 * te0 + te1 * te1 + te2 * te2;\r\n\r\n const te4 = array[offset + 4];\r\n const te5 = array[offset + 5];\r\n const te6 = array[offset + 6];\r\n const scaleYSq = te4 * te4 + te5 * te5 + te6 * te6;\r\n\r\n const te8 = array[offset + 8];\r\n const te9 = array[offset + 9];\r\n const te10 = array[offset + 10];\r\n const scaleZSq = te8 * te8 + te9 * te9 + te10 * te10;\r\n\r\n position.x = array[offset + 12];\r\n position.y = array[offset + 13];\r\n position.z = array[offset + 14];\r\n\r\n return Math.sqrt(Math.max(scaleXSq, scaleYSq, scaleZSq));\r\n}\r\n\r\nexport function applyMatrixAtToSphere(this: BatchedMesh, index: number, sphere: Sphere, center: Vector3, radius: number): void {\r\n const offset = index * 16;\r\n const array = this._matricesTexture.image.data as unknown as number[];\r\n\r\n const te0 = array[offset + 0];\r\n const te1 = array[offset + 1];\r\n const te2 = array[offset + 2];\r\n const te3 = array[offset + 3];\r\n const te4 = array[offset + 4];\r\n const te5 = array[offset + 5];\r\n const te6 = array[offset + 6];\r\n const te7 = array[offset + 7];\r\n const te8 = array[offset + 8];\r\n const te9 = array[offset + 9];\r\n const te10 = array[offset + 10];\r\n const te11 = array[offset + 11];\r\n const te12 = array[offset + 12];\r\n const te13 = array[offset + 13];\r\n const te14 = array[offset + 14];\r\n const te15 = array[offset + 15];\r\n\r\n const position = sphere.center;\r\n const x = center.x;\r\n const y = center.y;\r\n const z = center.z;\r\n const w = 1 / (te3 * x + te7 * y + te11 * z + te15);\r\n\r\n position.x = (te0 * x + te4 * y + te8 * z + te12) * w;\r\n position.y = (te1 * x + te5 * y + te9 * z + te13) * w;\r\n position.z = (te2 * x + te6 * y + te10 * z + te14) * w;\r\n\r\n const scaleXSq = te0 * te0 + te1 * te1 + te2 * te2;\r\n const scaleYSq = te4 * te4 + te5 * te5 + te6 * te6;\r\n const scaleZSq = te8 * te8 + te9 * te9 + te10 * te10;\r\n\r\n sphere.radius = radius * Math.sqrt(Math.max(scaleXSq, scaleYSq, scaleZSq));\r\n}\r\n","import { BatchedMesh, BufferGeometry, TypedArray } from 'three';\r\n\r\n// TODO: add optional distance and first load function like InstancedMesh2\r\n\r\nexport type LODInfo = { start: number; count: number; distance: number; hysteresis: number };\r\n\r\ndeclare module 'three' {\r\n interface BatchedMesh {\r\n /**\r\n * Adds a Level of Detail (LOD) geometry to the BatchedMesh.\r\n * @param geometryId The ID of the geometry to which the LOD is being added.\r\n * @param geometryOrIndex The BufferGeometry to be added as LOD or the index array.\r\n * @param distance The distance at which this LOD should be used.\r\n * @param hysteresis Optional hysteresis value for LOD transition.\r\n */\r\n addGeometryLOD(geometryId: number, geometryOrIndex: BufferGeometry | TypedArray, distance: number, hysteresis?: number): void;\r\n /**\r\n * Retrieves the LOD index for a given distance.\r\n * @param LOD The array of LOD information.\r\n * @param distance The distance to check against the LODs.\r\n * @returns The index of the appropriate LOD\r\n */\r\n getLODIndex(LOD: LODInfo[], distance: number): number;\r\n }\r\n}\r\n\r\nexport function addGeometryLOD(this: BatchedMesh, geometryId: number, geoOrIndex: BufferGeometry | TypedArray, distance: number, hysteresis = 0): void {\r\n const geometryInfo = this._geometryInfo[geometryId];\r\n const srcIndexArray = (geoOrIndex as BufferGeometry).isBufferGeometry ? (geoOrIndex as BufferGeometry).index.array : geoOrIndex as TypedArray;\r\n distance = distance ** 2;\r\n\r\n geometryInfo.LOD ??= [{ start: geometryInfo.start, count: geometryInfo.count, distance: 0, hysteresis: 0 }];\r\n\r\n const LOD = geometryInfo.LOD;\r\n const lastLOD = LOD[LOD.length - 1];\r\n const start = lastLOD.start + lastLOD.count;\r\n const count = srcIndexArray.length;\r\n\r\n if ((start - geometryInfo.start) + count > geometryInfo.reservedIndexCount) {\r\n throw new Error('BatchedMesh LOD: Reserved space request exceeds the maximum buffer size.');\r\n }\r\n\r\n LOD.push({ start, count, distance, hysteresis });\r\n\r\n const dstIndex = this.geometry.getIndex();\r\n const dstIndexArray = dstIndex.array;\r\n const vertexStart = geometryInfo.vertexStart;\r\n\r\n for (let i = 0; i < count; i++) {\r\n dstIndexArray[start + i] = srcIndexArray[i] + vertexStart;\r\n }\r\n\r\n dstIndex.needsUpdate = true;\r\n}\r\n\r\nexport function getLODIndex(LODs: LODInfo[], distance: number): number {\r\n for (let i = LODs.length - 1; i > 0; i--) {\r\n const level = LODs[i];\r\n const levelDistance = level.distance - (level.distance * level.hysteresis);\r\n if (distance >= levelDistance) return i;\r\n }\r\n\r\n return 0;\r\n}\r\n","import { BatchedMesh, Box3, Intersection, Matrix4, Mesh, Ray, Raycaster, Sphere, Vector3 } from 'three';\r\nimport { } from 'three-mesh-bvh'; // include only types\r\n\r\ndeclare module 'three' {\r\n interface BatchedMesh {\r\n checkInstanceIntersection(raycaster: Raycaster, objectIndex: number, result: Intersection[]): void;\r\n }\r\n}\r\n\r\nconst _intersections: Intersection[] = [];\r\nconst _mesh = new Mesh();\r\nconst _ray = new Ray();\r\nconst _direction = new Vector3();\r\nconst _worldScale = new Vector3();\r\nconst _invMatrixWorld = new Matrix4();\r\n\r\nexport function raycast(this: BatchedMesh, raycaster: Raycaster, result: Intersection[]): void {\r\n if (!this.material || this.instanceCount === 0) return;\r\n\r\n _mesh.geometry = this.geometry;\r\n _mesh.material = this.material;\r\n\r\n _mesh.geometry.boundingBox ??= new Box3();\r\n _mesh.geometry.boundingSphere ??= new Sphere();\r\n\r\n const originalRay = raycaster.ray;\r\n const originalNear = raycaster.near;\r\n const originalFar = raycaster.far;\r\n\r\n _invMatrixWorld.copy(this.matrixWorld).invert();\r\n\r