import * as THREE from 'three/webgpu'; import { Text } from './Text.cjs'; import type { TextMaterial, TextSyncCallback } from './Text.cjs'; import type { BufferGeometry, Camera, ColorRepresentation, ComputeNode, Event, IndirectStorageBufferAttribute, InstancedInterleavedBuffer, Material, Object3D, Renderer, Scene, StorageBufferAttribute, StorageInstancedBufferAttribute, TypedArray } from 'three/webgpu'; import type { WebGLRenderer } from 'three'; import type { TSLStorageNode, TSLUniformNode } from '../types/tsl.cjs'; export interface BatchedTextMemberRecord { index: number; glyphCount: number; glyphOffset?: number | undefined; dirty: boolean; } export interface BatchedTextGlyphData { atlasIndex?: number | undefined; memberIndex?: number | undefined; letterIndex?: number | undefined; bounds?: [ minX: number | undefined, minY: number | undefined, maxX: number | undefined, maxY: number | undefined ]; } export interface BatchedTextGlyphUpdate { atlasIndex?: number | undefined; index?: number | undefined; glyphIndex?: number | undefined; bounds?: ArrayLike | undefined; letterIndex?: number | undefined; letter?: number | undefined; } export interface BatchedTextCullOptions { sortObjects: boolean; useFrustum: boolean; lodNear: number; lodFar: number; lodMode: number; lodDensity: number; frustumPadXY: number; frustumPadZNear: number; frustumPadZFar: number; nearRadius?: number | undefined; falloffExp?: number | undefined; useLod?: boolean | undefined; } export interface BatchedTextMatrixAttribute { readonly isBufferAttribute?: boolean | undefined; readonly isInterleavedBufferAttribute?: boolean | undefined; readonly isInstancedBufferAttribute?: boolean | undefined; readonly isInstancedInterleavedBuffer?: boolean | undefined; readonly isStorageBufferAttribute?: boolean | undefined; readonly isStorageInstancedBufferAttribute?: boolean | undefined; array: TypedArray; count: number; itemSize?: number | undefined; needsUpdate: boolean; setArray?(array: TypedArray): unknown; } export interface BatchedTextMaterial extends TextMaterial { _isUtsuboBatchedText?: boolean | undefined; _uMemberCount?: TSLUniformNode<'float', number> | undefined; _textVisSSBO?: StorageBufferAttribute | undefined; _batchedMatricesBuffer?: BatchedTextMatrixAttribute | null | undefined; _batchedParamsABuffer?: InstancedInterleavedBuffer | undefined; _batchedParamsBBuffer?: InstancedInterleavedBuffer | undefined; } export interface BatchedTextBoundingSphere { center: THREE.Vector3; radius: number; } export interface BatchedTextGUIController { name(label: string): unknown; } export interface BatchedTextGUIFolder { add(target: object, property: string, ...options: unknown[]): BatchedTextGUIController; addFolder?(name: string): BatchedTextGUIFolder; destroy?(): void; parent?: { remove(controller: unknown): void; } | undefined; paramList?: unknown; } export interface BatchedTextCuller { REF_COUNT: number; activeCount: TSLUniformNode<'uint', number> | null; refPosSSBO: StorageBufferAttribute; outVisSSBO: StorageBufferAttribute; outIdNode: TSLStorageNode<'uint'>; drawStructNode: TSLStorageNode<'struct'>; perInstanceBoundingBox: boolean; boundingSpheresSSBO: StorageBufferAttribute | null; lodNear: TSLUniformNode<'float', number>; lodFar: TSLUniformNode<'float', number>; lodMode: TSLUniformNode<'float', number>; lodDensity: TSLUniformNode<'float', number>; enabled: TSLUniformNode<'bool', boolean>; frustumPadXY: TSLUniformNode<'float', number>; frustumPadZNear: TSLUniformNode<'float', number>; frustumPadZFar: TSLUniformNode<'float', number>; attachMesh(mesh: Object3D): void; setCameraUniforms(camera: Camera): void; update(): void; initBoundingSpheresStorage(data: Float32Array): void; setMaxBoundingSphere(boundsData: Float32Array): void; dispose(): void; } /** * ### WebGL Support has been temporarily disabled. * * High-performance batched rendering for multiple `Text` instances with GPU-accelerated * culling, sorting, and instancing. Renders thousands of text labels in a single draw call * with automatic transparency sorting for correct z-ordering. * * ## Why BatchedText is Fast * * **Single Draw Call Architecture** * - Renders all text instances in ONE draw call vs N draw calls * - Eliminates CPU overhead from state changes and draw call submission * - Uses GPU instancing with `StorageBufferAttribute` for transforms and styles * - Shared SDF atlas texture across all text members * * **GPU Compute Pipeline (WebGPU)** * - Frustum culling runs entirely on GPU via compute shaders * - LOD-based glyph sampling reduces overdraw for distant text * - Automatic back-to-front sorting for transparent SDF text in the GPU via bitonic sorting in compute shaders * - Indirect draw commands updated on GPU without CPU readback * - Dynamic workgroup dispatch via `computeIndirect` for efficient scaling * * **Transparency Z-Ordering** * - Sorted glyph packing ensures correct depth ordering for SDF anti-aliasing * - Per-member prefix sum computes glyph offsets in sorted order * - Glyphs scattered to packed buffer preserving back-to-front order * - Eliminates z-fighting between overlapping transparent text * * **Dynamic Updates with Pre-allocation** * - Set `maxTextCount` and `maxGlyphCount` to pre-allocate buffers for dynamic `addText()` * - Without pre-allocation, buffers are sized to initial content (no dynamic growth) * - `computeIndirect` dynamically adjusts GPU workgroup counts * - Uniform-based count tracking avoids shader recompilation within capacity * * ## Features * * - **Batched Rendering**: Single draw call for unlimited text instances * - **GPU Frustum Culling**: Skip rendering off-screen text (WebGPU only) * - **LOD Sampling**: Reduce glyph count for distant text * - **Billboarding**: Camera-facing text in world space * - **Per-Instance Styling**: Color, outline, opacity per text member * - **Dynamic Updates**: Add/remove/modify text at runtime via `sync()` * - **Static Mode**: `batch.staticMode = true`. Lock layout after initial pack for maximum performance * - **Transparency Sorting**: Correct z-order for SDF anti-aliased edges * - **TSL Hooks**: Custom `positionNode`, `opacityNode`, `colorNode` still work * * ## Usage * * ```js * import { BatchedText, Text } from 'three-blocks/experimental/runtime-sdf-text'; * * // maxTextCount= 5000 -Pre-allocate for up to 5000 text instances * // maxGlyphCount= 100000 - Pre-allocate for ~100k total glyphs (~20 per text) * // Create batched text container with pre-allocation for dynamic updates * const batch = new BatchedText(5000, 100000, material); * batch.billboarding = true * batch.staticMode = true * * // Add initial text instances * for (let i = 0; i < 1000; i++) { * const text = new Text(); * text.text = `Label ${i}`; * text.fontSize = 1; * text.color.setHSL(Math.random(), 0.7, 0.6); * text.position.set( Math.random()*20-10, Math.random()*20-10, Math.random()*20-10 ); * text.updateMatrixWorld(); * const id = batch.addText(text); * * // Set transform (like BatchedMesh API) are also available * // matrix.compose(position, quaternion, scale); * // batch.setMatrixAt(id, matrix); * // batch.setColorAt(id, color); * } * * scene.add(batch); * * // Dynamically add more text later (within maxTextCount/maxGlyphCount limits) * const newText = new Text(); * newText.text = 'Dynamic!'; * const newId = batch.addText(newText); // Returns -1 if capacity exceeded * if (newId >= 0) { * batch.setMatrixAt(newId, matrix); * } * * // Update existing text content * const text = batch.getTextAt(0); * text.text = 'Updated!'; * ``` * * ## Performance Tips * * - Use `static: true` if text content never changes after initial setup * - Enable `perObjectFrustumCulled` for large scenes with many off-screen labels * - Use `setCullingOptions()` to configure frustum culling and LOD sampling * - Range LOD uses `lodNear` + `lodFar` * - Exp LOD uses `lodNear` + `lodDensity` (exp density). `lodNear` shifts the start of the exp curve. * - Call `sync()` only when text content changes, not every frame * * ## LOD API * * ```js * // Range mode (distance-based fade) * batch.setCullingOptions({ * useFrustum: true, * lodMode: LOD_MODE_RANGE, * lodNear: 50, * lodFar: 400, * }); * * // Exp mode (density-based fade) * batch.setCullingOptions({ * lodMode: LOD_MODE_EXP, * lodNear: 50, * lodDensity: 0.00025, * }); * * // Disable LOD * batch.setCullingOptions({ lodMode: LOD_MODE_DISABLED }); * * // Optional: create the culler immediately (before first render) * batch.initCuller( renderer ); * batch.culler.lodMode.value = LOD_MODE_EXP; * ``` * * ## WebGL Limitations * * - GPU frustum culling requires WebGPU (compute shaders not available in WebGL) * - `perObjectFrustumCulled` automatically disabled on WebGL backend * - All other features (batching, billboarding, styling) work in WebGL * * @demo docs/demos/text_batched.html * @class BatchedText * @extends Text * @tags WebGPU * @short Batched renderer for many Text instances in one draw call with GPU culling, LOD, and sorting; shared SDF atlas. * @category Text * @fires syncstart - Fired when sync begins * @fires synccomplete - Fired when sync completes and buffers are ready */ export declare class BatchedText extends Text { material: BatchedTextMaterial; count: number; needsUpdate: boolean; staticMode: boolean; culler: BatchedTextCuller | null; cullerCapacity: number; _members: Map; _memberIndexLookup: (Text | undefined)[]; _maxTextCount: number; _maxGlyphCount: number; _isWebGL: boolean | null; _lastRenderer: Renderer | WebGLRenderer | null; _externalMatrixStorage: BatchedTextMatrixAttribute | null; _useExternalMatrices: boolean; _instanceMatrix: BatchedTextMatrixAttribute | null; _needsRepack: boolean; _paramsAArray: Float32Array; _paramsBArray: Float32Array; _paramsABuffer: InstancedInterleavedBuffer; _paramsBBuffer: InstancedInterleavedBuffer; _visibilityDirty: boolean; _onMemberSynced: (event: Event<'synccomplete', Text>) => void; _cullOptions: BatchedTextCullOptions; _visFallbackSSBO: StorageBufferAttribute; _perTextBoundingBox: boolean; _textBoundingSpheresArray: Float32Array; _textBoundingSpheresSSBO: StorageBufferAttribute | null; _maxBoundingSphere: BatchedTextBoundingSphere; _glyphCount: number; _glyphSrcBoundsSSBO: StorageBufferAttribute | null; _glyphSrcMetaSSBO: StorageBufferAttribute | null; _glyphPackedBoundsSIBA: StorageInstancedBufferAttribute | null; _glyphPackedMetaSSBO: StorageBufferAttribute | null; _glyphIndirect: IndirectStorageBufferAttribute | null; _glyphDrawStructNode: TSLStorageNode<'struct'> | null; _glyphPackNodes: Record | null; _packClearArgs: ComputeNode | null; _packSelect: ComputeNode | null; _packInitialized: boolean; _sortedMemberCount: number; _activeMemberCount: number; _glyphCapacity: number; _memberCapacity: number; _memberGlyphCountSSBO: StorageBufferAttribute | null; _memberGlyphOffsetSSBO: StorageBufferAttribute | null; _memberGlyphHeadSSBO: StorageBufferAttribute | null; _glyphDispatchIndirect: IndirectStorageBufferAttribute | null; _memberDispatchIndirect: IndirectStorageBufferAttribute | null; _uGlyphCount: TSLUniformNode<'uint', number> | null; _uMemberCount: TSLUniformNode<'uint', number> | null; _packUpdateDispatch: ComputeNode | null; _packClearMemberCounts: ComputeNode | null; _packCountGlyphs: ComputeNode | null; _packPrefixSum: ComputeNode | null; _packClearHeads: ComputeNode | null; _packScatterSorted: ComputeNode | null; _usePackedGlyphs: boolean; _currentVisSSBO: StorageBufferAttribute; _updateRefPosEachFrame: boolean; _uParentWorld: TSLUniformNode<'mat4', THREE.Matrix4>; _gpuRefPosCompute: ComputeNode | null; _gpuRefPosCount: number; _hasInitialSync: boolean; _visFromCullerActive: boolean; _transformsDirty: boolean; _capacityWarningLogged: boolean | undefined; _guiFolder: BatchedTextGUIFolder | null | undefined; /** * Create a batched text container. * * @param {number} [maxTextCount=Infinity] Maximum number of Text instances. Pre-allocates buffers for this capacity. Set to enable dynamic addText() without buffer overflow. * @param {number} [maxGlyphCount=Infinity] Maximum total glyphs across all text. Pre-allocates glyph buffers. Estimate ~10-50 glyphs per text instance. * @param {THREE.Material} [material] Base material (creates default if omitted). */ constructor(maxTextCount?: number | null, maxGlyphCount?: number | null, material?: BatchedTextMaterial | null); get billboarding(): boolean; set billboarding(v: boolean); get instanceMatrix(): BatchedTextMatrixAttribute | null; set instanceMatrix(attribute: BatchedTextMatrixAttribute | null); get perObjectFrustumCulled(): boolean; set perObjectFrustumCulled(v: boolean); /** * Configure GPU frustum culling and distance LOD without reaching into internal state. * Omitted fields retain their current values. * * @param {Object} options Culling and LOD values to update. * @returns {this} */ setCullingOptions(options: Partial): this; /** * Add objects to batch. `Text` instances become batched members. * Non-Text objects are added to scene graph normally. * * @param {...THREE.Object3D} objs Objects to add. * @returns {this} */ add(...objs: Object3D[]): this; /** * Remove objects from batch. * * @param {...THREE.Object3D} objs Objects to remove. * @returns {this} */ remove(...objs: Object3D[]): this; /** * Register a Text instance as a batched member. * Returns an instance ID that can be used with setMatrixAt, setColorAt, etc. * * If `maxTextCount` was specified in constructor options, this will return -1 * and log a warning when the limit is exceeded. * * @param {Text} text Text instance to batch. * @return {number} The instance ID for this text, or -1 if capacity exceeded or already added with same index. */ addText(text: Text): number; /** * Unregister a Text instance from batch. * * @param {Text} text Text instance to remove. */ removeText(text: Text): void; /** * Get the Text instance at the given instance ID. * * @param {number} instanceId The instance ID. * @return {Text|null} The Text instance, or null if not found. */ getTextAt(instanceId: number): Text | null; /** * Sets the given local transformation matrix to the defined text instance. * This updates the text's matrix and marks it for update. * * @param {number} instanceId The instance ID of the text to set the matrix of. * @param {THREE.Matrix4} matrix A 4x4 matrix representing the local transformation. * @return {BatchedText} A reference to this batched text. */ setMatrixAt(instanceId: number, matrix: THREE.Matrix4): this; /** * Returns the local transformation matrix of the defined text instance. * * @param {number} instanceId The instance ID of the text to get the matrix of. * @param {THREE.Matrix4} matrix The target object that is used to store the result. * @return {THREE.Matrix4} The text instance's local transformation matrix. */ getMatrixAt(instanceId: number, matrix: THREE.Matrix4): THREE.Matrix4; /** * Sets the given color to the defined text instance. * * @param {number} instanceId The instance ID of the text to set the color of. * @param {THREE.Color|number|string} color The color to set the instance to. * @return {BatchedText} A reference to this batched text. */ setColorAt(instanceId: number, color: ColorRepresentation): this; /** * Returns the color of the defined text instance. * * @param {number} instanceId The instance ID of the text to get the color of. * @param {THREE.Color} color The target object that is used to store the result. * @return {THREE.Color} The text instance's color. */ getColorAt(instanceId: number, color: THREE.Color): THREE.Color; /** * Returns glyph data for the given text instance without triggering a full sync. * Only the first glyph range for the member is returned; multi-glyph members are supported via glyphOffset/count. * * @param {number} instanceId * @param {object} [target] Optional target object to populate. * @returns {{atlasIndex:number,bounds:number[],letterIndex:number}|null} */ getGlyphAt(instanceId: number, target?: BatchedTextGlyphData): BatchedTextGlyphData | null; /** * Quickly update glyph data (atlas index, bounds, letter index) for a specific text instance * without triggering a full text sync/layout. Useful for single-glyph members such as counters. * * @param {number} instanceId * @param {{atlasIndex: (number|undefined), bounds: (number[]|undefined), letterIndex: (number|undefined)}} glyph * @returns {this} */ setGlyphAt(instanceId: number, glyph: BatchedTextGlyphUpdate | null | undefined): this; /** * Update world matrices and recompute bounds. * * @param {boolean} [force] Force update even if matrices haven't changed. */ updateMatrixWorld(force?: boolean): void; /** * Recompute bounding volumes from all member bounds. */ updateBounds(): void; /** * Returns true if running on WebGL backend (no compute shader support). * Returns null if backend hasn't been detected yet. * @type {boolean|null} */ get isWebGL(): boolean | null; /** * Returns true if GPU culling is active (WebGPU only, and culling requested). * @type {boolean} */ get isCullingActive(): boolean; /** * Detect WebGL backend and disable culling features if necessary. * Called on first render when renderer is available. * @param {THREE.WebGPURenderer} renderer * @private */ _detectBackend(renderer: Renderer | WebGLRenderer | null | undefined): void; /** * Configure material with batched instance data and TSL nodes. * * @param {THREE.Material} material Target material. * @private */ prepareMaterial(material: BatchedTextMaterial): void; /** * Internal render hook. Ensures children are synced, updates/creates the GPU culler, * refreshes visibility buffers and material flags, and lazily prepares storage buffers. * @internal */ onBeforeRender(renderer: Renderer | WebGLRenderer, _scene: Scene, camera: Camera, _geometry: BufferGeometry, material: Material): void; /** * Initialize the GPU culler early (before first render). * Useful when you need to access `culler` immediately. * * @param {THREE.WebGPURenderer} renderer Renderer instance. * @returns {ComputeInstanceCulling|null} The culler, or null if WebGL or sync not completed yet. */ initCuller(renderer: Renderer): BatchedTextCuller | null; /** * Internal render hook. Restore material-side settings after render. * @internal */ onAfterRender(_renderer: Renderer | WebGLRenderer, _scene: Scene, _camera: Camera, _geometry: BufferGeometry, material: Material): void; /** * Populate per-member storage buffers (matrices and style parameters), then call `super._prepareForRender`. * @param {THREE.Material} material * @internal */ _prepareForRender(material: BatchedTextMaterial): void; /** * Synchronize all member `Text` instances. Triggers repacking of instance attributes * when any member has changed, and rebuilds glyph data arrays. * * **Important:** This only syncs text *content* changes. Position/transform changes * require calling `setMatrixAt(instanceId, matrix)` after updating `text.position`. * * @param {function(): void} [callback] - Called when sync completes * @param {THREE.WebGPURenderer} renderer - **Required on first call** to detect * WebGPU/WebGL backend for proper SDF texture handling via `getTextRenderInfo()`. * Subsequent calls can omit this if the backend was already detected. * * @example * ```javascript * // Content change - requires sync with renderer * text.text = 'Updated!'; * batchedText.sync(null, renderer); * * // Position change - requires setMatrixAt, NOT sync * text.position.set(x, y, z); * text.updateMatrixWorld(); * batchedText.setMatrixAt(instanceId, text.matrixWorld); * ``` */ sync(callback?: TextSyncCallback | null, renderer?: Renderer): void; /** * Calculate bounding spheres for all text members based on their blockBounds. * Called internally after sync() completes. * Only used for GPU culling on WebGPU - skipped on WebGL. * @private */ _updateTextBoundingSpheres(): void; /** * Get the bounding sphere for a specific text instance (local space). * * @param {number} instanceId The instance ID of the text. * @returns {{center: THREE.Vector3, radius: number}|null} */ getTextBoundingSphereAt(instanceId: number): BatchedTextBoundingSphere | null; /** * Get or set per-text bounding box mode. * When true, each text instance gets its own bounding sphere for more accurate culling. * When false (default), uses the maximum bounding sphere of all instances for faster culling. * @type {boolean} */ get perTextBoundingBox(): boolean; set perTextBoundingBox(value: boolean); /** * Update a single member's instance matrix in the storage buffer using its current world matrix. * Does not recompute style parameters. * @param {Text} text */ updateMemberMatrixWorld(text: Text): void; /** * Ensure the GPU instance culler exists and matches the current member count. * Rebuilds when the number of members changes and wires visibility SSBO to the material. * @param {THREE.WebGPURenderer} renderer * @internal */ _ensureCuller(renderer: Renderer | null | undefined): void; /** * Build or rebuild the glyph packing compute/storage to enable indirect draws of visible glyphs. * Allocates SSBOs, sets up compute kernels for clearing and selecting visible glyphs, and marks * the material to rebuild with packed storage. * @param {number} totalGlyphs * @internal */ _ensureGlyphPackCompute(totalGlyphs: number, memberCount: number): void; /** * Rebuild only the kernels that reference the visibility buffer. * Called when _currentVisSSBO changes (e.g., from fallback to culler's buffer). * This avoids reallocating storage buffers which would wipe glyph data. * @internal */ _rebuildVisibilityKernels(): void; /** * Update culling (camera uniforms and per-member ref positions) and run glyph packing compute passes. * This is called automatically in `onBeforeRender`, so manual calls are typically not needed. * @param {THREE.WebGPURenderer} renderer * @param {THREE.Camera} camera * @internal */ updateCullingAndPacking(renderer: Renderer | null | undefined, camera: Camera): void; /** * Attach a simple GUI folder with controls for culling and LOD settings. * Compatible with lil-gui, dat.gui, and Three.js Inspector. * Note: Culling options are only shown on WebGPU backend. * @param {object} folder - A lil-gui instance or folder. */ attachGUI(folder: BatchedTextGUIFolder | null | undefined): void; /** * Destroy the attached GUI and clear reference. */ disposeGUI(): void; /** * Dispose resources associated with this helper and detach debug UI. */ dispose(): void; }