@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.
626 lines (625 loc) • 27.7 kB
JavaScript
import { VertexBuffer } from "../Buffers/buffer.js";
import { Camera } from "../Cameras/camera.js";
import { AddClipPlaneUniforms, BindClipPlane, PrepareStringDefinesForClipPlanes } from "../Materials/clipPlaneMaterialHelper.js";
import { EffectFallbacks } from "../Materials/effectFallbacks.js";
import { Material } from "../Materials/material.js";
import { BindBonesParameters, BindMorphTargetParameters, PrepareDefinesAndAttributesForMorphTargets, PushAttributesForInstances } from "../Materials/materialHelper.functions.js";
import { Color3, Color4 } from "../Maths/math.color.js";
import { ThinEffectLayer } from "./thinEffectLayer.js";
/**
* @internal
*/
export class ThinSelectionOutlineLayer extends ThinEffectLayer {
/**
* Instantiates a new selection outline Layer and references it to the scene..
* @param name The name of the layer
* @param scene The scene to use the layer in
* @param options Sets of none mandatory options to use with the layer (see IThinSelectionOutlineLayerOptions for more information)
* @param dontCheckIfReady Specifies if the layer should disable checking whether all the post processes are ready (default: false). To save performance, this should be set to true and you should call `isReady` manually before rendering to the layer.
*/
constructor(name, scene, options, dontCheckIfReady = false) {
super(name, scene, options !== undefined ? !!options.forceGLSL : false);
/**
* The outline color
*/
this.outlineColor = new Color3(1, 0.5, 0);
/**
* The thickness of the edges
*/
this.outlineThickness = 2.0;
/**
* The strength of the occlusion effect (default: 0.8)
*/
this.occlusionStrength = 0.8;
/**
* The occlusion threshold (default: 0.0001)
*/
this.occlusionThreshold = 0.0001;
/**
* The width of the source texture
*/
this.textureWidth = 0;
/**
* The height of the source texture
*/
this.textureHeight = 0;
/** @internal */
this._meshUniqueIdToSelectionId = [];
/** @internal */
this._selection = [];
this._nextSelectionId = 1;
// Adapt options
this._options = {
mainTextureRatio: 1.0,
mainTextureFixedSize: 0,
alphaBlendingMode: 2,
camera: null,
renderingGroupId: -1,
forceGLSL: false,
mainTextureType: 1,
mainTextureFormat: 7,
storeCameraSpaceZ: false,
outlineMethod: 0,
...options,
};
// Fall back to a supported mask texture type if the device doesn't support rendering to float framebuffers
// or linear filtering of float textures (e.g. OES_texture_float_linear missing on some iOS versions)
if (this._options.mainTextureType === 1 && !(this._engine.getCaps().textureFloatRender && this._engine.getCaps().textureFloatLinearFiltering)) {
this._options.mainTextureType = 2;
}
if (this._options.mainTextureType === 2 &&
!(this._engine.getCaps().textureHalfFloatRender && this._engine.getCaps().textureHalfFloatLinearFiltering)) {
this._options.mainTextureType = 0;
}
// When using an 8-bit render target, we cannot reliably store camera-space Z in the mask texture:
// depth would be clamped/quantized, breaking occlusion comparisons. In that case, force-disable
// storeCameraSpaceZ so the layer falls back to the supported behavior.
if (this._options.storeCameraSpaceZ && this._options.mainTextureType === 0) {
this._options.storeCameraSpaceZ = false;
}
// set clear color
this.neutralColor = new Color4(0.0, this._options.storeCameraSpaceZ ? 0.0 : 1.0, 0.0, 1.0);
// Initialize the layer
this._init(this._options);
// Do not render as long as no meshes have been added
this._shouldRender = false;
if (dontCheckIfReady) {
// When dontCheckIfReady is true, we are in the new ThinXXX layer mode, so we must call _createTextureAndPostProcesses ourselves (it is called by EffectLayer otherwise)
this._createTextureAndPostProcesses();
}
}
/**
* Gets the class name of the effect layer
* @returns the string with the class name of the effect layer
*/
getClassName() {
return "SelectionOutlineLayer";
}
/** @internal */
_internalIsSubMeshReady(subMesh, useInstances, _emissiveTexture) {
const engine = this._scene.getEngine();
const mesh = subMesh.getMesh();
const renderingMaterial = mesh._internalAbstractMeshDataInfo._materialForRenderPass?.[engine.currentRenderPassId];
if (renderingMaterial) {
return renderingMaterial.isReadyForSubMesh(mesh, subMesh, useInstances);
}
const material = subMesh.getMaterial();
if (!material) {
return false;
}
// selection outline layer is not compatible with custom materials
// if (this._useMeshMaterial(subMesh.getRenderingMesh())) {
// return material.isReadyForSubMesh(subMesh.getMesh(), subMesh, useInstances);
// }
const defines = [];
const attribs = [VertexBuffer.PositionKind];
let uv1 = false;
let uv2 = false;
const color = false;
// Alpha test
if (material.needAlphaTestingForMesh(mesh)) {
defines.push("#define ALPHATEST");
if (mesh.isVerticesDataPresent(VertexBuffer.UVKind)) {
attribs.push(VertexBuffer.UVKind);
defines.push("#define UV1");
uv1 = true;
}
if (mesh.isVerticesDataPresent(VertexBuffer.UV2Kind)) {
attribs.push(VertexBuffer.UV2Kind);
defines.push("#define UV2");
uv2 = true;
}
}
// Bones
const fallbacks = new EffectFallbacks();
if (mesh.useBones && mesh.computeBonesUsingShaders) {
attribs.push(VertexBuffer.MatricesIndicesKind);
attribs.push(VertexBuffer.MatricesWeightsKind);
if (mesh.numBoneInfluencers > 4) {
attribs.push(VertexBuffer.MatricesIndicesExtraKind);
attribs.push(VertexBuffer.MatricesWeightsExtraKind);
}
defines.push("#define NUM_BONE_INFLUENCERS " + mesh.numBoneInfluencers);
const skeleton = mesh.skeleton;
if (skeleton && skeleton.isUsingTextureForMatrices) {
defines.push("#define BONETEXTURE");
}
else {
defines.push("#define BonesPerMesh " + (skeleton ? skeleton.bones.length + 1 : 0));
}
if (mesh.numBoneInfluencers > 0) {
fallbacks.addCPUSkinningFallback(0, mesh);
}
}
else {
defines.push("#define NUM_BONE_INFLUENCERS 0");
}
// Morph targets
const numMorphInfluencers = mesh.morphTargetManager
? PrepareDefinesAndAttributesForMorphTargets(mesh.morphTargetManager, defines, attribs, mesh, true, // usePositionMorph
false, // useNormalMorph
false, // useTangentMorph
uv1, // useUVMorph
uv2, // useUV2Morph
color // useColorMorph
)
: 0;
// Instances
if (useInstances) {
defines.push("#define INSTANCES");
PushAttributesForInstances(attribs);
if (subMesh.getRenderingMesh().hasThinInstances) {
defines.push("#define THIN_INSTANCES");
}
}
// Baked vertex animations
const bvaManager = mesh.bakedVertexAnimationManager;
if (bvaManager && bvaManager.isEnabled) {
defines.push("#define BAKED_VERTEX_ANIMATION_TEXTURE");
if (useInstances) {
attribs.push("bakedVertexAnimationSettingsInstanced");
}
}
// ClipPlanes
PrepareStringDefinesForClipPlanes(material, this._scene, defines);
// Selection ID
if (useInstances) {
attribs.push(ThinSelectionOutlineLayer.InstanceSelectionIdAttributeName);
}
this._addCustomEffectDefines(defines);
// Get correct effect
const drawWrapper = subMesh._getDrawWrapper(undefined, true);
const cachedDefines = drawWrapper.defines;
const join = defines.join("\n");
if (cachedDefines !== join) {
const uniforms = [
"world",
"mBones",
"viewProjection",
"view",
"morphTargetInfluences",
"morphTargetCount",
"boneTextureInfo",
"diffuseMatrix",
"morphTargetTextureInfo",
"morphTargetTextureIndices",
"bakedVertexAnimationSettings",
"bakedVertexAnimationTextureSizeInverted",
"bakedVertexAnimationTime",
"bakedVertexAnimationTexture",
"depthValues",
"selectionId",
];
AddClipPlaneUniforms(uniforms);
drawWrapper.setEffect(this._engine.createEffect("selection", {
attributes: attribs,
uniformsNames: uniforms,
uniformBuffersNames: [],
samplers: ["diffuseSampler", "boneSampler", "morphTargets", "bakedVertexAnimationTexture"],
defines: join,
fallbacks: fallbacks,
onCompiled: null,
onError: null,
indexParameters: { maxSimultaneousMorphTargets: numMorphInfluencers },
shaderLanguage: this._shaderLanguage,
extraInitializationsAsync: this._shadersLoaded
? undefined
: async () => {
await this._importShadersAsync();
this._shadersLoaded = true;
},
}, this._engine), join);
}
const effectIsReady = drawWrapper.effect.isReady();
return effectIsReady && (this._dontCheckIfReady || (!this._dontCheckIfReady && this.isLayerReady()));
}
async _importShadersAsync() {
if (this._shaderLanguage === 1 /* ShaderLanguage.WGSL */) {
await Promise.all([
import("../ShadersWGSL/selection.vertex.js"),
import("../ShadersWGSL/selection.fragment.js"),
import("../ShadersWGSL/glowMapMerge.vertex.js"),
import("../ShadersWGSL/selectionOutline.fragment.js"),
]);
}
else {
await Promise.all([
import("../Shaders/selection.vertex.js"),
import("../Shaders/selection.fragment.js"),
import("../Shaders/glowMapMerge.vertex.js"),
import("../Shaders/selectionOutline.fragment.js"),
]);
}
await super._importShadersAsync();
}
/**
* Get the effect name of the layer.
* @returns The effect name
*/
getEffectName() {
return ThinSelectionOutlineLayer.EffectName;
}
/** @internal */
_createMergeEffect() {
const defines = [];
switch (this._options.outlineMethod) {
case 0:
defines.push("#define OUTLINELAYER_SAMPLING_TRIDIRECTIONAL");
break;
case 1:
defines.push("#define OUTLINELAYER_SAMPLING_OCTADIRECTIONAL");
break;
}
const join = defines.join("\n");
return this._engine.createEffect({
// glowMapMerge vertex is just a basic vertex shader for drawing a quad. so we reuse it here
vertex: "glowMapMerge",
// selection outline fragment does computation of outline with alpha channel for blending
fragment: "selectionOutline",
}, {
attributes: [VertexBuffer.PositionKind],
uniformsNames: ["screenSize", "outlineColor", "outlineThickness", "occlusionStrength", "occlusionThreshold"],
samplers: ["maskSampler", "depthSampler"],
defines: join,
fallbacks: null,
onCompiled: null,
onError: null,
shaderLanguage: this._shaderLanguage,
extraInitializationsAsync: this._shadersLoaded
? undefined
: async () => {
await this._importShadersAsync();
this._shadersLoaded = true;
},
}, this._engine);
}
/** @internal */
_createTextureAndPostProcesses() {
// we don't need to create a texture for this layer. since all computation is done in the merge effect
}
/**
* Checks for the readiness of the element composing the layer.
* @param subMesh the mesh to check for
* @param useInstances specify whether or not to use instances to render the mesh
* @returns true if ready otherwise, false
*/
isReady(subMesh, useInstances) {
const material = subMesh.getMaterial();
const mesh = subMesh.getRenderingMesh();
if (!material || !mesh || !this._selection) {
return false;
}
return super._isSubMeshReady(subMesh, useInstances, null);
}
/** @internal */
_canRenderMesh(_mesh, _material) {
return true;
}
_renderSubMesh(subMesh, enableAlphaMode = false) {
if (!this._internalShouldRender()) {
return;
}
const material = subMesh.getMaterial();
const ownerMesh = subMesh.getMesh();
const replacementMesh = subMesh.getReplacementMesh();
const renderingMesh = subMesh.getRenderingMesh();
const effectiveMesh = subMesh.getEffectiveMesh();
const scene = this._scene;
const engine = scene.getEngine();
effectiveMesh._internalAbstractMeshDataInfo._isActiveIntermediate = false;
if (!material) {
return;
}
// Do not block in blend mode.
if (!this._canRenderMesh(renderingMesh, material)) {
return;
}
// Culling
let sideOrientation = material._getEffectiveOrientation(renderingMesh);
const mainDeterminant = effectiveMesh._getWorldMatrixDeterminant();
if (mainDeterminant < 0) {
sideOrientation = sideOrientation === Material.ClockWiseSideOrientation ? Material.CounterClockWiseSideOrientation : Material.ClockWiseSideOrientation;
}
const reverse = sideOrientation === Material.ClockWiseSideOrientation;
engine.setState(material.backFaceCulling, material.zOffset, undefined, reverse, material.cullBackFaces, undefined, material.zOffsetUnits);
// Managing instances
const batch = renderingMesh._getInstancesRenderList(subMesh._id, !!replacementMesh);
if (batch.mustReturn) {
return;
}
// Early Exit per mesh
if (!this._shouldRenderMesh(renderingMesh)) {
return;
}
const hardwareInstancedRendering = batch.hardwareInstancedRendering[subMesh._id] || renderingMesh.hasThinInstances || !!renderingMesh._userInstancedBuffersStorage;
this._setEmissiveTextureAndColor(renderingMesh, subMesh, material);
this.onBeforeRenderMeshToEffect.notifyObservers(ownerMesh);
// selection outline layer is not compatible with custom materials
// if (this._useMeshMaterial(renderingMesh)) {
// subMesh.getMaterial()!._glowModeEnabled = true;
// renderingMesh.render(subMesh, enableAlphaMode, replacementMesh || undefined);
// subMesh.getMaterial()!._glowModeEnabled = false;
// } else
if (this._isSubMeshReady(subMesh, hardwareInstancedRendering, this._emissiveTextureAndColor.texture)) {
const renderingMaterial = effectiveMesh._internalAbstractMeshDataInfo._materialForRenderPass?.[engine.currentRenderPassId];
let drawWrapper = subMesh._getDrawWrapper();
if (!drawWrapper && renderingMaterial) {
drawWrapper = renderingMaterial._getDrawWrapper();
}
if (!drawWrapper) {
return;
}
const effect = drawWrapper.effect;
engine.enableEffect(drawWrapper);
if (!hardwareInstancedRendering) {
renderingMesh._bind(subMesh, effect, material.fillMode);
}
if (!renderingMaterial) {
effect.setMatrix("viewProjection", scene.getTransformMatrix());
if (this._options.storeCameraSpaceZ) {
effect.setMatrix("view", scene.getViewMatrix());
}
else {
const camera = this.camera || scene.activeCamera;
if (camera) {
const cameraIsOrtho = camera.mode === Camera.ORTHOGRAPHIC_CAMERA;
let minZ, maxZ;
if (cameraIsOrtho) {
minZ = !engine.useReverseDepthBuffer && engine.isNDCHalfZRange ? 0 : 1;
maxZ = engine.useReverseDepthBuffer && engine.isNDCHalfZRange ? 0 : 1;
}
else {
minZ = engine.useReverseDepthBuffer && engine.isNDCHalfZRange ? camera.minZ : engine.isNDCHalfZRange ? 0 : camera.minZ;
maxZ = engine.useReverseDepthBuffer && engine.isNDCHalfZRange ? 0 : camera.maxZ;
}
effect.setFloat2("depthValues", minZ, minZ + maxZ);
}
}
effect.setMatrix("world", effectiveMesh.getWorldMatrix());
}
else {
renderingMaterial.bindForSubMesh(effectiveMesh.getWorldMatrix(), effectiveMesh, subMesh);
}
if (!renderingMaterial) {
// Alpha test
if (material && material.needAlphaTestingForMesh(effectiveMesh)) {
const alphaTexture = material.getAlphaTestTexture();
if (alphaTexture) {
effect.setTexture("diffuseSampler", alphaTexture);
effect.setMatrix("diffuseMatrix", alphaTexture.getTextureMatrix());
}
}
// Bones
BindBonesParameters(renderingMesh, effect);
// Morph targets
BindMorphTargetParameters(renderingMesh, effect);
if (renderingMesh.morphTargetManager && renderingMesh.morphTargetManager.isUsingTextureForTargets) {
renderingMesh.morphTargetManager._bind(effect);
}
// Baked vertex animations
const bvaManager = subMesh.getMesh().bakedVertexAnimationManager;
if (bvaManager && bvaManager.isEnabled) {
bvaManager.bind(effect, hardwareInstancedRendering);
}
// Alpha mode
if (enableAlphaMode) {
engine.setAlphaMode(material.alphaMode);
}
// Clip planes
BindClipPlane(effect, material, scene);
// Selection ID
const selectionId = this._meshUniqueIdToSelectionId[renderingMesh.uniqueId];
if (!renderingMesh.hasInstances && !renderingMesh.hasThinInstances && !renderingMesh.isAnInstance && selectionId !== undefined) {
effect.setFloat("selectionId", selectionId);
}
}
// Draw
renderingMesh._processRendering(effectiveMesh, subMesh, effect, material.fillMode, batch, hardwareInstancedRendering, (isInstance, world) => effect.setMatrix("world", world));
}
else {
// Need to reset refresh rate of the main map
this._objectRenderer.resetRefreshCounter();
}
this.onAfterRenderMeshToEffect.notifyObservers(ownerMesh);
}
/** @internal */
_internalCompose(effect, _renderIndex) {
// Texture
this.bindTexturesForCompose(effect);
effect.setFloat2("screenSize", this.textureWidth, this.textureHeight);
effect.setColor3("outlineColor", this.outlineColor);
effect.setFloat("outlineThickness", this.outlineThickness);
effect.setFloat("occlusionStrength", this.occlusionStrength);
effect.setFloat("occlusionThreshold", this.occlusionThreshold);
// Cache
const engine = this._engine;
const previousStencilBuffer = engine.getStencilBuffer();
// Draw order
engine.setStencilBuffer(false);
engine.drawElementsType(Material.TriangleFillMode, 0, 6);
// Draw order
engine.setStencilBuffer(previousStencilBuffer);
}
/** @internal */
_setEmissiveTextureAndColor(_mesh, _subMesh, _material) {
// we don't use emissive texture or color for this layer
}
/**
* Returns true if the layer contains information to display, otherwise false.
* @returns true if the glow layer should be rendered
*/
shouldRender() {
return this._selection && super.shouldRender() ? true : false;
}
/** @internal */
_shouldRenderMesh(mesh) {
return this.hasMesh(mesh);
}
/** @internal */
_addCustomEffectDefines(defines) {
if (this._options.storeCameraSpaceZ) {
defines.push("#define STORE_CAMERASPACE_Z");
}
}
/**
* Determine if a given mesh will be used in the current effect.
* @param mesh mesh to test
* @returns true if the mesh will be used
*/
hasMesh(mesh) {
// we control selection as RTT render list
return super.hasMesh(mesh);
}
/** @internal */
_useMeshMaterial(_mesh) {
return false;
}
/**
* Remove all the meshes currently referenced in the selection outline layer
*/
clearSelection() {
if (!this._selection) {
return;
}
for (let index = 0; index < this._selection.length; ++index) {
const mesh = this._selection[index];
if (mesh._userInstancedBuffersStorage) {
const kind = ThinSelectionOutlineLayer.InstanceSelectionIdAttributeName;
// Dispose per-pass VBOs for this layer's own render passes only (WebGPU)
if (mesh._userInstancedBuffersStorage.renderPasses) {
for (const passId of this._objectRenderer.renderPassIds) {
const passVBOs = mesh._userInstancedBuffersStorage.renderPasses[passId];
if (passVBOs?.[kind]) {
passVBOs[kind].dispose();
delete passVBOs[kind];
}
}
}
mesh._userInstancedBuffersStorage.vertexBuffers[kind]?.dispose();
const vao = mesh._userInstancedBuffersStorage.vertexArrayObjects?.[kind];
if (vao) {
// invalidate VAO is very important to keep sync between VAO and vertex buffers
this._engine.releaseVertexArrayObject(vao);
delete mesh._userInstancedBuffersStorage.vertexArrayObjects[kind];
}
delete mesh._userInstancedBuffersStorage.data[kind];
delete mesh._userInstancedBuffersStorage.vertexBuffers[kind];
delete mesh._userInstancedBuffersStorage.strides[kind];
delete mesh._userInstancedBuffersStorage.sizes[kind];
if (Object.keys(mesh._userInstancedBuffersStorage.vertexBuffers).length === 0) {
mesh._userInstancedBuffersStorage = undefined;
}
}
if (mesh.instancedBuffers?.[ThinSelectionOutlineLayer.InstanceSelectionIdAttributeName] !== undefined) {
delete mesh.instancedBuffers[ThinSelectionOutlineLayer.InstanceSelectionIdAttributeName];
}
}
this._selection.length = 0;
this._meshUniqueIdToSelectionId.length = 0;
this._nextSelectionId = 1;
this._shouldRender = false;
}
/**
* Adds mesh or group of mesh to the current selection
*
* If a group of meshes is provided, they will outline as a single unit
* @param meshOrGroup Meshes to add to the selection
*/
addSelection(meshOrGroup) {
if (!this._selection) {
return;
}
const nextId = this._nextSelectionId;
const group = Array.isArray(meshOrGroup) ? meshOrGroup : [meshOrGroup];
if (group.length === 0) {
return;
}
for (let meshIndex = 0; meshIndex < group.length; ++meshIndex) {
const mesh = group[meshIndex];
this._selection.push(mesh); // add to render list
if (mesh.hasInstances || mesh.isAnInstance) {
const sourceMesh = mesh.sourceMesh ?? mesh;
if (sourceMesh.instancedBuffers?.[ThinSelectionOutlineLayer.InstanceSelectionIdAttributeName] === undefined) {
sourceMesh.registerInstancedBuffer(ThinSelectionOutlineLayer.InstanceSelectionIdAttributeName, 1);
}
mesh.instancedBuffers[ThinSelectionOutlineLayer.InstanceSelectionIdAttributeName] = nextId;
}
else if (mesh.hasThinInstances) {
const thinInstanceCount = mesh.thinInstanceCount;
const selectionIdData = new Float32Array(thinInstanceCount);
for (let i = 0; i < thinInstanceCount; i++) {
selectionIdData[i] = nextId;
}
mesh.thinInstanceSetBuffer(ThinSelectionOutlineLayer.InstanceSelectionIdAttributeName, selectionIdData, 1);
}
else {
this._meshUniqueIdToSelectionId[mesh.uniqueId] = nextId;
}
}
this._nextSelectionId += 1;
this._shouldRender = true;
}
/**
* Free any resources and references associated to a mesh.
* Internal use
* @param mesh The mesh to free.
* @internal
*/
_disposeMesh(mesh) {
const selection = this._selection;
if (!selection) {
return;
}
const index = selection.indexOf(mesh);
if (index !== -1) {
selection.splice(index, 1);
if (mesh.hasInstances) {
mesh.removeVerticesData(ThinSelectionOutlineLayer.InstanceSelectionIdAttributeName);
}
else if (mesh.hasThinInstances) {
mesh.thinInstanceSetBuffer(ThinSelectionOutlineLayer.InstanceSelectionIdAttributeName, null);
}
if (selection.length === 0) {
this._shouldRender = false;
}
}
}
/**
* Dispose the effect layer and free resources.
*/
dispose() {
this.clearSelection();
this._selection = null;
super.dispose();
}
}
/**
* Effect Name of the layer.
*/
ThinSelectionOutlineLayer.EffectName = "SelectionOutlineLayer";
/**
* Name of the instance selection ID attribute
* @internal
*/
ThinSelectionOutlineLayer.InstanceSelectionIdAttributeName = "instanceSelectionId";
//# sourceMappingURL=thinSelectionOutlineLayer.js.map