UNPKG

@babylonjs/core

Version:

Getting started? Play directly with the Babylon.js API using our [playground](https://playground.babylonjs.com/). It also contains a lot of samples to learn how to use it.

638 lines (637 loc) 28.3 kB
import { Scene } from "../scene.js"; import { Buffer, VertexBuffer } from "../Buffers/buffer.js"; import { AbstractMesh } from "../Meshes/abstractMesh.js"; import { Matrix, Vector3 } from "../Maths/math.vector.js"; import { SmartArray } from "../Misc/smartArray.js"; import { SceneComponentConstants } from "../sceneComponent.js"; import { BoundingBox } from "../Culling/boundingBox.js"; import { Material } from "../Materials/material.js"; import { ShaderMaterial } from "../Materials/shaderMaterial.js"; import { Color3 } from "../Maths/math.color.js"; import { Observable } from "../Misc/observable.js"; import { DrawWrapper } from "../Materials/drawWrapper.js"; import { UniformBuffer } from "../Materials/uniformBuffer.js"; import { CreateBoxVertexData } from "../Meshes/Builders/boxBuilder.js"; import { _RetryWithInterval } from "../Misc/timingTools.js"; import { Logger } from "../Misc/logger.js"; Object.defineProperty(Scene.prototype, "forceShowBoundingBoxes", { get: function () { return this._forceShowBoundingBoxes || false; }, set: function (value) { this._forceShowBoundingBoxes = value; // Lazyly creates a BB renderer if needed. if (value) { this.getBoundingBoxRenderer(); } }, enumerable: true, configurable: true, }); Scene.prototype.getBoundingBoxRenderer = function () { if (!this._boundingBoxRenderer) { this._boundingBoxRenderer = new BoundingBoxRenderer(this); } return this._boundingBoxRenderer; }; Object.defineProperty(AbstractMesh.prototype, "showBoundingBox", { get: function () { return this._showBoundingBox || false; }, set: function (value) { this._showBoundingBox = value; // Lazyly creates a BB renderer if needed. if (value) { this.getScene().getBoundingBoxRenderer(); } }, enumerable: true, configurable: true, }); const TempMatrix = Matrix.Identity(); const TempVec1 = new Vector3(); const TempVec2 = new Vector3(); // `Matrix.asArray` returns its internal array, so it can be directly updated const TempMatrixArray = TempMatrix.asArray(); // BoundingBox copies from it, so it's safe to reuse vectors here const DummyBoundingBox = new BoundingBox(TempVec1, TempVec1); /** * Component responsible of rendering the bounding box of the meshes in a scene. * This is usually used through the mesh.showBoundingBox or the scene.forceShowBoundingBoxes properties */ export class BoundingBoxRenderer { /** * Gets the shader language used in this renderer. */ get shaderLanguage() { return this._shaderLanguage; } /** * Instantiates a new bounding box renderer in a scene. * @param scene the scene the renderer renders in */ constructor(scene) { /** * The component name helpful to identify the component in the list of scene components. */ this.name = SceneComponentConstants.NAME_BOUNDINGBOXRENDERER; /** * Color of the bounding box lines placed in front of an object */ this.frontColor = new Color3(1, 1, 1); /** * Color of the bounding box lines placed behind an object */ this.backColor = new Color3(0.1, 0.1, 0.1); /** * Defines if the renderer should show the back lines or not */ this.showBackLines = true; /** * Observable raised before rendering a bounding box * When {@link BoundingBoxRenderer.useInstances} enabled, * this would only be triggered once for one rendering, instead of once every bounding box. * Events would be triggered with a dummy box to keep backwards compatibility, * the passed bounding box has no meaning and should be ignored. */ this.onBeforeBoxRenderingObservable = new Observable(); /** * Observable raised after rendering a bounding box * When {@link BoundingBoxRenderer.useInstances} enabled, * this would only be triggered once for one rendering, instead of once every bounding box. * Events would be triggered with a dummy box to keep backwards compatibility, * the passed bounding box has no meaning and should be ignored. */ this.onAfterBoxRenderingObservable = new Observable(); /** * Observable raised after resources are created */ this.onResourcesReadyObservable = new Observable(); /** * When false, no bounding boxes will be rendered */ this.enabled = true; /** Shader language used by the renderer */ this._shaderLanguage = 0 /* ShaderLanguage.GLSL */; /** * @internal */ this.renderList = new SmartArray(32); this._vertexBuffers = {}; this._fillIndexBuffer = null; this._fillIndexData = null; /** * Internal buffer for instanced rendering */ this._matrixBuffer = null; this._matrices = null; /** * Internal state of whether instanced rendering enabled */ this._useInstances = false; /** @internal */ this._drawWrapperFront = null; /** @internal */ this._drawWrapperBack = null; this.scene = scene; const engine = this.scene.getEngine(); if (engine.isWebGPU) { this._shaderLanguage = 1 /* ShaderLanguage.WGSL */; } scene._addComponent(this); this._uniformBufferFront = new UniformBuffer(this.scene.getEngine(), undefined, undefined, "BoundingBoxRendererFront", true); this._buildUniformLayout(this._uniformBufferFront); this._uniformBufferBack = new UniformBuffer(this.scene.getEngine(), undefined, undefined, "BoundingBoxRendererBack", true); this._buildUniformLayout(this._uniformBufferBack); } _buildUniformLayout(ubo) { ubo.addUniform("color", 4); ubo.addUniform("world", 16); ubo.addUniform("viewProjection", 16); ubo.addUniform("viewProjectionR", 16); ubo.create(); } /** * Registers the component in a given scene */ register() { this.scene._beforeEvaluateActiveMeshStage.registerStep(SceneComponentConstants.STEP_BEFOREEVALUATEACTIVEMESH_BOUNDINGBOXRENDERER, this, this.reset); this.scene._preActiveMeshStage.registerStep(SceneComponentConstants.STEP_PREACTIVEMESH_BOUNDINGBOXRENDERER, this, this._preActiveMesh); this.scene._evaluateSubMeshStage.registerStep(SceneComponentConstants.STEP_EVALUATESUBMESH_BOUNDINGBOXRENDERER, this, this._evaluateSubMesh); this.scene._afterRenderingGroupDrawStage.registerStep(SceneComponentConstants.STEP_AFTERRENDERINGGROUPDRAW_BOUNDINGBOXRENDERER, this, this.render); } /** * Checks if the renderer is ready asynchronously. * @param timeStep Time step in ms between retries (default is 16) * @param maxTimeout Maximum time in ms to wait for the graph to be ready (default is 30000) * @returns The promise that resolves when the renderer is ready */ async whenReadyAsync(timeStep = 16, maxTimeout = 30000) { this._prepareResources(); return await new Promise((resolve) => { _RetryWithInterval(() => { return this._colorShader.isReady(); }, () => { resolve(); }, (err, isTimeout) => { if (!isTimeout) { Logger.Error("BoundingBoxRenderer: An unexpected error occurred while waiting for the renderer to be ready."); if (err) { Logger.Error(err); if (err.stack) { Logger.Error(err.stack); } } } else { Logger.Error(`BoundingBoxRenderer: Timeout while waiting for the renderer to be ready.`); if (err) { Logger.Error(err); } } }, timeStep, maxTimeout); }); } /** @internal */ _evaluateSubMesh(mesh, subMesh) { if (mesh.showSubMeshesBoundingBox) { const boundingInfo = subMesh.getBoundingInfo(); if (boundingInfo !== null && boundingInfo !== undefined) { boundingInfo.boundingBox._tag = mesh.renderingGroupId; this.renderList.push(boundingInfo.boundingBox); } } } /** @internal */ _preActiveMesh(mesh) { if (mesh.showBoundingBox || this.scene.forceShowBoundingBoxes) { const boundingInfo = mesh.getBoundingInfo(); boundingInfo.boundingBox._tag = mesh.renderingGroupId; this.renderList.push(boundingInfo.boundingBox); } } _prepareResources() { if (this._colorShader) { return; } this._colorShader = new ShaderMaterial("colorShader", this.scene, "boundingBoxRenderer", { attributes: [VertexBuffer.PositionKind, "world0", "world1", "world2", "world3"], uniforms: ["world", "viewProjection", "viewProjectionR", "color"], uniformBuffers: ["BoundingBoxRenderer"], shaderLanguage: this._shaderLanguage, extraInitializationsAsync: async () => { if (this._shaderLanguage === 1 /* ShaderLanguage.WGSL */) { await Promise.all([import("../ShadersWGSL/boundingBoxRenderer.vertex.js"), import("../ShadersWGSL/boundingBoxRenderer.fragment.js")]); } else { await Promise.all([import("../Shaders/boundingBoxRenderer.vertex.js"), import("../Shaders/boundingBoxRenderer.fragment.js")]); } }, }, false); this._colorShader.setDefine("INSTANCES", this._useInstances); this._colorShader.doNotSerialize = true; this._colorShader.reservedDataStore = { hidden: true, }; this._colorShaderForOcclusionQuery = new ShaderMaterial("colorShaderOccQuery", this.scene, "boundingBoxRenderer", { attributes: [VertexBuffer.PositionKind], uniforms: ["world", "viewProjection", "viewProjectionR", "color"], uniformBuffers: ["BoundingBoxRenderer"], shaderLanguage: this._shaderLanguage, extraInitializationsAsync: async () => { if (this._shaderLanguage === 1 /* ShaderLanguage.WGSL */) { await Promise.all([import("../ShadersWGSL/boundingBoxRenderer.vertex.js"), import("../ShadersWGSL/boundingBoxRenderer.fragment.js")]); } else { await Promise.all([import("../Shaders/boundingBoxRenderer.vertex.js"), import("../Shaders/boundingBoxRenderer.fragment.js")]); } }, }, true); this._colorShaderForOcclusionQuery.doNotSerialize = true; this._colorShaderForOcclusionQuery.reservedDataStore = { hidden: true, }; const engine = this.scene.getEngine(); const boxdata = CreateBoxVertexData({ size: 1.0 }); this._vertexBuffers[VertexBuffer.PositionKind] = new VertexBuffer(engine, boxdata.positions, VertexBuffer.PositionKind, false); this._createIndexBuffer(); this._fillIndexData = boxdata.indices; this.onResourcesReadyObservable.notifyObservers(this); } _createIndexBuffer() { const engine = this.scene.getEngine(); this._indexBuffer = engine.createIndexBuffer([0, 1, 1, 2, 2, 3, 3, 0, 4, 5, 5, 6, 6, 7, 7, 4, 0, 7, 1, 6, 2, 5, 3, 4]); } /** * Rebuilds the elements related to this component in case of * context lost for instance. */ rebuild() { const vb = this._vertexBuffers[VertexBuffer.PositionKind]; if (vb) { vb._rebuild(); } this._createIndexBuffer(); if (this._matrixBuffer) { this._matrixBuffer._rebuild(); } } /** * @internal */ reset() { this.renderList.reset(); } /** * Render the bounding boxes of a specific rendering group * @param renderingGroupId defines the rendering group to render */ render(renderingGroupId) { if (this.renderList.length === 0 || !this.enabled) { return; } if (this._useInstances) { this._renderInstanced(renderingGroupId); return; } this._prepareResources(); if (!this._colorShader.isReady()) { return; } const engine = this.scene.getEngine(); engine.setDepthWrite(false); const transformMatrix = this.scene.getTransformMatrix(); for (let boundingBoxIndex = 0; boundingBoxIndex < this.renderList.length; boundingBoxIndex++) { const boundingBox = this.renderList.data[boundingBoxIndex]; if (boundingBox._tag !== renderingGroupId) { continue; } this._createWrappersForBoundingBox(boundingBox); this.onBeforeBoxRenderingObservable.notifyObservers(boundingBox); const min = boundingBox.minimum; const max = boundingBox.maximum; const diff = max.subtract(min); const median = min.add(diff.scale(0.5)); const worldMatrix = Matrix.Scaling(diff.x, diff.y, diff.z) .multiply(Matrix.Translation(median.x, median.y, median.z)) .multiply(boundingBox.getWorldMatrix()); const useReverseDepthBuffer = engine.useReverseDepthBuffer; if (this.showBackLines) { const drawWrapperBack = boundingBox._drawWrapperBack ?? this._colorShader._getDrawWrapper(); this._colorShader._preBind(drawWrapperBack); engine.bindBuffers(this._vertexBuffers, this._indexBuffer, this._colorShader.getEffect()); // Back if (useReverseDepthBuffer) { engine.setDepthFunctionToLessOrEqual(); } else { engine.setDepthFunctionToGreaterOrEqual(); } this._uniformBufferBack.bindToEffect(drawWrapperBack.effect, "BoundingBoxRenderer"); this._uniformBufferBack.updateColor4("color", this.backColor, 1); this._uniformBufferBack.updateMatrix("world", worldMatrix); this._uniformBufferBack.updateMatrix("viewProjection", transformMatrix); this._uniformBufferBack.update(); // Draw order engine.drawElementsType(Material.LineListDrawMode, 0, 24); } const drawWrapperFront = boundingBox._drawWrapperFront ?? this._colorShader._getDrawWrapper(); this._colorShader._preBind(drawWrapperFront); engine.bindBuffers(this._vertexBuffers, this._indexBuffer, this._colorShader.getEffect()); // Front if (useReverseDepthBuffer) { engine.setDepthFunctionToGreater(); } else { engine.setDepthFunctionToLess(); } this._uniformBufferFront.bindToEffect(drawWrapperFront.effect, "BoundingBoxRenderer"); this._uniformBufferFront.updateColor4("color", this.frontColor, 1); this._uniformBufferFront.updateMatrix("world", worldMatrix); this._uniformBufferFront.updateMatrix("viewProjection", transformMatrix); this._uniformBufferFront.update(); // Draw order engine.drawElementsType(Material.LineListDrawMode, 0, 24); this.onAfterBoxRenderingObservable.notifyObservers(boundingBox); } this._colorShader.unbind(); engine.setDepthFunctionToLessOrEqual(); engine.setDepthWrite(true); } _createWrappersForBoundingBox(boundingBox) { if (!boundingBox._drawWrapperFront) { const engine = this.scene.getEngine(); boundingBox._drawWrapperFront = new DrawWrapper(engine); boundingBox._drawWrapperBack = new DrawWrapper(engine); boundingBox._drawWrapperFront.setEffect(this._colorShader.getEffect()); boundingBox._drawWrapperBack.setEffect(this._colorShader.getEffect()); } } /** * In case of occlusion queries, we can render the occlusion bounding box through this method * @param mesh Define the mesh to render the occlusion bounding box for */ renderOcclusionBoundingBox(mesh) { const engine = this.scene.getEngine(); if (this._renderPassIdForOcclusionQuery === undefined) { this._renderPassIdForOcclusionQuery = engine.createRenderPassId(`Render pass for occlusion query`); } const currentRenderPassId = engine.currentRenderPassId; engine.currentRenderPassId = this._renderPassIdForOcclusionQuery; this._prepareResources(); const subMesh = mesh.subMeshes[0]; if (!this._colorShaderForOcclusionQuery.isReady(mesh, undefined, subMesh) || !mesh.hasBoundingInfo) { engine.currentRenderPassId = currentRenderPassId; return; } if (!this._fillIndexBuffer) { this._fillIndexBuffer = engine.createIndexBuffer(this._fillIndexData); } const useReverseDepthBuffer = engine.useReverseDepthBuffer; engine.setDepthWrite(false); engine.setColorWrite(false); const boundingBox = mesh.getBoundingInfo().boundingBox; const min = boundingBox.minimum; const max = boundingBox.maximum; const diff = max.subtract(min); const median = min.add(diff.scale(0.5)); const worldMatrix = Matrix.Scaling(diff.x, diff.y, diff.z) .multiply(Matrix.Translation(median.x, median.y, median.z)) .multiply(boundingBox.getWorldMatrix()); const drawWrapper = subMesh._drawWrapper; this._colorShaderForOcclusionQuery._preBind(drawWrapper); engine.bindBuffers(this._vertexBuffers, this._fillIndexBuffer, drawWrapper.effect); if (useReverseDepthBuffer) { engine.setDepthFunctionToGreater(); } else { engine.setDepthFunctionToLess(); } this.scene.resetCachedMaterial(); this._uniformBufferFront.bindToEffect(drawWrapper.effect, "BoundingBoxRenderer"); this._uniformBufferFront.updateMatrix("world", worldMatrix); this._uniformBufferFront.updateMatrix("viewProjection", this.scene.getTransformMatrix()); this._uniformBufferFront.update(); engine.drawElementsType(Material.TriangleFillMode, 0, 36); this._colorShaderForOcclusionQuery.unbind(); engine.setDepthFunctionToLessOrEqual(); engine.setDepthWrite(true); engine.setColorWrite(true); engine.currentRenderPassId = currentRenderPassId; } /** * Sets whether to use instanced rendering. * When not enabled, BoundingBoxRenderer renders in a loop, * calling engine.drawElementsType for each bounding box in renderList, * making every bounding box 1 or 2 draw call. * When enabled, it collects bounding boxes to render, * and render all boxes in 1 or 2 draw call. * This could make the rendering with many bounding boxes much faster than not enabled, * but could result in a difference in rendering result if * {@link BoundingBoxRenderer.showBackLines} enabled, * because drawing the black/white part of each box one after the other * can be different from drawing the black part of all boxes and then the white part. * Also, when enabled, events of {@link BoundingBoxRenderer.onBeforeBoxRenderingObservable} * and {@link BoundingBoxRenderer.onAfterBoxRenderingObservable} would only be triggered once * for one rendering, instead of once every bounding box. * Events would be triggered with a dummy box to keep backwards compatibility, * the passed bounding box has no meaning and should be ignored. * @param val whether to use instanced rendering */ set useInstances(val) { this._useInstances = val; if (this._colorShader) { this._colorShader.setDefine("INSTANCES", val); } if (!val) { this._cleanupInstances(); } } get useInstances() { return this._useInstances; } /** * Instanced render the bounding boxes of a specific rendering group * @param renderingGroupId defines the rendering group to render */ _renderInstanced(renderingGroupId) { if (this.renderList.length === 0 || !this.enabled) { return; } this._prepareResources(); if (!this._colorShader.isReady()) { return; } const colorShader = this._colorShader; let matrices = this._matrices; const expectedLength = this.renderList.length * 16; if (!matrices || matrices.length < expectedLength || matrices.length > expectedLength * 2) { matrices = new Float32Array(expectedLength); this._matrices = matrices; } this.onBeforeBoxRenderingObservable.notifyObservers(DummyBoundingBox); let instancesCount = 0; for (let boundingBoxIndex = 0; boundingBoxIndex < this.renderList.length; boundingBoxIndex++) { const boundingBox = this.renderList.data[boundingBoxIndex]; if (boundingBox._tag !== renderingGroupId) { continue; } const min = boundingBox.minimum; const max = boundingBox.maximum; const diff = max.subtractToRef(min, TempVec2); const median = min.addToRef(diff.scaleToRef(0.5, TempVec1), TempVec1); const m = TempMatrixArray; // Directly update the matrix values in column-major order m[0] = diff._x; // Scale X m[3] = median._x; // Translate X m[5] = diff._y; // Scale Y m[7] = median._y; // Translate Y m[10] = diff._z; // Scale Z m[11] = median._z; // Translate Z TempMatrix.multiplyToArray(boundingBox.getWorldMatrix(), matrices, instancesCount * 16); instancesCount++; } const engine = this.scene.getEngine(); // keeps the original depth function and depth write const depthFunction = engine.getDepthFunction() ?? 515; const depthWrite = engine.getDepthWrite(); engine.setDepthWrite(false); const matrixBuffer = this._matrixBuffer; if (matrixBuffer?.isUpdatable() && matrixBuffer.getData() === matrices) { matrixBuffer.update(matrices); } else { this._createInstanceBuffer(matrices); } this._createWrappersForBoundingBox(this); const useReverseDepthBuffer = engine.useReverseDepthBuffer; const transformMatrix = this.scene.getTransformMatrix(); if (this.showBackLines) { const drawWrapperBack = this._drawWrapperBack ?? colorShader._getDrawWrapper(); colorShader._preBind(drawWrapperBack); engine.bindBuffers(this._vertexBuffers, this._indexBuffer, colorShader.getEffect()); // Back if (useReverseDepthBuffer) { engine.setDepthFunctionToLessOrEqual(); } else { engine.setDepthFunctionToGreaterOrEqual(); } const _uniformBufferBack = this._uniformBufferBack; _uniformBufferBack.bindToEffect(drawWrapperBack.effect, "BoundingBoxRenderer"); _uniformBufferBack.updateColor4("color", this.backColor, 1); _uniformBufferBack.updateMatrix("viewProjection", transformMatrix); _uniformBufferBack.update(); // Draw order engine.drawElementsType(Material.LineListDrawMode, 0, 24, instancesCount); } const drawWrapperFront = colorShader._getDrawWrapper(); colorShader._preBind(drawWrapperFront); engine.bindBuffers(this._vertexBuffers, this._indexBuffer, colorShader.getEffect()); // Front if (useReverseDepthBuffer) { engine.setDepthFunctionToGreater(); } else { engine.setDepthFunctionToLess(); } const _uniformBufferFront = this._uniformBufferFront; _uniformBufferFront.bindToEffect(drawWrapperFront.effect, "BoundingBoxRenderer"); _uniformBufferFront.updateColor4("color", this.frontColor, 1); _uniformBufferFront.updateMatrix("viewProjection", transformMatrix); _uniformBufferFront.update(); // Draw order engine.drawElementsType(Material.LineListDrawMode, 0, 24, instancesCount); this.onAfterBoxRenderingObservable.notifyObservers(DummyBoundingBox); colorShader.unbind(); engine.setDepthFunction(depthFunction); engine.setDepthWrite(depthWrite); } /** * Creates buffer for instanced rendering * @param buffer buffer to set */ _createInstanceBuffer(buffer) { const vertexBuffers = this._vertexBuffers; this._cleanupInstanceBuffer(); const matrixBuffer = new Buffer(this.scene.getEngine(), buffer, true, 16, false, true); vertexBuffers.world0 = matrixBuffer.createVertexBuffer("world0", 0, 4); vertexBuffers.world1 = matrixBuffer.createVertexBuffer("world1", 4, 4); vertexBuffers.world2 = matrixBuffer.createVertexBuffer("world2", 8, 4); vertexBuffers.world3 = matrixBuffer.createVertexBuffer("world3", 12, 4); this._matrixBuffer = matrixBuffer; } /** * Clean up buffers for instanced rendering */ _cleanupInstanceBuffer() { const vertexBuffers = this._vertexBuffers; if (vertexBuffers.world0) { vertexBuffers.world0.dispose(); delete vertexBuffers.world0; } if (vertexBuffers.world1) { vertexBuffers.world1.dispose(); delete vertexBuffers.world1; } if (vertexBuffers.world2) { vertexBuffers.world2.dispose(); delete vertexBuffers.world2; } if (vertexBuffers.world3) { vertexBuffers.world3.dispose(); delete vertexBuffers.world3; } this._matrices = null; if (this._matrixBuffer) { this._matrixBuffer.dispose(); this._matrixBuffer = null; } } /** * Clean up resources for instanced rendering */ _cleanupInstances() { this._cleanupInstanceBuffer(); if (this._drawWrapperFront) { this._drawWrapperFront.dispose(); this._drawWrapperFront = null; } if (this._drawWrapperBack) { this._drawWrapperBack.dispose(); this._drawWrapperBack = null; } } /** * Dispose and release the resources attached to this renderer. */ dispose() { if (this._renderPassIdForOcclusionQuery !== undefined) { this.scene.getEngine().releaseRenderPassId(this._renderPassIdForOcclusionQuery); this._renderPassIdForOcclusionQuery = undefined; } if (!this._colorShader) { return; } this.onBeforeBoxRenderingObservable.clear(); this.onAfterBoxRenderingObservable.clear(); this.onResourcesReadyObservable.clear(); this.renderList.dispose(); this._colorShader.dispose(); this._colorShaderForOcclusionQuery.dispose(); this._uniformBufferFront.dispose(); this._uniformBufferBack.dispose(); const buffer = this._vertexBuffers[VertexBuffer.PositionKind]; if (buffer) { buffer.dispose(); this._vertexBuffers[VertexBuffer.PositionKind] = null; } this.scene.getEngine()._releaseBuffer(this._indexBuffer); if (this._fillIndexBuffer) { this.scene.getEngine()._releaseBuffer(this._fillIndexBuffer); this._fillIndexBuffer = null; } this._cleanupInstances(); } } //# sourceMappingURL=boundingBoxRenderer.js.map