From bb6f65c8a4d245e4c464e11bf250df26231071ef Mon Sep 17 00:00:00 2001 From: noname0310 Date: Mon, 22 Dec 2025 15:14:31 +0900 Subject: [PATCH 01/17] selection outline initial implementation --- .../selectionOutlineRenderingPipeline.ts | 580 ++++++++++++++++++ .../core/src/Shaders/selection.fragment.fx | 16 + .../dev/core/src/Shaders/selection.vertex.fx | 43 ++ .../src/Shaders/selectionOutline.fragment.fx | 58 ++ 4 files changed, 697 insertions(+) create mode 100644 packages/dev/core/src/PostProcesses/RenderPipeline/Pipelines/selectionOutlineRenderingPipeline.ts create mode 100644 packages/dev/core/src/Shaders/selection.fragment.fx create mode 100644 packages/dev/core/src/Shaders/selection.vertex.fx create mode 100644 packages/dev/core/src/Shaders/selectionOutline.fragment.fx diff --git a/packages/dev/core/src/PostProcesses/RenderPipeline/Pipelines/selectionOutlineRenderingPipeline.ts b/packages/dev/core/src/PostProcesses/RenderPipeline/Pipelines/selectionOutlineRenderingPipeline.ts new file mode 100644 index 00000000000..368298f1ef3 --- /dev/null +++ b/packages/dev/core/src/PostProcesses/RenderPipeline/Pipelines/selectionOutlineRenderingPipeline.ts @@ -0,0 +1,580 @@ +import { Camera } from "../../../Cameras/camera"; +import type { AbstractEngine } from "../../../Engines/abstractEngine"; +import { Constants } from "../../../Engines/constants"; +import { Engine } from "../../../Engines/engine"; +import type { Effect } from "../../../Materials/effect"; +import type { EffectWrapperCreationOptions } from "../../../Materials/effectRenderer"; +import { EffectWrapper } from "../../../Materials/effectRenderer"; +import { ShaderLanguage } from "../../../Materials/shaderLanguage"; +import type { IShaderMaterialOptions } from "../../../Materials/shaderMaterial"; +import { ShaderMaterial } from "../../../Materials/shaderMaterial"; +import { RenderTargetTexture } from "../../../Materials/Textures/renderTargetTexture"; +import { Color3, Color4 } from "../../../Maths/math.color"; +import type { Matrix } from "../../../Maths/math.vector"; +import type { AbstractMesh } from "../../../Meshes/abstractMesh"; +import { VertexBuffer } from "../../../Meshes/buffer"; +import type { InstancedMesh } from "../../../Meshes/instancedMesh"; +import type { Mesh } from "../../../Meshes/mesh"; +import type { SubMesh } from "../../../Meshes/subMesh"; +import { serialize } from "../../../Misc/decorators"; +import { Logger } from "../../../Misc/logger"; +import type { Observer } from "../../../Misc/observable"; +import type { DepthRenderer } from "../../../Rendering/depthRenderer"; +import "../../../Rendering/depthRendererSceneComponent"; +import type { Scene } from "../../../scene"; +import type { Nullable } from "../../../types"; +import type { PostProcessOptions } from "../../postProcess"; +import { PostProcess } from "../../postProcess"; + +class SelectionMaterial extends ShaderMaterial { + private readonly _meshUniqueIdToSelectionId: number[]; + + public constructor(name: string, scene: Scene, shaderLanguage: ShaderLanguage, meshUniqueIdToSelectionId: number[]) { + const defines: string[] = []; + const options: Partial = { + attributes: [VertexBuffer.PositionKind, SelectionOutlineRenderingPipeline.InstanceSelectionIdAttributeName], + uniforms: ["world", "viewProjection", "selectionId", "depthValues"], + needAlphaBlending: false, + defines: defines, + useClipPlane: null, + shaderLanguage: shaderLanguage, + extraInitializationsAsync: async () => { + if (this.shaderLanguage === ShaderLanguage.WGSL) { + // await Promise.all([import("../ShadersWGSL/selection.fragment"), import("../ShadersWGSL/selection.vertex")]); + } else { + await Promise.all([import("../../../Shaders/selection.fragment"), import("../Shaders/selection.vertex")]); + } + }, + }; + super(name, scene, "selection", options, false); + + this._meshUniqueIdToSelectionId = meshUniqueIdToSelectionId; + } + + public override bind(world: Matrix, mesh?: AbstractMesh, effectOverride?: Nullable, subMesh?: SubMesh): void { + super.bind(world, mesh, effectOverride, subMesh); + if (!mesh) { + return; + } + + const storeEffectOnSubMeshes = subMesh && this._storeEffectOnSubMeshes; + const effect = effectOverride ?? (storeEffectOnSubMeshes ? subMesh.effect : this.getEffect()); + + if (!effect) { + return; + } + + if (!mesh.hasInstances && !mesh.isAnInstance && this._meshUniqueIdToSelectionId[mesh.uniqueId] !== undefined) { + const selectionId = this._meshUniqueIdToSelectionId[mesh.uniqueId]; + effect.setFloat("selectionId", selectionId); + } + + const engine = this.getScene().getEngine(); + + const camera = this.getScene().activeCamera; + let minZ: number = 1; + let maxZ: number = 10000; + if (camera) { + const cameraIsOrtho = camera.mode === Camera.ORTHOGRAPHIC_CAMERA; + + 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); + } +} + +/** + * Options used to create a ThinSelectionOutlinePostprocess + */ +export interface IThinSelectionOutlinePostProcessOptions extends EffectWrapperCreationOptions {} + +class ThinSelectionOutlinePostprocess extends EffectWrapper { + /** + * The fragment shader url + */ + public static readonly FragmentUrl = "selectionOutline"; + + /** + * The list of uniforms used by the effect + */ + public static readonly Uniforms = ["screenSize", "outlineColor", "outlineThickness", "occlusionStrength"]; + + /** + * The list of samplers used by the effect + */ + public static readonly Samplers = ["maskSampler", "depthSampler"]; + + protected override _gatherImports(useWebGPU: boolean, list: Promise[]): void { + if (useWebGPU) { + this._webGPUReady = true; + // list.push(import("../ShadersWGSL/selectionOutline.fragment")); + } else { + list.push(import("../Shaders/selectionOutline.fragment")); + } + } + + /** + * Constructs a new thin selection outline post process + * @param name Name of the effect + * @param engine Engine to use to render the effect. If not provided, the last created engine will be used + * @param options Options to configure the effect + */ + public constructor(name: string, engine: Nullable = null, options?: IThinSelectionOutlinePostProcessOptions) { + super({ + ...options, + name, + engine: engine || Engine.LastCreatedEngine!, + useShaderStore: true, + useAsPostProcess: true, + fragmentShader: ThinSelectionOutlinePostprocess.FragmentUrl, + uniforms: ThinSelectionOutlinePostprocess.Uniforms, + samplers: ThinSelectionOutlinePostprocess.Samplers, + }); + } + + /** + * The camera to use to calculate the outline + */ + public camera: Nullable = null; + + /** + * THe outline color + */ + public outlineColor: Color3 = new Color3(1, 0.5, 0); + + /** + * The thickness of the edges + */ + public outlineThickness: number = 2.0; + + /** + * The strength of the occlusion effect (default: 0.8) + */ + public occlusionStrength: number = 0.8; + + /** + * The width of the source texture + */ + public textureWidth: number = 0; + + /** + * The height of the source texture + */ + public textureHeight: number = 0; + + public override bind(noDefaultBindings = false): void { + super.bind(noDefaultBindings); + + const effect = this._drawWrapper.effect!; + + effect.setFloat2("screenSize", this.textureWidth, this.textureHeight); + effect.setColor3("outlineColor", this.outlineColor); + effect.setFloat("outlineThickness", this.outlineThickness); + effect.setFloat("occlusionStrength", this.occlusionStrength); + } +} + +interface ISelectionOutlinePostprocessOptions extends IThinSelectionOutlinePostProcessOptions, PostProcessOptions {} + +class SelectionOutlinePostprocess extends PostProcess { + @serialize() + public get outlineColor(): Color3 { + return this._effectWrapper.outlineColor; + } + public set outlineColor(value: Color3) { + this._effectWrapper.outlineColor = value; + } + + @serialize() + public get outlineThickness(): number { + return this._effectWrapper.outlineThickness; + } + public set outlineThickness(value: number) { + this._effectWrapper.outlineThickness = value; + } + + @serialize() + public get occlusionStrength(): number { + return this._effectWrapper.occlusionStrength; + } + public set occlusionStrength(value: number) { + this._effectWrapper.occlusionStrength = value; + } + + declare protected _effectWrapper: ThinSelectionOutlinePostprocess; + private _maskTexture: Nullable = null; + private _depthTexture: Nullable = null; + + public constructor(name: string, maskTexture: RenderTargetTexture, depthTexture: RenderTargetTexture, options: ISelectionOutlinePostprocessOptions) { + const localOptions: PostProcessOptions = { + uniforms: ThinSelectionOutlinePostprocess.Uniforms, + samplers: ThinSelectionOutlinePostprocess.Samplers, + ...options, + }; + + super(name, ThinSelectionOutlinePostprocess.FragmentUrl, { + effectWrapper: !options.effectWrapper ? new ThinSelectionOutlinePostprocess(name, options.engine, localOptions) : undefined, + uniforms: ThinSelectionOutlinePostprocess.Uniforms, + samplers: ThinSelectionOutlinePostprocess.Samplers, + ...options, + }); + + this._maskTexture = maskTexture; + this._depthTexture = depthTexture; + this.onApplyObservable.add((effect: Effect) => { + if (!this._maskTexture) { + Logger.Warn("No mask texture set on SelectionOutlinePostprocess"); + return; + } + + this._effectWrapper.textureWidth = this.width; + this._effectWrapper.textureHeight = this.height; + + effect.setTexture("maskSampler", this._maskTexture); + effect.setTexture("depthSampler", this._depthTexture); + + this._effectWrapper.camera = this._maskTexture.activeCamera!; + }); + } + + public override getClassName(): string { + return "SelectionOutlinePostprocess"; + } + + public set maskTexture(value: RenderTargetTexture) { + this._maskTexture = value; + } + + public set depthTexture(value: RenderTargetTexture) { + this._depthTexture = value; + } +} + +interface ISelectionOutlineRendererOptions { + /** + * Color of the outline (default: (1, 0.5, 0) - orange) + */ + outlineColor?: Color3; + /** + * Number of samples for the final post process (default: 4) + */ + samples?: number; +} + +/** + * + */ +export class SelectionOutlineRenderingPipeline { + /** + * + */ + public static readonly InstanceSelectionIdAttributeName = "instanceSelectionId"; + + private readonly _name: string; + private readonly _camera: Camera; + private readonly _scene: Scene; + private readonly _depthRenderer: DepthRenderer; + + private _samples: number = 4; + public get samples(): number { + return this._samples; + } + public set samples(value: number) { + this._samples = value; + if (this._outlineProcess) { + this._outlineProcess.samples = value; + } + } + + private _outlineColor: Color3 = new Color3(1, 0.5, 0); + public get outlineColor(): Color3 { + return this._outlineColor; + } + public set outlineColor(value: Color3) { + this._outlineColor = value; + if (this._outlineProcess) { + this._outlineProcess.outlineColor = value; + } + } + + private _outlineThickness: number = 2.0; + public get outlineThickness(): number { + return this._outlineThickness; + } + public set outlineThickness(value: number) { + this._outlineThickness = value; + if (this._outlineProcess) { + this._outlineProcess.outlineThickness = value; + } + } + + private _occlusionStrength: number = 0.8; + public get occlusionStrength(): number { + return this._occlusionStrength; + } + public set occlusionStrength(value: number) { + this._occlusionStrength = value; + if (this._outlineProcess) { + this._outlineProcess.occlusionStrength = value; + } + } + + private readonly _meshUniqueIdToSelectionId: number[] = []; + + private readonly _selectionMaterialCache: Nullable[] = new Array(9).fill(null); + private readonly _selection: AbstractMesh[] = []; + + private _maskTexture: Nullable = null; + private _outlineProcess: Nullable = null; + private _isOutlineProcessAttached: boolean = false; + + private _resizeObserver: Nullable> = null; + + private _nextSelectionId = 1; + + /** Shader language used by the generator */ + protected _shaderLanguage = ShaderLanguage.GLSL; + + /** + * Gets the shader language used in this generator. + */ + public get shaderLanguage(): ShaderLanguage { + return this._shaderLanguage; + } + + /** + * @param name name of the process + * @param camera camera used for post process + * @param options options for the outline renderer + * + */ + public constructor(name: string, camera: Camera, options: ISelectionOutlineRendererOptions = {}) { + this._name = name; + this._camera = camera; + this._scene = camera.getScene(); + + this._depthRenderer = this._scene.enableDepthRenderer(camera); + + if (options.outlineColor) { + this._outlineColor = options.outlineColor; + } + if (options.samples) { + this._samples = options.samples; + } + + const engine = this._scene.getEngine(); + + if (engine.isWebGPU) { + this._shaderLanguage = ShaderLanguage.WGSL; + } + + // handle resize + this._resizeObserver = engine.onResizeObservable.add(this._resizeBuffer, undefined, undefined, this); + } + + private _resizeBuffer(): void { + const engine = this._scene.getEngine(); + const width = engine.getRenderWidth(); + const height = engine.getRenderHeight(); + + if (this._maskTexture !== null) { + const size = this._maskTexture.getSize(); + if (size.width !== width || size.height !== height) { + this._maskTexture.resize({ width: width, height: height }); + } + } + + if (this._depthRenderer !== null) { + const depthMap = this._depthRenderer.getDepthMap(); + const depthSize = depthMap.getSize(); + if (depthSize.width !== width || depthSize.height !== height) { + depthMap.resize({ width: width, height: height }); + } + } + } + + private _createRenderTargetTexture(): RenderTargetTexture { + if (this._maskTexture) { + return this._maskTexture; + } + + const engine = this._scene.getEngine(); + + this._maskTexture = new RenderTargetTexture(this._name + "_mask", { width: engine.getRenderWidth(), height: engine.getRenderHeight() }, this._scene, { + type: Constants.TEXTURETYPE_HALF_FLOAT, + format: Constants.TEXTUREFORMAT_RG, + }); + this._maskTexture.noPrePassRenderer = true; + this._maskTexture.clearColor = new Color4(0, 1, 1, 1); + this._maskTexture.wrapU = Constants.TEXTURE_CLAMP_ADDRESSMODE; + this._maskTexture.wrapV = Constants.TEXTURE_CLAMP_ADDRESSMODE; + + this._scene.customRenderTargets.push(this._maskTexture); + + return this._maskTexture; + } + + private _clearSelectionMaterials(): void { + for (let i = 0; i < this._selectionMaterialCache.length; ++i) { + const material = this._selectionMaterialCache[i]; + if (material !== null) { + material.dispose(); + this._selectionMaterialCache[i] = null; + } + } + } + + private _getSelectionMaterial(scene: Scene, fillMode: number): SelectionMaterial { + if (fillMode < 0 || 8 < fillMode) { + fillMode = Constants.MATERIAL_TriangleFillMode; + } + + const cachedMaterial = this._selectionMaterialCache[fillMode]; + if (cachedMaterial) { + return cachedMaterial; + } + + const engine = scene.getEngine(); + + if (engine.isWebGPU) { + this._shaderLanguage = ShaderLanguage.WGSL; + } + + const newMaterial = new SelectionMaterial(this._name + "_selection_material", scene, this._shaderLanguage, this._meshUniqueIdToSelectionId); + newMaterial.fillMode = fillMode; + newMaterial.backFaceCulling = false; + + this._selectionMaterialCache[fillMode] = newMaterial; + return newMaterial; + } + + private _prepareSelectionOutlinePostProcess(): void { + if (this._outlineProcess) { + return; + } + + if (!this._maskTexture) { + throw new Error("Mask texture not created before preparing outline post process"); + } + + this._outlineProcess = new SelectionOutlinePostprocess(this._name + "_outline", this._maskTexture, this._depthRenderer.getDepthMap(), { + camera: this._camera, + engine: this._scene.getEngine(), + }); + this._outlineProcess.outlineColor = this._outlineColor; + this._outlineProcess.outlineThickness = this._outlineThickness; + this._outlineProcess.occlusionStrength = this._occlusionStrength; + this._outlineProcess.samples = this._samples; + + this._isOutlineProcessAttached = true; + } + + public clearSelection(): void { + if (this._selection.length === 0) { + return; + } + + for (let index = 0; index < this._selection.length; ++index) { + const mesh = this._selection[index]; + if (mesh.hasInstances) { + (mesh as Mesh).removeVerticesData(SelectionOutlineRenderingPipeline.InstanceSelectionIdAttributeName); + } + if (this._maskTexture) { + this._maskTexture.setMaterialForRendering(mesh, undefined); + } + } + this._selection.length = 0; + this._meshUniqueIdToSelectionId.length = 0; + if (this._maskTexture) { + this._maskTexture.renderList = []; + } + + this._nextSelectionId = 1; + + if (this._outlineProcess && this._isOutlineProcessAttached) { + this._camera.detachPostProcess(this._outlineProcess); + this._isOutlineProcessAttached = false; + } + } + + public addSelection(meshes: (AbstractMesh | AbstractMesh[])[]): void { + if (meshes.length === 0) { + return; + } + + // prepare target texture + const maskTexture = this._createRenderTargetTexture(); + + let nextId = this._nextSelectionId; + for (let groupIndex = 0; groupIndex < meshes.length; ++groupIndex) { + const meshOrGroup = meshes[groupIndex]; + const id = nextId; + nextId += 1; + + const group = Array.isArray(meshOrGroup) ? meshOrGroup : [meshOrGroup]; + for (let meshIndex = 0; meshIndex < group.length; ++meshIndex) { + const mesh = group[meshIndex]; + + const material = this._getSelectionMaterial(this._scene, mesh.material?.fillMode ?? Constants.MATERIAL_TriangleFillMode); + maskTexture.setMaterialForRendering(group, material); + this._selection.push(mesh); // add to render list + + if (mesh.hasInstances || mesh.isAnInstance) { + const sourceMesh = (mesh as InstancedMesh).sourceMesh ?? (mesh as Mesh); + + if (sourceMesh.instancedBuffers?.[SelectionOutlineRenderingPipeline.InstanceSelectionIdAttributeName] === undefined) { + sourceMesh.registerInstancedBuffer(SelectionOutlineRenderingPipeline.InstanceSelectionIdAttributeName, 1); + // todo: consider unregistering buffer on dispose + } + + mesh.instancedBuffers[SelectionOutlineRenderingPipeline.InstanceSelectionIdAttributeName] = id; + } else { + this._meshUniqueIdToSelectionId[mesh.uniqueId] = id; + } + } + } + this._nextSelectionId = nextId; + + maskTexture.renderList = [...this._selection]; + + // set up outline post process + this._prepareSelectionOutlinePostProcess(); + if (!this._isOutlineProcessAttached) { + this._camera.attachPostProcess(this._outlineProcess!); + this._isOutlineProcessAttached = true; + } + } + + public dispose(): void { + this.clearSelection(); + + if (this._maskTexture !== null) { + const index = this._scene.customRenderTargets.indexOf(this._maskTexture); + if (index !== -1) { + this._scene.customRenderTargets.splice(index, 1); + } + this._maskTexture.dispose(); + this._maskTexture = null; + } + this._clearSelectionMaterials(); + + if (this._outlineProcess) { + if (this._isOutlineProcessAttached) { + this._camera.detachPostProcess(this._outlineProcess); + this._isOutlineProcessAttached = false; + } + } + + this._outlineProcess?.dispose(); + this._outlineProcess = null; + + this._resizeObserver?.remove(); + this._resizeObserver = null; + } +} diff --git a/packages/dev/core/src/Shaders/selection.fragment.fx b/packages/dev/core/src/Shaders/selection.fragment.fx new file mode 100644 index 00000000000..f45237dac07 --- /dev/null +++ b/packages/dev/core/src/Shaders/selection.fragment.fx @@ -0,0 +1,16 @@ +#if defined(INSTANCES) +flat varying float vSelectionId; +#else +uniform float selectionId; +#endif +varying float vDepthMetric; + +void main(void) { +#if defined(INSTANCES) + float id = vSelectionId; +#else + float id = selectionId; +#endif + + gl_FragColor = vec4(id, vDepthMetric, 0.0, 1.0); +} diff --git a/packages/dev/core/src/Shaders/selection.vertex.fx b/packages/dev/core/src/Shaders/selection.vertex.fx new file mode 100644 index 00000000000..8a0f9954412 --- /dev/null +++ b/packages/dev/core/src/Shaders/selection.vertex.fx @@ -0,0 +1,43 @@ +// Attributes +attribute vec3 position; +#if defined(INSTANCES) +attribute float instanceSelectionId; +#endif + +#include +#include +#include +#include[0..maxSimultaneousMorphTargets] + +// Uniforms + +#include +uniform mat4 viewProjection; +uniform vec2 depthValues; + +// Output +#if defined(INSTANCES) +flat varying float vSelectionId; +#endif +varying float vDepthMetric; + +void main(void) { + +#include +#include[0..maxSimultaneousMorphTargets] +#include +#include +#include + vec4 worldPos = finalWorld * vec4(position, 1.0); + gl_Position = viewProjection * worldPos; + + #ifdef USE_REVERSE_DEPTHBUFFER + vDepthMetric = ((-gl_Position.z + depthValues.x) / (depthValues.y)); + #else + vDepthMetric = ((gl_Position.z + depthValues.x) / (depthValues.y)); + #endif + +#if defined(INSTANCES) + vSelectionId = instanceSelectionId; +#endif +} diff --git a/packages/dev/core/src/Shaders/selectionOutline.fragment.fx b/packages/dev/core/src/Shaders/selectionOutline.fragment.fx new file mode 100644 index 00000000000..ee577fbdf0a --- /dev/null +++ b/packages/dev/core/src/Shaders/selectionOutline.fragment.fx @@ -0,0 +1,58 @@ +// samplers +uniform sampler2D textureSampler; +uniform sampler2D maskSampler; +uniform sampler2D depthSampler; + +// varyings +varying vec2 vUV; + +// uniforms +uniform vec2 screenSize; +uniform vec3 outlineColor; +uniform float outlineThickness; +uniform float occlusionStrength; + +#define CUSTOM_FRAGMENT_DEFINITIONS + +void main(void) +{ + vec4 screenColor = texture2D(textureSampler, vUV); + + vec2 texelSize = 1.0 / screenSize; + vec2 sampleOffset = texelSize * outlineThickness; + + // sample mask texture for edge detection and depth-based occlusion + vec2 centerMask = texture2D(maskSampler, vUV).rg; + vec2 maskX = texture2D(maskSampler, vUV + vec2(sampleOffset.x, 0.0)).rg; + vec2 maskY = texture2D(maskSampler, vUV + vec2(0.0, sampleOffset.y)).rg; + vec2 maskXY = texture2D(maskSampler, vUV + sampleOffset).rg; + + // gradient magnitude edge detection + vec3 gradient = vec3( + centerMask.r - maskX.r, + centerMask.r - maskY.r, + centerMask.r - maskXY.r + ); + float edgeStrength = length(gradient); + float outlineMask = step(0.1, edgeStrength); // 0.1 is the outline threshold + + // sample depth texture for depth-based occlusion + float depthCenter = texture2D(depthSampler, vUV).r; + float depthX = texture2D(depthSampler, vUV + vec2(sampleOffset.x, 0.0)).r; + float depthY = texture2D(depthSampler, vUV + vec2(0.0, sampleOffset.y)).r; + float depthXY = texture2D(depthSampler, vUV + sampleOffset).r; + + const float occlusionThreshold = 0.01; + float occlusionCenter = step(occlusionThreshold, abs(centerMask.g - depthCenter)); + float occlusionX = step(occlusionThreshold, abs(maskX.g - depthX)); + float occlusionY = step(occlusionThreshold, abs(maskY.g - depthY)); + float occlusionXY = step(occlusionThreshold, abs(maskXY.g - depthXY)); + + float occlusionFactor = max(max(occlusionCenter, occlusionX), max(occlusionY, occlusionXY)); + + float finalOutlineMask = outlineMask * (1.0 - occlusionStrength * occlusionFactor); + + vec3 finalColor = mix(screenColor.rgb, outlineColor, finalOutlineMask); + + gl_FragColor = vec4(finalColor, screenColor.a); +} From 7cd7dc7be47799653672848b5f0a84f20fc13346 Mon Sep 17 00:00:00 2001 From: noname0310 Date: Tue, 23 Dec 2025 10:17:11 +0900 Subject: [PATCH 02/17] add WebGPU support --- .../selectionOutlineRenderingPipeline.ts | 8 +-- .../src/ShadersWGSL/selection.fragment.fx | 17 ++++++ .../core/src/ShadersWGSL/selection.vertex.fx | 44 +++++++++++++ .../ShadersWGSL/selectionOutline.fragment.fx | 61 +++++++++++++++++++ 4 files changed, 126 insertions(+), 4 deletions(-) create mode 100644 packages/dev/core/src/ShadersWGSL/selection.fragment.fx create mode 100644 packages/dev/core/src/ShadersWGSL/selection.vertex.fx create mode 100644 packages/dev/core/src/ShadersWGSL/selectionOutline.fragment.fx diff --git a/packages/dev/core/src/PostProcesses/RenderPipeline/Pipelines/selectionOutlineRenderingPipeline.ts b/packages/dev/core/src/PostProcesses/RenderPipeline/Pipelines/selectionOutlineRenderingPipeline.ts index 368298f1ef3..16236dcd4be 100644 --- a/packages/dev/core/src/PostProcesses/RenderPipeline/Pipelines/selectionOutlineRenderingPipeline.ts +++ b/packages/dev/core/src/PostProcesses/RenderPipeline/Pipelines/selectionOutlineRenderingPipeline.ts @@ -40,9 +40,9 @@ class SelectionMaterial extends ShaderMaterial { shaderLanguage: shaderLanguage, extraInitializationsAsync: async () => { if (this.shaderLanguage === ShaderLanguage.WGSL) { - // await Promise.all([import("../ShadersWGSL/selection.fragment"), import("../ShadersWGSL/selection.vertex")]); + await Promise.all([import("../../../ShadersWGSL/selection.fragment"), import("../../../ShadersWGSL/selection.vertex")]); } else { - await Promise.all([import("../../../Shaders/selection.fragment"), import("../Shaders/selection.vertex")]); + await Promise.all([import("../../../Shaders/selection.fragment"), import("../../../Shaders/selection.vertex")]); } }, }; @@ -113,9 +113,9 @@ class ThinSelectionOutlinePostprocess extends EffectWrapper { protected override _gatherImports(useWebGPU: boolean, list: Promise[]): void { if (useWebGPU) { this._webGPUReady = true; - // list.push(import("../ShadersWGSL/selectionOutline.fragment")); + list.push(import("../../../ShadersWGSL/selectionOutline.fragment")); } else { - list.push(import("../Shaders/selectionOutline.fragment")); + list.push(import("../../../Shaders/selectionOutline.fragment")); } } diff --git a/packages/dev/core/src/ShadersWGSL/selection.fragment.fx b/packages/dev/core/src/ShadersWGSL/selection.fragment.fx new file mode 100644 index 00000000000..5276f542115 --- /dev/null +++ b/packages/dev/core/src/ShadersWGSL/selection.fragment.fx @@ -0,0 +1,17 @@ +#if defined(INSTANCES) +flat varying vSelectionId: f32; +#else +uniform selectionId: f32; +#endif +varying vDepthMetric: f32; + +@fragment +fn main(input: FragmentInputs) -> FragmentOutputs { +#if defined(INSTANCES) + var id: f32 = input.vSelectionId; +#else + var id: f32 = uniforms.selectionId; +#endif + + fragmentOutputs.color = vec4(id, input.vDepthMetric, 0.0, 1.0); +} diff --git a/packages/dev/core/src/ShadersWGSL/selection.vertex.fx b/packages/dev/core/src/ShadersWGSL/selection.vertex.fx new file mode 100644 index 00000000000..9148569d341 --- /dev/null +++ b/packages/dev/core/src/ShadersWGSL/selection.vertex.fx @@ -0,0 +1,44 @@ +// Attributes +attribute position: vec3f; +#if defined(INSTANCES) +attribute instanceSelectionId: f32; +#endif + +#include +#include +#include +#include[0..maxSimultaneousMorphTargets] + +// Uniforms + +#include +uniform viewProjection: mat4x4f; +uniform depthValues: vec2f; + +// Output +#if defined(INSTANCES) +flat varying vSelectionId: f32; +#endif +varying vDepthMetric: f32; + +@vertex +fn main(input: VertexInputs) -> FragmentInputs { + +#include +#include[0..maxSimultaneousMorphTargets] +#include +#include +#include + var worldPos: vec4f = finalWorld * vec4f(input.position, 1.0); + vertexOutputs.position = uniforms.viewProjection * worldPos; + + #ifdef USE_REVERSE_DEPTHBUFFER + vertexOutputs.vDepthMetric = ((-vertexOutputs.position.z + uniforms.depthValues.x) / (uniforms.depthValues.y)); + #else + vertexOutputs.vDepthMetric = ((vertexOutputs.position.z + uniforms.depthValues.x) / (uniforms.depthValues.y)); + #endif + +#if defined(INSTANCES) + vertexOutputs.vSelectionId = input.instanceSelectionId; +#endif +} diff --git a/packages/dev/core/src/ShadersWGSL/selectionOutline.fragment.fx b/packages/dev/core/src/ShadersWGSL/selectionOutline.fragment.fx new file mode 100644 index 00000000000..9181a85cea2 --- /dev/null +++ b/packages/dev/core/src/ShadersWGSL/selectionOutline.fragment.fx @@ -0,0 +1,61 @@ +// samplers +var textureSamplerSampler: sampler; +uniform textureSampler: texture_2d; +var maskSamplerSampler: sampler; +uniform maskSampler: texture_2d; +var depthSamplerSampler: sampler; +uniform depthSampler: texture_2d; + +// varyings +varying vUV: vec2f; + +// uniforms +uniform screenSize: vec2f; +uniform outlineColor: vec3f; +uniform outlineThickness: f32; +uniform occlusionStrength: f32; + +#define CUSTOM_FRAGMENT_DEFINITIONS + +@fragment +fn main(input: FragmentInputs) -> FragmentOutputs { + var screenColor: vec4f = textureSampleLevel(textureSampler, textureSamplerSampler, input.vUV, 0.0); + + var texelSize: vec2f = 1.0 / uniforms.screenSize; + var sampleOffset: vec2f = texelSize * uniforms.outlineThickness; + + // sample mask texture for edge detection and depth-based occlusion + var centerMask: vec2f = textureSampleLevel(maskSampler, maskSamplerSampler, input.vUV, 0.0).rg; + var maskX: vec2f = textureSampleLevel(maskSampler, maskSamplerSampler, input.vUV + vec2f(sampleOffset.x, 0.0), 0.0).rg; + var maskY: vec2f = textureSampleLevel(maskSampler, maskSamplerSampler, input.vUV + vec2f(0.0, sampleOffset.y), 0.0).rg; + var maskXY: vec2f = textureSampleLevel(maskSampler, maskSamplerSampler, input.vUV + sampleOffset, 0.0).rg; + + // gradient magnitude edge detection + var gradient: vec3f = vec3f( + centerMask.r - maskX.r, + centerMask.r - maskY.r, + centerMask.r - maskXY.r + ); + var edgeStrength: f32 = length(gradient); + var outlineMask: f32 = step(0.1, edgeStrength); // 0.1 is the outline threshold + + // sample depth texture for depth-based occlusion + var depthCenter: f32 = textureSampleLevel(depthSampler, depthSamplerSampler, input.vUV, 0.0).r; + var depthX: f32 = textureSampleLevel(depthSampler, depthSamplerSampler, input.vUV + vec2f(sampleOffset.x, 0.0), 0.0).r; + var depthY: f32 = textureSampleLevel(depthSampler, depthSamplerSampler, input.vUV + vec2f(0.0, sampleOffset.y), 0.0).r; + var depthXY: f32 = textureSampleLevel(depthSampler, depthSamplerSampler, input.vUV + sampleOffset, 0.0).r; + + const var occlusionThreshold: f32 = 0.01; + var occlusionCenter: f32 = step(occlusionThreshold, abs(centerMask.g - depthCenter)); + var occlusionX: f32 = step(occlusionThreshold, abs(maskX.g - depthX)); + var occlusionY: f32 = step(occlusionThreshold, abs(maskY.g - depthY)); + var occlusionXY: f32 = step(occlusionThreshold, abs(maskXY.g - depthXY)); + + var occlusionFactor: f32 = max(max(occlusionCenter, occlusionX), max(occlusionY, occlusionXY)); + + var finalOutlineMask: f32 = outlineMask * (1.0 - uniforms.occlusionStrength * occlusionFactor); + + var finalColor: vec3f = mix(screenColor.rgb, uniforms.outlineColor, finalOutlineMask); + + fragmentOutputs.color = vec4f(finalColor, screenColor.a); +} From 5151bdcdad8c0e62f976fc5b008d251cb8a41e18 Mon Sep 17 00:00:00 2001 From: noname0310 Date: Tue, 23 Dec 2025 22:19:02 +0900 Subject: [PATCH 03/17] Export selectionOutlineRenderingPipeline module --- .../dev/core/src/PostProcesses/RenderPipeline/Pipelines/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/dev/core/src/PostProcesses/RenderPipeline/Pipelines/index.ts b/packages/dev/core/src/PostProcesses/RenderPipeline/Pipelines/index.ts index ab1ed8e86cd..3be001d4a84 100644 --- a/packages/dev/core/src/PostProcesses/RenderPipeline/Pipelines/index.ts +++ b/packages/dev/core/src/PostProcesses/RenderPipeline/Pipelines/index.ts @@ -1,5 +1,6 @@ export * from "./defaultRenderingPipeline"; export * from "./lensRenderingPipeline"; +export * from "./selectionOutlineRenderingPipeline"; export * from "./ssao2RenderingPipeline"; export * from "./ssaoRenderingPipeline"; export * from "./standardRenderingPipeline"; From 226bb9d2526ad349206b86b639bbae29713fb721 Mon Sep 17 00:00:00 2001 From: noname0310 Date: Tue, 23 Dec 2025 23:30:34 +0900 Subject: [PATCH 04/17] handle instanced buffer null case --- packages/dev/core/src/Meshes/instancedMesh.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/dev/core/src/Meshes/instancedMesh.ts b/packages/dev/core/src/Meshes/instancedMesh.ts index 29eda86ebcf..be6ec94aa23 100644 --- a/packages/dev/core/src/Meshes/instancedMesh.ts +++ b/packages/dev/core/src/Meshes/instancedMesh.ts @@ -771,7 +771,7 @@ Mesh.prototype._processInstancedBuffers = function (visibleInstances: Nullable Date: Wed, 24 Dec 2025 08:50:16 +0900 Subject: [PATCH 05/17] Change occlusion factor calculation to use min instead of max --- packages/dev/core/src/Shaders/selectionOutline.fragment.fx | 2 +- .../dev/core/src/ShadersWGSL/selectionOutline.fragment.fx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/dev/core/src/Shaders/selectionOutline.fragment.fx b/packages/dev/core/src/Shaders/selectionOutline.fragment.fx index ee577fbdf0a..8ac23779399 100644 --- a/packages/dev/core/src/Shaders/selectionOutline.fragment.fx +++ b/packages/dev/core/src/Shaders/selectionOutline.fragment.fx @@ -48,7 +48,7 @@ void main(void) float occlusionY = step(occlusionThreshold, abs(maskY.g - depthY)); float occlusionXY = step(occlusionThreshold, abs(maskXY.g - depthXY)); - float occlusionFactor = max(max(occlusionCenter, occlusionX), max(occlusionY, occlusionXY)); + float occlusionFactor = min(min(occlusionCenter, occlusionX), min(occlusionY, occlusionXY)); float finalOutlineMask = outlineMask * (1.0 - occlusionStrength * occlusionFactor); diff --git a/packages/dev/core/src/ShadersWGSL/selectionOutline.fragment.fx b/packages/dev/core/src/ShadersWGSL/selectionOutline.fragment.fx index 9181a85cea2..e6ca3ab0de8 100644 --- a/packages/dev/core/src/ShadersWGSL/selectionOutline.fragment.fx +++ b/packages/dev/core/src/ShadersWGSL/selectionOutline.fragment.fx @@ -45,13 +45,13 @@ fn main(input: FragmentInputs) -> FragmentOutputs { var depthY: f32 = textureSampleLevel(depthSampler, depthSamplerSampler, input.vUV + vec2f(0.0, sampleOffset.y), 0.0).r; var depthXY: f32 = textureSampleLevel(depthSampler, depthSamplerSampler, input.vUV + sampleOffset, 0.0).r; - const var occlusionThreshold: f32 = 0.01; + const occlusionThreshold: f32 = 0.01; var occlusionCenter: f32 = step(occlusionThreshold, abs(centerMask.g - depthCenter)); var occlusionX: f32 = step(occlusionThreshold, abs(maskX.g - depthX)); var occlusionY: f32 = step(occlusionThreshold, abs(maskY.g - depthY)); var occlusionXY: f32 = step(occlusionThreshold, abs(maskXY.g - depthXY)); - var occlusionFactor: f32 = max(max(occlusionCenter, occlusionX), max(occlusionY, occlusionXY)); + var occlusionFactor: f32 = min(min(occlusionCenter, occlusionX), min(occlusionY, occlusionXY)); var finalOutlineMask: f32 = outlineMask * (1.0 - uniforms.occlusionStrength * occlusionFactor); From d38797986b98a5df904a7e5cda9b58221c5b4b8e Mon Sep 17 00:00:00 2001 From: noname0310 Date: Wed, 24 Dec 2025 10:51:55 +0900 Subject: [PATCH 06/17] adjust occlusion threshold for better visual --- packages/dev/core/src/Shaders/selectionOutline.fragment.fx | 2 +- packages/dev/core/src/ShadersWGSL/selectionOutline.fragment.fx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/dev/core/src/Shaders/selectionOutline.fragment.fx b/packages/dev/core/src/Shaders/selectionOutline.fragment.fx index 8ac23779399..08779a07a4d 100644 --- a/packages/dev/core/src/Shaders/selectionOutline.fragment.fx +++ b/packages/dev/core/src/Shaders/selectionOutline.fragment.fx @@ -42,7 +42,7 @@ void main(void) float depthY = texture2D(depthSampler, vUV + vec2(0.0, sampleOffset.y)).r; float depthXY = texture2D(depthSampler, vUV + sampleOffset).r; - const float occlusionThreshold = 0.01; + const float occlusionThreshold = 0.0000001; float occlusionCenter = step(occlusionThreshold, abs(centerMask.g - depthCenter)); float occlusionX = step(occlusionThreshold, abs(maskX.g - depthX)); float occlusionY = step(occlusionThreshold, abs(maskY.g - depthY)); diff --git a/packages/dev/core/src/ShadersWGSL/selectionOutline.fragment.fx b/packages/dev/core/src/ShadersWGSL/selectionOutline.fragment.fx index e6ca3ab0de8..6fa7c45988f 100644 --- a/packages/dev/core/src/ShadersWGSL/selectionOutline.fragment.fx +++ b/packages/dev/core/src/ShadersWGSL/selectionOutline.fragment.fx @@ -45,7 +45,7 @@ fn main(input: FragmentInputs) -> FragmentOutputs { var depthY: f32 = textureSampleLevel(depthSampler, depthSamplerSampler, input.vUV + vec2f(0.0, sampleOffset.y), 0.0).r; var depthXY: f32 = textureSampleLevel(depthSampler, depthSamplerSampler, input.vUV + sampleOffset, 0.0).r; - const occlusionThreshold: f32 = 0.01; + const occlusionThreshold: f32 = 0.0000001; var occlusionCenter: f32 = step(occlusionThreshold, abs(centerMask.g - depthCenter)); var occlusionX: f32 = step(occlusionThreshold, abs(maskX.g - depthX)); var occlusionY: f32 = step(occlusionThreshold, abs(maskY.g - depthY)); From 9b963f3b5f3b282fee423b47e77211616de935f1 Mon Sep 17 00:00:00 2001 From: noname0310 Date: Wed, 24 Dec 2025 14:57:11 +0900 Subject: [PATCH 07/17] Refactor selection outline post process implementation --- .../selectionOutlineRenderingPipeline.ts | 248 +++++------------- packages/dev/core/src/PostProcesses/index.ts | 2 + .../selectionOutlinePostProcess.ts | 114 ++++++++ .../thinSelectionOutlinePostProcess.ts | 90 +++++++ 4 files changed, 276 insertions(+), 178 deletions(-) create mode 100644 packages/dev/core/src/PostProcesses/selectionOutlinePostProcess.ts create mode 100644 packages/dev/core/src/PostProcesses/thinSelectionOutlinePostProcess.ts diff --git a/packages/dev/core/src/PostProcesses/RenderPipeline/Pipelines/selectionOutlineRenderingPipeline.ts b/packages/dev/core/src/PostProcesses/RenderPipeline/Pipelines/selectionOutlineRenderingPipeline.ts index 16236dcd4be..3eb522b8359 100644 --- a/packages/dev/core/src/PostProcesses/RenderPipeline/Pipelines/selectionOutlineRenderingPipeline.ts +++ b/packages/dev/core/src/PostProcesses/RenderPipeline/Pipelines/selectionOutlineRenderingPipeline.ts @@ -1,10 +1,7 @@ import { Camera } from "../../../Cameras/camera"; import type { AbstractEngine } from "../../../Engines/abstractEngine"; import { Constants } from "../../../Engines/constants"; -import { Engine } from "../../../Engines/engine"; import type { Effect } from "../../../Materials/effect"; -import type { EffectWrapperCreationOptions } from "../../../Materials/effectRenderer"; -import { EffectWrapper } from "../../../Materials/effectRenderer"; import { ShaderLanguage } from "../../../Materials/shaderLanguage"; import type { IShaderMaterialOptions } from "../../../Materials/shaderMaterial"; import { ShaderMaterial } from "../../../Materials/shaderMaterial"; @@ -16,19 +13,29 @@ import { VertexBuffer } from "../../../Meshes/buffer"; import type { InstancedMesh } from "../../../Meshes/instancedMesh"; import type { Mesh } from "../../../Meshes/mesh"; import type { SubMesh } from "../../../Meshes/subMesh"; -import { serialize } from "../../../Misc/decorators"; -import { Logger } from "../../../Misc/logger"; import type { Observer } from "../../../Misc/observable"; import type { DepthRenderer } from "../../../Rendering/depthRenderer"; -import "../../../Rendering/depthRendererSceneComponent"; import type { Scene } from "../../../scene"; import type { Nullable } from "../../../types"; -import type { PostProcessOptions } from "../../postProcess"; -import { PostProcess } from "../../postProcess"; +import "../../../Rendering/depthRendererSceneComponent"; +import { SelectionOutlinePostProcess } from "../../selectionOutlinePostProcess"; + +/** + * Selection material used to generate the selection mask + * + * Selection material use r and g channels to store the selection ID and depth information + */ class SelectionMaterial extends ShaderMaterial { private readonly _meshUniqueIdToSelectionId: number[]; + /** + * Constructs a new selection mask material + * @param name The name of the material + * @param scene The scene the material belongs to + * @param shaderLanguage The shader language to use + * @param meshUniqueIdToSelectionId Mapping from mesh unique IDs to selection IDs + */ public constructor(name: string, scene: Scene, shaderLanguage: ShaderLanguage, meshUniqueIdToSelectionId: number[]) { const defines: string[] = []; const options: Partial = { @@ -51,6 +58,13 @@ class SelectionMaterial extends ShaderMaterial { this._meshUniqueIdToSelectionId = meshUniqueIdToSelectionId; } + /** + * Binds the material to the mesh + * @param world The world matrix + * @param mesh The mesh to bind the material to + * @param effectOverride An optional effect override + * @param subMesh The submesh to bind the material to + */ public override bind(world: Matrix, mesh?: AbstractMesh, effectOverride?: Nullable, subMesh?: SubMesh): void { super.bind(world, mesh, effectOverride, subMesh); if (!mesh) { @@ -90,173 +104,9 @@ class SelectionMaterial extends ShaderMaterial { } /** - * Options used to create a ThinSelectionOutlinePostprocess + * Options for the selection outline rendering pipeline */ -export interface IThinSelectionOutlinePostProcessOptions extends EffectWrapperCreationOptions {} - -class ThinSelectionOutlinePostprocess extends EffectWrapper { - /** - * The fragment shader url - */ - public static readonly FragmentUrl = "selectionOutline"; - - /** - * The list of uniforms used by the effect - */ - public static readonly Uniforms = ["screenSize", "outlineColor", "outlineThickness", "occlusionStrength"]; - - /** - * The list of samplers used by the effect - */ - public static readonly Samplers = ["maskSampler", "depthSampler"]; - - protected override _gatherImports(useWebGPU: boolean, list: Promise[]): void { - if (useWebGPU) { - this._webGPUReady = true; - list.push(import("../../../ShadersWGSL/selectionOutline.fragment")); - } else { - list.push(import("../../../Shaders/selectionOutline.fragment")); - } - } - - /** - * Constructs a new thin selection outline post process - * @param name Name of the effect - * @param engine Engine to use to render the effect. If not provided, the last created engine will be used - * @param options Options to configure the effect - */ - public constructor(name: string, engine: Nullable = null, options?: IThinSelectionOutlinePostProcessOptions) { - super({ - ...options, - name, - engine: engine || Engine.LastCreatedEngine!, - useShaderStore: true, - useAsPostProcess: true, - fragmentShader: ThinSelectionOutlinePostprocess.FragmentUrl, - uniforms: ThinSelectionOutlinePostprocess.Uniforms, - samplers: ThinSelectionOutlinePostprocess.Samplers, - }); - } - - /** - * The camera to use to calculate the outline - */ - public camera: Nullable = null; - - /** - * THe outline color - */ - public outlineColor: Color3 = new Color3(1, 0.5, 0); - - /** - * The thickness of the edges - */ - public outlineThickness: number = 2.0; - - /** - * The strength of the occlusion effect (default: 0.8) - */ - public occlusionStrength: number = 0.8; - - /** - * The width of the source texture - */ - public textureWidth: number = 0; - - /** - * The height of the source texture - */ - public textureHeight: number = 0; - - public override bind(noDefaultBindings = false): void { - super.bind(noDefaultBindings); - - const effect = this._drawWrapper.effect!; - - effect.setFloat2("screenSize", this.textureWidth, this.textureHeight); - effect.setColor3("outlineColor", this.outlineColor); - effect.setFloat("outlineThickness", this.outlineThickness); - effect.setFloat("occlusionStrength", this.occlusionStrength); - } -} - -interface ISelectionOutlinePostprocessOptions extends IThinSelectionOutlinePostProcessOptions, PostProcessOptions {} - -class SelectionOutlinePostprocess extends PostProcess { - @serialize() - public get outlineColor(): Color3 { - return this._effectWrapper.outlineColor; - } - public set outlineColor(value: Color3) { - this._effectWrapper.outlineColor = value; - } - - @serialize() - public get outlineThickness(): number { - return this._effectWrapper.outlineThickness; - } - public set outlineThickness(value: number) { - this._effectWrapper.outlineThickness = value; - } - - @serialize() - public get occlusionStrength(): number { - return this._effectWrapper.occlusionStrength; - } - public set occlusionStrength(value: number) { - this._effectWrapper.occlusionStrength = value; - } - - declare protected _effectWrapper: ThinSelectionOutlinePostprocess; - private _maskTexture: Nullable = null; - private _depthTexture: Nullable = null; - - public constructor(name: string, maskTexture: RenderTargetTexture, depthTexture: RenderTargetTexture, options: ISelectionOutlinePostprocessOptions) { - const localOptions: PostProcessOptions = { - uniforms: ThinSelectionOutlinePostprocess.Uniforms, - samplers: ThinSelectionOutlinePostprocess.Samplers, - ...options, - }; - - super(name, ThinSelectionOutlinePostprocess.FragmentUrl, { - effectWrapper: !options.effectWrapper ? new ThinSelectionOutlinePostprocess(name, options.engine, localOptions) : undefined, - uniforms: ThinSelectionOutlinePostprocess.Uniforms, - samplers: ThinSelectionOutlinePostprocess.Samplers, - ...options, - }); - - this._maskTexture = maskTexture; - this._depthTexture = depthTexture; - this.onApplyObservable.add((effect: Effect) => { - if (!this._maskTexture) { - Logger.Warn("No mask texture set on SelectionOutlinePostprocess"); - return; - } - - this._effectWrapper.textureWidth = this.width; - this._effectWrapper.textureHeight = this.height; - - effect.setTexture("maskSampler", this._maskTexture); - effect.setTexture("depthSampler", this._depthTexture); - - this._effectWrapper.camera = this._maskTexture.activeCamera!; - }); - } - - public override getClassName(): string { - return "SelectionOutlinePostprocess"; - } - - public set maskTexture(value: RenderTargetTexture) { - this._maskTexture = value; - } - - public set depthTexture(value: RenderTargetTexture) { - this._depthTexture = value; - } -} - -interface ISelectionOutlineRendererOptions { +export interface ISelectionOutlineRenderingPipelineOptions { /** * Color of the outline (default: (1, 0.5, 0) - orange) */ @@ -268,11 +118,18 @@ interface ISelectionOutlineRendererOptions { } /** + * Selection outline rendering pipeline + * + * Use optimized brute force approach to render outlines around selected objects * + * The selection rendering pipeline use two main steps: + * 1. Render selected objects to a mask texture where r and g channels store selection ID and depth information + * 2. Apply a post process that will use the mask texture to render outlines around selected objects */ export class SelectionOutlineRenderingPipeline { /** - * + * Name of the instance selection ID attribute + * @internal */ public static readonly InstanceSelectionIdAttributeName = "instanceSelectionId"; @@ -282,6 +139,9 @@ export class SelectionOutlineRenderingPipeline { private readonly _depthRenderer: DepthRenderer; private _samples: number = 4; + /** + * Gets or sets the number of samples used for the outline post process (default: 4) + */ public get samples(): number { return this._samples; } @@ -293,6 +153,9 @@ export class SelectionOutlineRenderingPipeline { } private _outlineColor: Color3 = new Color3(1, 0.5, 0); + /** + * Gets or sets the outline color (default: (1, 0.5, 0) - orange) + */ public get outlineColor(): Color3 { return this._outlineColor; } @@ -304,6 +167,9 @@ export class SelectionOutlineRenderingPipeline { } private _outlineThickness: number = 2.0; + /** + * Gets or sets the outline thickness (default: 2.0) + */ public get outlineThickness(): number { return this._outlineThickness; } @@ -315,6 +181,9 @@ export class SelectionOutlineRenderingPipeline { } private _occlusionStrength: number = 0.8; + /** + * Gets or sets the occlusion strength (default: 0.8) + */ public get occlusionStrength(): number { return this._occlusionStrength; } @@ -331,7 +200,7 @@ export class SelectionOutlineRenderingPipeline { private readonly _selection: AbstractMesh[] = []; private _maskTexture: Nullable = null; - private _outlineProcess: Nullable = null; + private _outlineProcess: Nullable = null; private _isOutlineProcessAttached: boolean = false; private _resizeObserver: Nullable> = null; @@ -354,7 +223,7 @@ export class SelectionOutlineRenderingPipeline { * @param options options for the outline renderer * */ - public constructor(name: string, camera: Camera, options: ISelectionOutlineRendererOptions = {}) { + public constructor(name: string, camera: Camera, options: ISelectionOutlineRenderingPipelineOptions = {}) { this._name = name; this._camera = camera; this._scene = camera.getScene(); @@ -463,7 +332,7 @@ export class SelectionOutlineRenderingPipeline { throw new Error("Mask texture not created before preparing outline post process"); } - this._outlineProcess = new SelectionOutlinePostprocess(this._name + "_outline", this._maskTexture, this._depthRenderer.getDepthMap(), { + this._outlineProcess = new SelectionOutlinePostProcess(this._name + "_outline", this._maskTexture, this._depthRenderer.getDepthMap(), { camera: this._camera, engine: this._scene.getEngine(), }); @@ -475,6 +344,9 @@ export class SelectionOutlineRenderingPipeline { this._isOutlineProcessAttached = true; } + /** + * Clears the current selection + */ public clearSelection(): void { if (this._selection.length === 0) { return; @@ -503,6 +375,12 @@ export class SelectionOutlineRenderingPipeline { } } + /** + * Adds meshe or group of meshes to the current selection + * + * If a group of meshes is provided, they will outline as a single unit + * @param meshes Meshes to add to the selection + */ public addSelection(meshes: (AbstractMesh | AbstractMesh[])[]): void { if (meshes.length === 0) { return; @@ -551,6 +429,20 @@ export class SelectionOutlineRenderingPipeline { } } + /** + * Sets the current selection, replacing any previous selection + * + * If a group of meshes is provided, they will outline as a single unit + * @param meshes Meshes to set as the current selection + */ + public setSelection(meshes: (AbstractMesh | AbstractMesh[])[]): void { + this.clearSelection(); + this.addSelection(meshes); + } + + /** + * Disposes the rendering pipeline + */ public dispose(): void { this.clearSelection(); diff --git a/packages/dev/core/src/PostProcesses/index.ts b/packages/dev/core/src/PostProcesses/index.ts index aa5efcdf49a..d8fc7bd6dda 100644 --- a/packages/dev/core/src/PostProcesses/index.ts +++ b/packages/dev/core/src/PostProcesses/index.ts @@ -24,6 +24,7 @@ export * from "./postProcess"; export * from "./postProcessManager"; export * from "./refractionPostProcess"; export * from "./RenderPipeline/index"; +export * from "./selectionOutlinePostProcess"; export * from "./sharpenPostProcess"; export * from "./stereoscopicInterlacePostProcess"; export * from "./tonemapPostProcess"; @@ -51,6 +52,7 @@ export * from "./thinMotionBlurPostProcess"; export * from "./thinPassPostProcess"; export * from "./thinSharpenPostProcess"; export * from "./thinScreenSpaceCurvaturePostProcess"; +export * from "./thinSelectionOutlinePostProcess"; export * from "./thinTonemapPostProcess"; // Postprocess diff --git a/packages/dev/core/src/PostProcesses/selectionOutlinePostProcess.ts b/packages/dev/core/src/PostProcesses/selectionOutlinePostProcess.ts new file mode 100644 index 00000000000..0fd52d19da5 --- /dev/null +++ b/packages/dev/core/src/PostProcesses/selectionOutlinePostProcess.ts @@ -0,0 +1,114 @@ +import type { Effect } from "../Materials/effect"; +import type { RenderTargetTexture } from "../Materials/Textures/renderTargetTexture"; +import type { Color3 } from "../Maths/math.color"; +import { serialize } from "../Misc/decorators"; +import { Logger } from "../Misc/logger"; +import { RegisterClass } from "../Misc/typeStore"; +import type { Nullable } from "../types"; +import type { PostProcessOptions } from "./postProcess"; +import { PostProcess } from "./postProcess"; +import { ThinSelectionOutlinePostProcess } from "./thinSelectionOutlinePostProcess"; + +/** + * Post process used to render an outline around selected objects + */ +export class SelectionOutlinePostProcess extends PostProcess { + /** + * Gets or sets the outline color + */ + @serialize() + public get outlineColor(): Color3 { + return this._effectWrapper.outlineColor; + } + public set outlineColor(value: Color3) { + this._effectWrapper.outlineColor = value; + } + + /** + * Gets or sets the outline thickness + */ + @serialize() + public get outlineThickness(): number { + return this._effectWrapper.outlineThickness; + } + public set outlineThickness(value: number) { + this._effectWrapper.outlineThickness = value; + } + + /** + * Gets or sets the occlusion strength + */ + @serialize() + public get occlusionStrength(): number { + return this._effectWrapper.occlusionStrength; + } + public set occlusionStrength(value: number) { + this._effectWrapper.occlusionStrength = value; + } + + declare protected _effectWrapper: ThinSelectionOutlinePostProcess; + private _maskTexture: Nullable = null; + private _depthTexture: Nullable = null; + + /** + * Constructs a new selection outline post process + * @param name The name of the effect + * @param maskTexture The mask texture + * @param depthTexture The depth texture + * @param options The options for the post process + */ + public constructor(name: string, maskTexture: RenderTargetTexture, depthTexture: RenderTargetTexture, options: PostProcessOptions) { + const localOptions: PostProcessOptions = { + uniforms: ThinSelectionOutlinePostProcess.Uniforms, + samplers: ThinSelectionOutlinePostProcess.Samplers, + camera: maskTexture.activeCamera, // camera must be same as the one used to render the mask texture + ...options, + }; + + super(name, ThinSelectionOutlinePostProcess.FragmentUrl, { + effectWrapper: !options.effectWrapper ? new ThinSelectionOutlinePostProcess(name, options.engine, localOptions) : undefined, + ...localOptions, + }); + + this._maskTexture = maskTexture; + this._depthTexture = depthTexture; + this.onApplyObservable.add((effect: Effect) => { + if (!this._maskTexture) { + Logger.Warn("No mask texture set on SelectionOutlinePostProcess"); + return; + } + + this._effectWrapper.textureWidth = this.width; + this._effectWrapper.textureHeight = this.height; + + effect.setTexture("maskSampler", this._maskTexture); + effect.setTexture("depthSampler", this._depthTexture); + }); + } + + /** + * Gets a string identifying the name of the class + * @returns "SelectionOutlinePostProcess" string + */ + public override getClassName(): string { + return "SelectionOutlinePostProcess"; + } + + /** + * Sets the mask texture + * @param value The mask texture + */ + public set maskTexture(value: RenderTargetTexture) { + this._maskTexture = value; + } + + /** + * Sets the depth texture + * @param value The depth texture + */ + public set depthTexture(value: RenderTargetTexture) { + this._depthTexture = value; + } +} + +RegisterClass("BABYLON.SelectionOutlinePostProcess", SelectionOutlinePostProcess); diff --git a/packages/dev/core/src/PostProcesses/thinSelectionOutlinePostProcess.ts b/packages/dev/core/src/PostProcesses/thinSelectionOutlinePostProcess.ts new file mode 100644 index 00000000000..e6b5db8a391 --- /dev/null +++ b/packages/dev/core/src/PostProcesses/thinSelectionOutlinePostProcess.ts @@ -0,0 +1,90 @@ +import type { AbstractEngine } from "../Engines/abstractEngine"; +import { Engine } from "../Engines/engine"; +import type { EffectWrapperCreationOptions } from "../Materials/effectRenderer"; +import { EffectWrapper } from "../Materials/effectRenderer"; +import { Color3 } from "../Maths/math.color"; +import type { Nullable } from "../types"; + +/** + * Post process used to render a thin outline around selected objects + */ +export class ThinSelectionOutlinePostProcess extends EffectWrapper { + /** + * The fragment shader url + */ + public static readonly FragmentUrl = "selectionOutline"; + + /** + * The list of uniforms used by the effect + */ + public static readonly Uniforms = ["screenSize", "outlineColor", "outlineThickness", "occlusionStrength"]; + + /** + * The list of samplers used by the effect + */ + public static readonly Samplers = ["maskSampler", "depthSampler"]; + + protected override _gatherImports(useWebGPU: boolean, list: Promise[]): void { + if (useWebGPU) { + this._webGPUReady = true; + list.push(import("../ShadersWGSL/selectionOutline.fragment")); + } else { + list.push(import("../Shaders/selectionOutline.fragment")); + } + } + + /** + * Constructs a new thin selection outline post process + * @param name Name of the effect + * @param engine Engine to use to render the effect. If not provided, the last created engine will be used + * @param options Options to configure the effect + */ + public constructor(name: string, engine: Nullable = null, options?: EffectWrapperCreationOptions) { + super({ + ...options, + name, + engine: engine || Engine.LastCreatedEngine!, + useShaderStore: true, + useAsPostProcess: true, + fragmentShader: ThinSelectionOutlinePostProcess.FragmentUrl, + uniforms: ThinSelectionOutlinePostProcess.Uniforms, + samplers: ThinSelectionOutlinePostProcess.Samplers, + }); + } + + /** + * THe outline color + */ + public outlineColor: Color3 = new Color3(1, 0.5, 0); + + /** + * The thickness of the edges + */ + public outlineThickness: number = 2.0; + + /** + * The strength of the occlusion effect (default: 0.8) + */ + public occlusionStrength: number = 0.8; + + /** + * The width of the source texture + */ + public textureWidth: number = 0; + + /** + * The height of the source texture + */ + public textureHeight: number = 0; + + public override bind(noDefaultBindings = false): void { + super.bind(noDefaultBindings); + + const effect = this._drawWrapper.effect!; + + effect.setFloat2("screenSize", this.textureWidth, this.textureHeight); + effect.setColor3("outlineColor", this.outlineColor); + effect.setFloat("outlineThickness", this.outlineThickness); + effect.setFloat("occlusionStrength", this.occlusionStrength); + } +} From b30d8a9a545afcde4516a96b4ef4364af91e9171 Mon Sep 17 00:00:00 2001 From: noname0310 Date: Wed, 24 Dec 2025 16:19:44 +0900 Subject: [PATCH 08/17] Use serializeAsColor3 for outlineColor property --- .../dev/core/src/PostProcesses/selectionOutlinePostProcess.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/dev/core/src/PostProcesses/selectionOutlinePostProcess.ts b/packages/dev/core/src/PostProcesses/selectionOutlinePostProcess.ts index 0fd52d19da5..67d616ac8ff 100644 --- a/packages/dev/core/src/PostProcesses/selectionOutlinePostProcess.ts +++ b/packages/dev/core/src/PostProcesses/selectionOutlinePostProcess.ts @@ -1,7 +1,7 @@ import type { Effect } from "../Materials/effect"; import type { RenderTargetTexture } from "../Materials/Textures/renderTargetTexture"; import type { Color3 } from "../Maths/math.color"; -import { serialize } from "../Misc/decorators"; +import { serialize, serializeAsColor3 } from "../Misc/decorators"; import { Logger } from "../Misc/logger"; import { RegisterClass } from "../Misc/typeStore"; import type { Nullable } from "../types"; @@ -16,7 +16,7 @@ export class SelectionOutlinePostProcess extends PostProcess { /** * Gets or sets the outline color */ - @serialize() + @serializeAsColor3() public get outlineColor(): Color3 { return this._effectWrapper.outlineColor; } From bea95d79cd8f8f1ee0c0392ddc1af993b77e93cd Mon Sep 17 00:00:00 2001 From: noname0310 Date: Fri, 26 Dec 2025 17:32:32 +0900 Subject: [PATCH 09/17] introduce selection mask renderer --- .../src/Rendering/selectionMaskRenderer.ts | 339 ++++++++++++++++++ 1 file changed, 339 insertions(+) create mode 100644 packages/dev/core/src/Rendering/selectionMaskRenderer.ts diff --git a/packages/dev/core/src/Rendering/selectionMaskRenderer.ts b/packages/dev/core/src/Rendering/selectionMaskRenderer.ts new file mode 100644 index 00000000000..79eef206ac1 --- /dev/null +++ b/packages/dev/core/src/Rendering/selectionMaskRenderer.ts @@ -0,0 +1,339 @@ +import { Camera } from "../Cameras/camera"; +import { Constants } from "../Engines/constants"; +import type { Effect } from "../Materials/effect"; +import { ShaderLanguage } from "../Materials/shaderLanguage"; +import type { IShaderMaterialOptions } from "../Materials/shaderMaterial"; +import { ShaderMaterial } from "../Materials/shaderMaterial"; +import { RenderTargetTexture } from "../Materials/Textures/renderTargetTexture"; +import { Color4 } from "../Maths/math.color"; +import type { Matrix } from "../Maths/math.vector"; +import type { AbstractMesh } from "../Meshes/abstractMesh"; +import { VertexBuffer } from "../Meshes/buffer"; +import type { InstancedMesh } from "../Meshes/instancedMesh"; +import type { Mesh } from "../Meshes/mesh"; +import type { SubMesh } from "../Meshes/subMesh"; +import type { Scene } from "../scene"; +import type { Nullable } from "../types"; + +/** + * Selection material used to generate the selection mask + * + * Selection material use r and g channels to store the selection ID and depth information + */ +class SelectionMaterial extends ShaderMaterial { + private readonly _meshUniqueIdToSelectionId: number[]; + + /** + * Constructs a new selection mask material + * @param name The name of the material + * @param scene The scene the material belongs to + * @param shaderLanguage The shader language to use + * @param meshUniqueIdToSelectionId Mapping from mesh unique IDs to selection IDs + */ + public constructor(name: string, scene: Scene, shaderLanguage: ShaderLanguage, meshUniqueIdToSelectionId: number[]) { + const defines: string[] = []; + const options: Partial = { + attributes: [VertexBuffer.PositionKind, SelectionMaskRenderer.InstanceSelectionIdAttributeName], + uniforms: ["world", "viewProjection", "selectionId", "depthValues"], + needAlphaBlending: false, + defines: defines, + useClipPlane: null, + shaderLanguage: shaderLanguage, + extraInitializationsAsync: async () => { + if (this.shaderLanguage === ShaderLanguage.WGSL) { + await Promise.all([import("../ShadersWGSL/selection.fragment"), import("../ShadersWGSL/selection.vertex")]); + } else { + await Promise.all([import("../Shaders/selection.fragment"), import("../Shaders/selection.vertex")]); + } + }, + }; + super(name, scene, "selection", options, false); + + this._meshUniqueIdToSelectionId = meshUniqueIdToSelectionId; + } + + /** + * Binds the material to the mesh + * @param world The world matrix + * @param mesh The mesh to bind the material to + * @param effectOverride An optional effect override + * @param subMesh The submesh to bind the material to + */ + public override bind(world: Matrix, mesh?: AbstractMesh, effectOverride?: Nullable, subMesh?: SubMesh): void { + super.bind(world, mesh, effectOverride, subMesh); + if (!mesh) { + return; + } + + const storeEffectOnSubMeshes = subMesh && this._storeEffectOnSubMeshes; + const effect = effectOverride ?? (storeEffectOnSubMeshes ? subMesh.effect : this.getEffect()); + + if (!effect) { + return; + } + + if (!mesh.hasInstances && !mesh.isAnInstance && this._meshUniqueIdToSelectionId[mesh.uniqueId] !== undefined) { + const selectionId = this._meshUniqueIdToSelectionId[mesh.uniqueId]; + effect.setFloat("selectionId", selectionId); + } + + const engine = this.getScene().getEngine(); + + const camera = this.getScene().activeCamera; + let minZ: number = 1; + let maxZ: number = 10000; + if (camera) { + const cameraIsOrtho = camera.mode === Camera.ORTHOGRAPHIC_CAMERA; + + 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); + } +} + +/** + * Selection mask renderer + * + * Renders selected objects to a mask texture where r and g channels store selection ID and depth information + */ +export class SelectionMaskRenderer { + /** + * Name of the instance selection ID attribute + * @internal + */ + public static readonly InstanceSelectionIdAttributeName = "instanceSelectionId"; + + private readonly _name: string; + private readonly _scene: Scene; + private _maskTexture: Nullable; + private _isRttAddedToScene; + + private readonly _meshUniqueIdToSelectionId: number[] = []; + + private readonly _selectionMaterialCache: Nullable[] = new Array(9).fill(null); + private readonly _selection: AbstractMesh[] = []; + + private _nextSelectionId = 1; + + /** Shader language used by the generator */ + protected _shaderLanguage = ShaderLanguage.GLSL; + + /** + * Gets the shader language used in this generator. + */ + public get shaderLanguage(): ShaderLanguage { + return this._shaderLanguage; + } + + /** + * Constructs a new selection mask renderer + * @param name Name of the render target + * @param scene The scene the renderer belongs to + * @param camera The camera to be used to render the depth map (default: scene's active camera) + */ + public constructor(name: string, scene: Scene, camera: Nullable = null) { + this._name = name; + this._scene = scene; + + const engine = scene.getEngine(); + + if (engine.isWebGPU) { + this._shaderLanguage = ShaderLanguage.WGSL; + } + + this._maskTexture = new RenderTargetTexture(name, { width: engine.getRenderWidth(), height: engine.getRenderHeight() }, this._scene, { + type: Constants.TEXTURETYPE_HALF_FLOAT, + format: Constants.TEXTUREFORMAT_RG, + }); + this._maskTexture.wrapU = Constants.TEXTURE_CLAMP_ADDRESSMODE; + this._maskTexture.wrapV = Constants.TEXTURE_CLAMP_ADDRESSMODE; + this._maskTexture.refreshRate = 1; + this._maskTexture.renderParticles = false; + this._maskTexture.renderList = null; + this._maskTexture.noPrePassRenderer = true; + this._maskTexture.clearColor = new Color4(0, 1, 1, 1); + + this._maskTexture.activeCamera = camera; + this._maskTexture.ignoreCameraViewport = true; + this._maskTexture.useCameraPostProcesses = false; + + this._isRttAddedToScene = false; + // this._scene.customRenderTargets.push(this._maskTexture); + } + + private _clearSelectionMaterials(): void { + for (let i = 0; i < this._selectionMaterialCache.length; ++i) { + const material = this._selectionMaterialCache[i]; + if (material !== null) { + material.dispose(); + this._selectionMaterialCache[i] = null; + } + } + } + + private _getSelectionMaterial(scene: Scene, fillMode: number): SelectionMaterial { + if (fillMode < 0 || 8 < fillMode) { + fillMode = Constants.MATERIAL_TriangleFillMode; + } + + const cachedMaterial = this._selectionMaterialCache[fillMode]; + if (cachedMaterial) { + return cachedMaterial; + } + + const engine = scene.getEngine(); + + if (engine.isWebGPU) { + this._shaderLanguage = ShaderLanguage.WGSL; + } + + const newMaterial = new SelectionMaterial(this._name + "_selection_material", scene, this._shaderLanguage, this._meshUniqueIdToSelectionId); + newMaterial.fillMode = fillMode; + newMaterial.backFaceCulling = false; + + this._selectionMaterialCache[fillMode] = newMaterial; + return newMaterial; + } + + private _addRenderTargetToScene(): void { + if (this._isRttAddedToScene) { + return; + } + this._scene.customRenderTargets.push(this._maskTexture!); + this._isRttAddedToScene = true; + } + + private _removeRenderTargetFromScene(): void { + if (!this._isRttAddedToScene) { + return; + } + const index = this._scene.customRenderTargets.indexOf(this._maskTexture!); + if (index !== -1) { + this._scene.customRenderTargets.splice(index, 1); + } + this._isRttAddedToScene = false; + } + + /** + * Clears the current selection + * @param removeRenderTargetFromScene If true, removes the render target from the scene's custom render targets + */ + public clearSelection(removeRenderTargetFromScene = true): void { + if (this._selection.length === 0) { + return; + } + + for (let index = 0; index < this._selection.length; ++index) { + const mesh = this._selection[index]; + if (mesh.hasInstances) { + (mesh as Mesh).removeVerticesData(SelectionMaskRenderer.InstanceSelectionIdAttributeName); + } + if (this._maskTexture) { + this._maskTexture.setMaterialForRendering(mesh, undefined); + } + } + this._selection.length = 0; + this._meshUniqueIdToSelectionId.length = 0; + if (this._maskTexture) { + this._maskTexture.renderList = []; + } + + this._nextSelectionId = 1; + if (removeRenderTargetFromScene) { + this._removeRenderTargetFromScene(); + } + } + + /** + * Adds meshe or group of meshes to the current selection + * + * If a group of meshes is provided, they will outline as a single unit + * @param meshes Meshes to add to the selection + */ + public addSelection(meshes: (AbstractMesh | AbstractMesh[])[]): void { + if (meshes.length === 0) { + return; + } + + // prepare target texture + const maskTexture = this._maskTexture; + if (!maskTexture) { + return; // if disposed + } + + let nextId = this._nextSelectionId; + for (let groupIndex = 0; groupIndex < meshes.length; ++groupIndex) { + const meshOrGroup = meshes[groupIndex]; + const id = nextId; + nextId += 1; + + const group = Array.isArray(meshOrGroup) ? meshOrGroup : [meshOrGroup]; + for (let meshIndex = 0; meshIndex < group.length; ++meshIndex) { + const mesh = group[meshIndex]; + + const material = this._getSelectionMaterial(this._scene, mesh.material?.fillMode ?? Constants.MATERIAL_TriangleFillMode); + maskTexture.setMaterialForRendering(group, material); + this._selection.push(mesh); // add to render list + + if (mesh.hasInstances || mesh.isAnInstance) { + const sourceMesh = (mesh as InstancedMesh).sourceMesh ?? (mesh as Mesh); + + if (sourceMesh.instancedBuffers?.[SelectionMaskRenderer.InstanceSelectionIdAttributeName] === undefined) { + sourceMesh.registerInstancedBuffer(SelectionMaskRenderer.InstanceSelectionIdAttributeName, 1); + // todo: consider unregistering buffer on dispose + } + + mesh.instancedBuffers[SelectionMaskRenderer.InstanceSelectionIdAttributeName] = id; + } else { + this._meshUniqueIdToSelectionId[mesh.uniqueId] = id; + } + } + } + this._nextSelectionId = nextId; + + maskTexture.renderList = [...this._selection]; + this._addRenderTargetToScene(); + } + + /** + * Sets the current selection, replacing any previous selection + * + * If a group of meshes is provided, they will outline as a single unit + * @param meshes Meshes to set as the current selection + */ + public setSelection(meshes: (AbstractMesh | AbstractMesh[])[]): void { + this.clearSelection(false); + this.addSelection(meshes); + } + + /** + * Gets the mask texture. if renderer has been disposed, null is returned + * @returns The mask texture + */ + public getMaskTexture(): Nullable { + return this._maskTexture; + } + + /** + * Disposes the selection mask renderer + */ + public dispose(): void { + this.clearSelection(); + + if (this._maskTexture !== null) { + const index = this._scene.customRenderTargets.indexOf(this._maskTexture); + if (index !== -1) { + this._scene.customRenderTargets.splice(index, 1); + } + this._maskTexture.dispose(); + this._maskTexture = null; + } + this._clearSelectionMaterials(); + } +} From 224dd3c97b9882cecbfe210d366a454664e63459 Mon Sep 17 00:00:00 2001 From: noname0310 Date: Fri, 26 Dec 2025 17:33:33 +0900 Subject: [PATCH 10/17] add missing export --- packages/dev/core/src/Rendering/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/dev/core/src/Rendering/index.ts b/packages/dev/core/src/Rendering/index.ts index ff32ab94837..4e88374da91 100644 --- a/packages/dev/core/src/Rendering/index.ts +++ b/packages/dev/core/src/Rendering/index.ts @@ -13,6 +13,7 @@ export * from "./iblCdfGeneratorSceneComponent"; export * from "./IBLShadows/iblShadowsRenderPipeline"; export * from "./prePassRenderer"; export * from "./prePassRendererSceneComponent"; +export * from "./selectionMaskRenderer"; export * from "./subSurfaceSceneComponent"; export * from "./outlineRenderer"; export * from "./renderingGroup"; From e9622e39f17193de53b18ab4f96259eeef285f8f Mon Sep 17 00:00:00 2001 From: noname0310 Date: Fri, 26 Dec 2025 20:05:37 +0900 Subject: [PATCH 11/17] refactor into pipeline --- .../selectionOutlineRenderingPipeline.ts | 552 ++++++------------ .../selectionOutlinePostProcess.ts | 114 ---- .../src/Rendering/selectionMaskRenderer.ts | 66 +-- .../selectionOutlineConfiguration.ts | 23 + .../core/src/Shaders/selection.fragment.fx | 4 +- .../dev/core/src/Shaders/selection.vertex.fx | 10 +- .../src/ShadersWGSL/selection.fragment.fx | 4 +- .../core/src/ShadersWGSL/selection.vertex.fx | 10 +- 8 files changed, 255 insertions(+), 528 deletions(-) delete mode 100644 packages/dev/core/src/PostProcesses/selectionOutlinePostProcess.ts create mode 100644 packages/dev/core/src/Rendering/selectionOutlineConfiguration.ts diff --git a/packages/dev/core/src/PostProcesses/RenderPipeline/Pipelines/selectionOutlineRenderingPipeline.ts b/packages/dev/core/src/PostProcesses/RenderPipeline/Pipelines/selectionOutlineRenderingPipeline.ts index 3eb522b8359..26be5b1e09f 100644 --- a/packages/dev/core/src/PostProcesses/RenderPipeline/Pipelines/selectionOutlineRenderingPipeline.ts +++ b/packages/dev/core/src/PostProcesses/RenderPipeline/Pipelines/selectionOutlineRenderingPipeline.ts @@ -1,121 +1,23 @@ -import { Camera } from "../../../Cameras/camera"; -import type { AbstractEngine } from "../../../Engines/abstractEngine"; +import type { Camera } from "../../../Cameras/camera"; import { Constants } from "../../../Engines/constants"; -import type { Effect } from "../../../Materials/effect"; -import { ShaderLanguage } from "../../../Materials/shaderLanguage"; -import type { IShaderMaterialOptions } from "../../../Materials/shaderMaterial"; -import { ShaderMaterial } from "../../../Materials/shaderMaterial"; -import { RenderTargetTexture } from "../../../Materials/Textures/renderTargetTexture"; -import { Color3, Color4 } from "../../../Maths/math.color"; -import type { Matrix } from "../../../Maths/math.vector"; +import type { Color3 } from "../../../Maths/math.color"; import type { AbstractMesh } from "../../../Meshes/abstractMesh"; -import { VertexBuffer } from "../../../Meshes/buffer"; -import type { InstancedMesh } from "../../../Meshes/instancedMesh"; -import type { Mesh } from "../../../Meshes/mesh"; -import type { SubMesh } from "../../../Meshes/subMesh"; -import type { Observer } from "../../../Misc/observable"; -import type { DepthRenderer } from "../../../Rendering/depthRenderer"; import type { Scene } from "../../../scene"; import type { Nullable } from "../../../types"; -import "../../../Rendering/depthRendererSceneComponent"; -import { SelectionOutlinePostProcess } from "../../selectionOutlinePostProcess"; - -/** - * Selection material used to generate the selection mask - * - * Selection material use r and g channels to store the selection ID and depth information - */ -class SelectionMaterial extends ShaderMaterial { - private readonly _meshUniqueIdToSelectionId: number[]; - - /** - * Constructs a new selection mask material - * @param name The name of the material - * @param scene The scene the material belongs to - * @param shaderLanguage The shader language to use - * @param meshUniqueIdToSelectionId Mapping from mesh unique IDs to selection IDs - */ - public constructor(name: string, scene: Scene, shaderLanguage: ShaderLanguage, meshUniqueIdToSelectionId: number[]) { - const defines: string[] = []; - const options: Partial = { - attributes: [VertexBuffer.PositionKind, SelectionOutlineRenderingPipeline.InstanceSelectionIdAttributeName], - uniforms: ["world", "viewProjection", "selectionId", "depthValues"], - needAlphaBlending: false, - defines: defines, - useClipPlane: null, - shaderLanguage: shaderLanguage, - extraInitializationsAsync: async () => { - if (this.shaderLanguage === ShaderLanguage.WGSL) { - await Promise.all([import("../../../ShadersWGSL/selection.fragment"), import("../../../ShadersWGSL/selection.vertex")]); - } else { - await Promise.all([import("../../../Shaders/selection.fragment"), import("../../../Shaders/selection.vertex")]); - } - }, - }; - super(name, scene, "selection", options, false); - - this._meshUniqueIdToSelectionId = meshUniqueIdToSelectionId; - } - - /** - * Binds the material to the mesh - * @param world The world matrix - * @param mesh The mesh to bind the material to - * @param effectOverride An optional effect override - * @param subMesh The submesh to bind the material to - */ - public override bind(world: Matrix, mesh?: AbstractMesh, effectOverride?: Nullable, subMesh?: SubMesh): void { - super.bind(world, mesh, effectOverride, subMesh); - if (!mesh) { - return; - } - - const storeEffectOnSubMeshes = subMesh && this._storeEffectOnSubMeshes; - const effect = effectOverride ?? (storeEffectOnSubMeshes ? subMesh.effect : this.getEffect()); - - if (!effect) { - return; - } - - if (!mesh.hasInstances && !mesh.isAnInstance && this._meshUniqueIdToSelectionId[mesh.uniqueId] !== undefined) { - const selectionId = this._meshUniqueIdToSelectionId[mesh.uniqueId]; - effect.setFloat("selectionId", selectionId); - } - - const engine = this.getScene().getEngine(); - - const camera = this.getScene().activeCamera; - let minZ: number = 1; - let maxZ: number = 10000; - if (camera) { - const cameraIsOrtho = camera.mode === Camera.ORTHOGRAPHIC_CAMERA; - - 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); - } -} - -/** - * Options for the selection outline rendering pipeline - */ -export interface ISelectionOutlineRenderingPipelineOptions { - /** - * Color of the outline (default: (1, 0.5, 0) - orange) - */ - outlineColor?: Color3; - /** - * Number of samples for the final post process (default: 4) - */ - samples?: number; -} +import { PostProcessRenderPipeline } from "../postProcessRenderPipeline"; +import { serialize, serializeAsColor3 } from "../../../Misc/decorators"; +import { RegisterClass } from "../../../Misc/typeStore"; +import { SerializationHelper } from "../../../Misc/decorators.serialization"; +import { GeometryBufferRenderer } from "../../../Rendering/geometryBufferRenderer"; +import type { PrePassRenderer } from "../../../Rendering/prePassRenderer"; +import { SelectionMaskRenderer } from "../../../Rendering/selectionMaskRenderer"; +import { PostProcess } from "../../postProcess"; +import { ThinSelectionOutlinePostProcess } from "../../thinSelectionOutlinePostProcess"; +import type { Effect } from "../../../Materials/effect"; +import { SelectionOutlineConfiguration } from "../../../Rendering/selectionOutlineConfiguration"; +import type { ISize } from "../../../Maths/math.size"; +import { PostProcessRenderEffect } from "../postProcessRenderEffect"; /** * Selection outline rendering pipeline @@ -126,307 +28,244 @@ export interface ISelectionOutlineRenderingPipelineOptions { * 1. Render selected objects to a mask texture where r and g channels store selection ID and depth information * 2. Apply a post process that will use the mask texture to render outlines around selected objects */ -export class SelectionOutlineRenderingPipeline { +export class SelectionOutlineRenderingPipeline extends PostProcessRenderPipeline { /** - * Name of the instance selection ID attribute - * @internal + * The Selection Outline PostProcess effect id in the pipeline */ - public static readonly InstanceSelectionIdAttributeName = "instanceSelectionId"; + // eslint-disable-next-line @typescript-eslint/naming-convention + public SelectionOutlineEffect: string = "SelectionOutlineEffect"; - private readonly _name: string; - private readonly _camera: Camera; - private readonly _scene: Scene; - private readonly _depthRenderer: DepthRenderer; + @serialize("msaaSamples") + private _msaaSamples: number = 4; + /** + * Gets or sets the number of samples used for the outline post process + */ + public get msaaSamples(): number { + return this._msaaSamples; + } + public set msaaSamples(value: number) { + this._msaaSamples = value; + if (this._outlinePostProcess) { + this._outlinePostProcess.samples = value; + } + } - private _samples: number = 4; + private _forcedGeometryBuffer: Nullable = null; /** - * Gets or sets the number of samples used for the outline post process (default: 4) + * Force rendering the geometry through geometry buffer. */ - public get samples(): number { - return this._samples; + @serialize() + private _forceGeometryBuffer: boolean = false; + private get _geometryBufferRenderer(): Nullable { + if (!this._forceGeometryBuffer) { + return null; + } + return this._forcedGeometryBuffer ?? this._scene.geometryBufferRenderer; } - public set samples(value: number) { - this._samples = value; - if (this._outlineProcess) { - this._outlineProcess.samples = value; + private get _prePassRenderer(): Nullable { + if (this._forceGeometryBuffer) { + return null; } + return this._scene.prePassRenderer; } - private _outlineColor: Color3 = new Color3(1, 0.5, 0); /** * Gets or sets the outline color (default: (1, 0.5, 0) - orange) */ + @serializeAsColor3("outlineColor") public get outlineColor(): Color3 { - return this._outlineColor; + return this._thinOutlinePostProcess.outlineColor; } public set outlineColor(value: Color3) { - this._outlineColor = value; - if (this._outlineProcess) { - this._outlineProcess.outlineColor = value; - } + this._thinOutlinePostProcess.outlineColor = value; } - private _outlineThickness: number = 2.0; /** * Gets or sets the outline thickness (default: 2.0) */ + @serialize("outlineThickness") public get outlineThickness(): number { - return this._outlineThickness; + return this._thinOutlinePostProcess.outlineThickness; } public set outlineThickness(value: number) { - this._outlineThickness = value; - if (this._outlineProcess) { - this._outlineProcess.outlineThickness = value; - } + this._thinOutlinePostProcess.outlineThickness = value; } - private _occlusionStrength: number = 0.8; /** * Gets or sets the occlusion strength (default: 0.8) */ + @serialize("occlusionStrength") public get occlusionStrength(): number { - return this._occlusionStrength; + return this._thinOutlinePostProcess.occlusionStrength; } public set occlusionStrength(value: number) { - this._occlusionStrength = value; - if (this._outlineProcess) { - this._outlineProcess.occlusionStrength = value; - } + this._thinOutlinePostProcess.occlusionStrength = value; } - private readonly _meshUniqueIdToSelectionId: number[] = []; - - private readonly _selectionMaterialCache: Nullable[] = new Array(9).fill(null); - private readonly _selection: AbstractMesh[] = []; + private readonly _scene: Scene; + private readonly _textureType: number; - private _maskTexture: Nullable = null; - private _outlineProcess: Nullable = null; - private _isOutlineProcessAttached: boolean = false; + private _maskRenderer: SelectionMaskRenderer; + private _thinOutlinePostProcess: ThinSelectionOutlinePostProcess; + private _outlinePostProcess: PostProcess; - private _resizeObserver: Nullable> = null; + /** + * Constructs a new selection outline rendering pipeline + * @param name The rendering pipeline name + * @param scene The scene linked to this pipeline + * @param cameras The array of cameras that the rendering pipeline will be attached to + * @param forceGeometryBuffer Set to true if you want to use the legacy geometry buffer renderer. You can also pass an existing instance of GeometryBufferRenderer if you want to use your own geometry buffer renderer. + * @param textureType The type of texture where the scene will be rendered (default: Constants.TEXTURETYPE_UNSIGNED_BYTE) + * + */ + public constructor( + name: string, + scene: Scene, + cameras?: Camera[], + forceGeometryBuffer: boolean | GeometryBufferRenderer = false, + textureType = Constants.TEXTURETYPE_UNSIGNED_BYTE + ) { + super(scene.getEngine(), name); + + this._scene = scene; + this._textureType = textureType; + if (forceGeometryBuffer instanceof GeometryBufferRenderer) { + this._forceGeometryBuffer = true; + this._forcedGeometryBuffer = forceGeometryBuffer; + } else { + this._forceGeometryBuffer = forceGeometryBuffer; + } - private _nextSelectionId = 1; + // Set up assets + if (this._forceGeometryBuffer) { + if (!this._forcedGeometryBuffer) { + scene.enableGeometryBufferRenderer(); + } + } else { + scene.enablePrePassRenderer(); + } - /** Shader language used by the generator */ - protected _shaderLanguage = ShaderLanguage.GLSL; + this._maskRenderer = new SelectionMaskRenderer(name, scene); + this._thinOutlinePostProcess = new ThinSelectionOutlinePostProcess(name, scene.getEngine()); + + this._outlinePostProcess = this._createSelectionOutlinePostProcess(this._textureType); + + this.addEffect( + new PostProcessRenderEffect( + scene.getEngine(), + this.SelectionOutlineEffect, + () => { + return this._outlinePostProcess; + }, + true + ) + ); + + scene.postProcessRenderPipelineManager.addPipeline(this); + if (cameras) { + scene.postProcessRenderPipelineManager.attachCamerasToRenderPipeline(name, cameras); + } + } /** - * Gets the shader language used in this generator. + * Get the class name + * @returns "SelectionOutlineRenderingPipeline" */ - public get shaderLanguage(): ShaderLanguage { - return this._shaderLanguage; + public override getClassName(): string { + return "SelectionOutlineRenderingPipeline"; } /** - * @param name name of the process - * @param camera camera used for post process - * @param options options for the outline renderer - * + * Removes the internal pipeline assets and detaches the pipeline from the scene cameras + * @param disableGeometryBufferRenderer Set to true if you want to disable the Geometry Buffer renderer */ - public constructor(name: string, camera: Camera, options: ISelectionOutlineRenderingPipelineOptions = {}) { - this._name = name; - this._camera = camera; - this._scene = camera.getScene(); - - this._depthRenderer = this._scene.enableDepthRenderer(camera); + public override dispose(disableGeometryBufferRenderer: boolean = false): void { + this.clearSelection(); - if (options.outlineColor) { - this._outlineColor = options.outlineColor; - } - if (options.samples) { - this._samples = options.samples; + for (let i = 0; i < this._cameras.length; i++) { + const camera = this._cameras[i]; + this._outlinePostProcess?.dispose(camera); } - const engine = this._scene.getEngine(); - - if (engine.isWebGPU) { - this._shaderLanguage = ShaderLanguage.WGSL; + if (disableGeometryBufferRenderer && !this._forcedGeometryBuffer) { + this._scene.disableGeometryBufferRenderer(); } - // handle resize - this._resizeObserver = engine.onResizeObservable.add(this._resizeBuffer, undefined, undefined, this); - } + this._scene.postProcessRenderPipelineManager.detachCamerasFromRenderPipeline(this._name, this._scene.cameras); + this._scene.postProcessRenderPipelineManager.removePipeline(this._name); - private _resizeBuffer(): void { - const engine = this._scene.getEngine(); - const width = engine.getRenderWidth(); - const height = engine.getRenderHeight(); - - if (this._maskTexture !== null) { - const size = this._maskTexture.getSize(); - if (size.width !== width || size.height !== height) { - this._maskTexture.resize({ width: width, height: height }); - } - } + this._thinOutlinePostProcess.dispose(); - if (this._depthRenderer !== null) { - const depthMap = this._depthRenderer.getDepthMap(); - const depthSize = depthMap.getSize(); - if (depthSize.width !== width || depthSize.height !== height) { - depthMap.resize({ width: width, height: height }); - } - } + super.dispose(); } - private _createRenderTargetTexture(): RenderTargetTexture { - if (this._maskTexture) { - return this._maskTexture; - } - + private _getTextureSize(): ISize { const engine = this._scene.getEngine(); + const prePassRenderer = this._prePassRenderer; - this._maskTexture = new RenderTargetTexture(this._name + "_mask", { width: engine.getRenderWidth(), height: engine.getRenderHeight() }, this._scene, { - type: Constants.TEXTURETYPE_HALF_FLOAT, - format: Constants.TEXTUREFORMAT_RG, - }); - this._maskTexture.noPrePassRenderer = true; - this._maskTexture.clearColor = new Color4(0, 1, 1, 1); - this._maskTexture.wrapU = Constants.TEXTURE_CLAMP_ADDRESSMODE; - this._maskTexture.wrapV = Constants.TEXTURE_CLAMP_ADDRESSMODE; - - this._scene.customRenderTargets.push(this._maskTexture); + let textureSize: ISize = { width: engine.getRenderWidth(), height: engine.getRenderHeight() }; - return this._maskTexture; - } + if (prePassRenderer && this._scene.activeCamera?._getFirstPostProcess() === this._outlinePostProcess) { + const renderTarget = prePassRenderer.getRenderTarget(); - private _clearSelectionMaterials(): void { - for (let i = 0; i < this._selectionMaterialCache.length; ++i) { - const material = this._selectionMaterialCache[i]; - if (material !== null) { - material.dispose(); - this._selectionMaterialCache[i] = null; + if (renderTarget && renderTarget.textures) { + textureSize = renderTarget.textures[prePassRenderer.getIndex(Constants.PREPASS_COLOR_TEXTURE_TYPE)].getSize(); } - } - } - - private _getSelectionMaterial(scene: Scene, fillMode: number): SelectionMaterial { - if (fillMode < 0 || 8 < fillMode) { - fillMode = Constants.MATERIAL_TriangleFillMode; - } - - const cachedMaterial = this._selectionMaterialCache[fillMode]; - if (cachedMaterial) { - return cachedMaterial; - } - - const engine = scene.getEngine(); - - if (engine.isWebGPU) { - this._shaderLanguage = ShaderLanguage.WGSL; + } else if (this._outlinePostProcess!.inputTexture) { + textureSize.width = this._outlinePostProcess!.inputTexture.width; + textureSize.height = this._outlinePostProcess!.inputTexture.height; } - const newMaterial = new SelectionMaterial(this._name + "_selection_material", scene, this._shaderLanguage, this._meshUniqueIdToSelectionId); - newMaterial.fillMode = fillMode; - newMaterial.backFaceCulling = false; - - this._selectionMaterialCache[fillMode] = newMaterial; - return newMaterial; + return textureSize; } - private _prepareSelectionOutlinePostProcess(): void { - if (this._outlineProcess) { - return; + private _createSelectionOutlinePostProcess(textureType: number): PostProcess { + const outlinePostProcess = new PostProcess("selectionOutline", ThinSelectionOutlinePostProcess.FragmentUrl, { + engine: this._scene.getEngine(), + textureType, + effectWrapper: this._thinOutlinePostProcess, + }); + if (!this._forceGeometryBuffer) { + outlinePostProcess._prePassEffectConfiguration = new SelectionOutlineConfiguration(); } + outlinePostProcess.samples = this._msaaSamples; + outlinePostProcess.onApplyObservable.add((effect: Effect) => { + if (this._geometryBufferRenderer) { + effect.setTexture("depthSampler", this._geometryBufferRenderer.getGBuffer().textures[0]); + } else if (this._prePassRenderer) { + effect.setTexture("depthSampler", this._prePassRenderer.getRenderTarget().textures[this._prePassRenderer.getIndex(Constants.PREPASS_DEPTH_TEXTURE_TYPE)]); + } - if (!this._maskTexture) { - throw new Error("Mask texture not created before preparing outline post process"); - } + const textureSize = this._getTextureSize(); + this._thinOutlinePostProcess.textureWidth = textureSize.width; + this._thinOutlinePostProcess.textureHeight = textureSize.height; - this._outlineProcess = new SelectionOutlinePostProcess(this._name + "_outline", this._maskTexture, this._depthRenderer.getDepthMap(), { - camera: this._camera, - engine: this._scene.getEngine(), + effect.setTexture("maskSampler", this._maskRenderer.getMaskTexture()); }); - this._outlineProcess.outlineColor = this._outlineColor; - this._outlineProcess.outlineThickness = this._outlineThickness; - this._outlineProcess.occlusionStrength = this._occlusionStrength; - this._outlineProcess.samples = this._samples; - this._isOutlineProcessAttached = true; + return outlinePostProcess; } /** * Clears the current selection */ public clearSelection(): void { - if (this._selection.length === 0) { - return; - } - - for (let index = 0; index < this._selection.length; ++index) { - const mesh = this._selection[index]; - if (mesh.hasInstances) { - (mesh as Mesh).removeVerticesData(SelectionOutlineRenderingPipeline.InstanceSelectionIdAttributeName); - } - if (this._maskTexture) { - this._maskTexture.setMaterialForRendering(mesh, undefined); - } - } - this._selection.length = 0; - this._meshUniqueIdToSelectionId.length = 0; - if (this._maskTexture) { - this._maskTexture.renderList = []; - } - - this._nextSelectionId = 1; + this._maskRenderer.clearSelection(); - if (this._outlineProcess && this._isOutlineProcessAttached) { - this._camera.detachPostProcess(this._outlineProcess); - this._isOutlineProcessAttached = false; - } + // if (this._outlinePostProcess && this._isOutlineProcessAttached) { + // this._camera.detachPostProcess(this._outlinePostProcess); + // this._isOutlineProcessAttached = false; + // } } /** * Adds meshe or group of meshes to the current selection - * + * * If a group of meshes is provided, they will outline as a single unit * @param meshes Meshes to add to the selection */ public addSelection(meshes: (AbstractMesh | AbstractMesh[])[]): void { - if (meshes.length === 0) { - return; - } - - // prepare target texture - const maskTexture = this._createRenderTargetTexture(); - - let nextId = this._nextSelectionId; - for (let groupIndex = 0; groupIndex < meshes.length; ++groupIndex) { - const meshOrGroup = meshes[groupIndex]; - const id = nextId; - nextId += 1; - - const group = Array.isArray(meshOrGroup) ? meshOrGroup : [meshOrGroup]; - for (let meshIndex = 0; meshIndex < group.length; ++meshIndex) { - const mesh = group[meshIndex]; - - const material = this._getSelectionMaterial(this._scene, mesh.material?.fillMode ?? Constants.MATERIAL_TriangleFillMode); - maskTexture.setMaterialForRendering(group, material); - this._selection.push(mesh); // add to render list - - if (mesh.hasInstances || mesh.isAnInstance) { - const sourceMesh = (mesh as InstancedMesh).sourceMesh ?? (mesh as Mesh); - - if (sourceMesh.instancedBuffers?.[SelectionOutlineRenderingPipeline.InstanceSelectionIdAttributeName] === undefined) { - sourceMesh.registerInstancedBuffer(SelectionOutlineRenderingPipeline.InstanceSelectionIdAttributeName, 1); - // todo: consider unregistering buffer on dispose - } - - mesh.instancedBuffers[SelectionOutlineRenderingPipeline.InstanceSelectionIdAttributeName] = id; - } else { - this._meshUniqueIdToSelectionId[mesh.uniqueId] = id; - } - } - } - this._nextSelectionId = nextId; - - maskTexture.renderList = [...this._selection]; - - // set up outline post process - this._prepareSelectionOutlinePostProcess(); - if (!this._isOutlineProcessAttached) { - this._camera.attachPostProcess(this._outlineProcess!); - this._isOutlineProcessAttached = true; - } + this._maskRenderer.addSelection(meshes); } /** @@ -436,37 +275,34 @@ export class SelectionOutlineRenderingPipeline { * @param meshes Meshes to set as the current selection */ public setSelection(meshes: (AbstractMesh | AbstractMesh[])[]): void { - this.clearSelection(); - this.addSelection(meshes); + this._maskRenderer.setSelection(meshes); } - /** - * Disposes the rendering pipeline + * Serialize the rendering pipeline (Used when exporting) + * @returns the serialized object */ - public dispose(): void { - this.clearSelection(); - - if (this._maskTexture !== null) { - const index = this._scene.customRenderTargets.indexOf(this._maskTexture); - if (index !== -1) { - this._scene.customRenderTargets.splice(index, 1); - } - this._maskTexture.dispose(); - this._maskTexture = null; - } - this._clearSelectionMaterials(); + public serialize(): any { + const serializationObject = SerializationHelper.Serialize(this); + serializationObject.customType = "SelectionOutlineRenderingPipeline"; - if (this._outlineProcess) { - if (this._isOutlineProcessAttached) { - this._camera.detachPostProcess(this._outlineProcess); - this._isOutlineProcessAttached = false; - } - } - - this._outlineProcess?.dispose(); - this._outlineProcess = null; + return serializationObject; + } - this._resizeObserver?.remove(); - this._resizeObserver = null; + /** + * Parse the serialized pipeline + * @param source Source pipeline. + * @param scene The scene to load the pipeline to. + * @param rootUrl The URL of the serialized pipeline. + * @returns An instantiated pipeline from the serialized object. + */ + public static Parse(source: any, scene: Scene, rootUrl: string): SelectionOutlineRenderingPipeline { + return SerializationHelper.Parse( + () => new SelectionOutlineRenderingPipeline(source._name, scene, undefined, source._forceGeometryBuffer, source._textureType), + source, + scene, + rootUrl + ); } } + +RegisterClass("BABYLON.SelectionOutlineRenderingPipeline", SelectionOutlineRenderingPipeline); diff --git a/packages/dev/core/src/PostProcesses/selectionOutlinePostProcess.ts b/packages/dev/core/src/PostProcesses/selectionOutlinePostProcess.ts deleted file mode 100644 index 67d616ac8ff..00000000000 --- a/packages/dev/core/src/PostProcesses/selectionOutlinePostProcess.ts +++ /dev/null @@ -1,114 +0,0 @@ -import type { Effect } from "../Materials/effect"; -import type { RenderTargetTexture } from "../Materials/Textures/renderTargetTexture"; -import type { Color3 } from "../Maths/math.color"; -import { serialize, serializeAsColor3 } from "../Misc/decorators"; -import { Logger } from "../Misc/logger"; -import { RegisterClass } from "../Misc/typeStore"; -import type { Nullable } from "../types"; -import type { PostProcessOptions } from "./postProcess"; -import { PostProcess } from "./postProcess"; -import { ThinSelectionOutlinePostProcess } from "./thinSelectionOutlinePostProcess"; - -/** - * Post process used to render an outline around selected objects - */ -export class SelectionOutlinePostProcess extends PostProcess { - /** - * Gets or sets the outline color - */ - @serializeAsColor3() - public get outlineColor(): Color3 { - return this._effectWrapper.outlineColor; - } - public set outlineColor(value: Color3) { - this._effectWrapper.outlineColor = value; - } - - /** - * Gets or sets the outline thickness - */ - @serialize() - public get outlineThickness(): number { - return this._effectWrapper.outlineThickness; - } - public set outlineThickness(value: number) { - this._effectWrapper.outlineThickness = value; - } - - /** - * Gets or sets the occlusion strength - */ - @serialize() - public get occlusionStrength(): number { - return this._effectWrapper.occlusionStrength; - } - public set occlusionStrength(value: number) { - this._effectWrapper.occlusionStrength = value; - } - - declare protected _effectWrapper: ThinSelectionOutlinePostProcess; - private _maskTexture: Nullable = null; - private _depthTexture: Nullable = null; - - /** - * Constructs a new selection outline post process - * @param name The name of the effect - * @param maskTexture The mask texture - * @param depthTexture The depth texture - * @param options The options for the post process - */ - public constructor(name: string, maskTexture: RenderTargetTexture, depthTexture: RenderTargetTexture, options: PostProcessOptions) { - const localOptions: PostProcessOptions = { - uniforms: ThinSelectionOutlinePostProcess.Uniforms, - samplers: ThinSelectionOutlinePostProcess.Samplers, - camera: maskTexture.activeCamera, // camera must be same as the one used to render the mask texture - ...options, - }; - - super(name, ThinSelectionOutlinePostProcess.FragmentUrl, { - effectWrapper: !options.effectWrapper ? new ThinSelectionOutlinePostProcess(name, options.engine, localOptions) : undefined, - ...localOptions, - }); - - this._maskTexture = maskTexture; - this._depthTexture = depthTexture; - this.onApplyObservable.add((effect: Effect) => { - if (!this._maskTexture) { - Logger.Warn("No mask texture set on SelectionOutlinePostProcess"); - return; - } - - this._effectWrapper.textureWidth = this.width; - this._effectWrapper.textureHeight = this.height; - - effect.setTexture("maskSampler", this._maskTexture); - effect.setTexture("depthSampler", this._depthTexture); - }); - } - - /** - * Gets a string identifying the name of the class - * @returns "SelectionOutlinePostProcess" string - */ - public override getClassName(): string { - return "SelectionOutlinePostProcess"; - } - - /** - * Sets the mask texture - * @param value The mask texture - */ - public set maskTexture(value: RenderTargetTexture) { - this._maskTexture = value; - } - - /** - * Sets the depth texture - * @param value The depth texture - */ - public set depthTexture(value: RenderTargetTexture) { - this._depthTexture = value; - } -} - -RegisterClass("BABYLON.SelectionOutlinePostProcess", SelectionOutlinePostProcess); diff --git a/packages/dev/core/src/Rendering/selectionMaskRenderer.ts b/packages/dev/core/src/Rendering/selectionMaskRenderer.ts index 79eef206ac1..bb9da8c0d91 100644 --- a/packages/dev/core/src/Rendering/selectionMaskRenderer.ts +++ b/packages/dev/core/src/Rendering/selectionMaskRenderer.ts @@ -1,4 +1,5 @@ -import { Camera } from "../Cameras/camera"; +import type { Camera } from "../Cameras/camera"; +import type { AbstractEngine } from "../Engines/abstractEngine"; import { Constants } from "../Engines/constants"; import type { Effect } from "../Materials/effect"; import { ShaderLanguage } from "../Materials/shaderLanguage"; @@ -12,6 +13,7 @@ import { VertexBuffer } from "../Meshes/buffer"; import type { InstancedMesh } from "../Meshes/instancedMesh"; import type { Mesh } from "../Meshes/mesh"; import type { SubMesh } from "../Meshes/subMesh"; +import type { Observer } from "../Misc/observable"; import type { Scene } from "../scene"; import type { Nullable } from "../types"; @@ -34,7 +36,7 @@ class SelectionMaterial extends ShaderMaterial { const defines: string[] = []; const options: Partial = { attributes: [VertexBuffer.PositionKind, SelectionMaskRenderer.InstanceSelectionIdAttributeName], - uniforms: ["world", "viewProjection", "selectionId", "depthValues"], + uniforms: ["world", "viewProjection", "view", "selectionId"], needAlphaBlending: false, defines: defines, useClipPlane: null, @@ -76,24 +78,6 @@ class SelectionMaterial extends ShaderMaterial { const selectionId = this._meshUniqueIdToSelectionId[mesh.uniqueId]; effect.setFloat("selectionId", selectionId); } - - const engine = this.getScene().getEngine(); - - const camera = this.getScene().activeCamera; - let minZ: number = 1; - let maxZ: number = 10000; - if (camera) { - const cameraIsOrtho = camera.mode === Camera.ORTHOGRAPHIC_CAMERA; - - 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); } } @@ -114,6 +98,8 @@ export class SelectionMaskRenderer { private _maskTexture: Nullable; private _isRttAddedToScene; + private _resizeObserver: Nullable>; + private readonly _meshUniqueIdToSelectionId: number[] = []; private readonly _selectionMaterialCache: Nullable[] = new Array(9).fill(null); @@ -157,7 +143,7 @@ export class SelectionMaskRenderer { this._maskTexture.renderParticles = false; this._maskTexture.renderList = null; this._maskTexture.noPrePassRenderer = true; - this._maskTexture.clearColor = new Color4(0, 1, 1, 1); + this._maskTexture.clearColor = new Color4(0, 0, 1, 1); this._maskTexture.activeCamera = camera; this._maskTexture.ignoreCameraViewport = true; @@ -165,6 +151,27 @@ export class SelectionMaskRenderer { this._isRttAddedToScene = false; // this._scene.customRenderTargets.push(this._maskTexture); + + this._resizeObserver = engine.onResizeObservable.add(() => { + this._maskTexture?.resize({ width: engine.getRenderWidth(), height: engine.getRenderHeight() }); + }); + } + + /** + * Disposes the selection mask renderer + */ + public dispose(): void { + this.clearSelection(); + + if (this._maskTexture !== null) { + this._removeRenderTargetFromScene(); + this._maskTexture.dispose(); + this._maskTexture = null; + } + this._clearSelectionMaterials(); + + this._resizeObserver?.remove(); + this._resizeObserver = null; } private _clearSelectionMaterials(): void { @@ -319,21 +326,4 @@ export class SelectionMaskRenderer { public getMaskTexture(): Nullable { return this._maskTexture; } - - /** - * Disposes the selection mask renderer - */ - public dispose(): void { - this.clearSelection(); - - if (this._maskTexture !== null) { - const index = this._scene.customRenderTargets.indexOf(this._maskTexture); - if (index !== -1) { - this._scene.customRenderTargets.splice(index, 1); - } - this._maskTexture.dispose(); - this._maskTexture = null; - } - this._clearSelectionMaterials(); - } } diff --git a/packages/dev/core/src/Rendering/selectionOutlineConfiguration.ts b/packages/dev/core/src/Rendering/selectionOutlineConfiguration.ts new file mode 100644 index 00000000000..4032bf2c2f1 --- /dev/null +++ b/packages/dev/core/src/Rendering/selectionOutlineConfiguration.ts @@ -0,0 +1,23 @@ +import { Constants } from "../Engines/constants"; +import type { PrePassEffectConfiguration } from "./prePassEffectConfiguration"; + +/** + * Contains all parameters needed for the prepass to perform + * selection outline rendering + */ +export class SelectionOutlineConfiguration implements PrePassEffectConfiguration { + /** + * Is selection outline enabled + */ + public enabled = false; + + /** + * Name of the configuration + */ + public name = "selectionOutline"; + + /** + * Textures that should be present in the MRT for this effect to work + */ + public readonly texturesRequired: number[] = [Constants.PREPASS_DEPTH_TEXTURE_TYPE]; +} diff --git a/packages/dev/core/src/Shaders/selection.fragment.fx b/packages/dev/core/src/Shaders/selection.fragment.fx index f45237dac07..3858df07fd7 100644 --- a/packages/dev/core/src/Shaders/selection.fragment.fx +++ b/packages/dev/core/src/Shaders/selection.fragment.fx @@ -3,7 +3,7 @@ flat varying float vSelectionId; #else uniform float selectionId; #endif -varying float vDepthMetric; +varying float vViewPosZ; void main(void) { #if defined(INSTANCES) @@ -12,5 +12,5 @@ void main(void) { float id = selectionId; #endif - gl_FragColor = vec4(id, vDepthMetric, 0.0, 1.0); + gl_FragColor = vec4(id, vViewPosZ, 0.0, 1.0); } diff --git a/packages/dev/core/src/Shaders/selection.vertex.fx b/packages/dev/core/src/Shaders/selection.vertex.fx index 8a0f9954412..b0b180184b0 100644 --- a/packages/dev/core/src/Shaders/selection.vertex.fx +++ b/packages/dev/core/src/Shaders/selection.vertex.fx @@ -13,13 +13,13 @@ attribute float instanceSelectionId; #include uniform mat4 viewProjection; -uniform vec2 depthValues; +uniform mat4 view; // Output #if defined(INSTANCES) flat varying float vSelectionId; #endif -varying float vDepthMetric; +varying float vViewPosZ; void main(void) { @@ -31,11 +31,7 @@ void main(void) { vec4 worldPos = finalWorld * vec4(position, 1.0); gl_Position = viewProjection * worldPos; - #ifdef USE_REVERSE_DEPTHBUFFER - vDepthMetric = ((-gl_Position.z + depthValues.x) / (depthValues.y)); - #else - vDepthMetric = ((gl_Position.z + depthValues.x) / (depthValues.y)); - #endif + vViewPosZ = (view * worldPos).z; #if defined(INSTANCES) vSelectionId = instanceSelectionId; diff --git a/packages/dev/core/src/ShadersWGSL/selection.fragment.fx b/packages/dev/core/src/ShadersWGSL/selection.fragment.fx index 5276f542115..f319000a9c9 100644 --- a/packages/dev/core/src/ShadersWGSL/selection.fragment.fx +++ b/packages/dev/core/src/ShadersWGSL/selection.fragment.fx @@ -3,7 +3,7 @@ flat varying vSelectionId: f32; #else uniform selectionId: f32; #endif -varying vDepthMetric: f32; +varying vViewPosZ: f32; @fragment fn main(input: FragmentInputs) -> FragmentOutputs { @@ -13,5 +13,5 @@ fn main(input: FragmentInputs) -> FragmentOutputs { var id: f32 = uniforms.selectionId; #endif - fragmentOutputs.color = vec4(id, input.vDepthMetric, 0.0, 1.0); + fragmentOutputs.color = vec4(id, input.vViewPosZ, 0.0, 1.0); } diff --git a/packages/dev/core/src/ShadersWGSL/selection.vertex.fx b/packages/dev/core/src/ShadersWGSL/selection.vertex.fx index 9148569d341..72d1bdebb30 100644 --- a/packages/dev/core/src/ShadersWGSL/selection.vertex.fx +++ b/packages/dev/core/src/ShadersWGSL/selection.vertex.fx @@ -13,13 +13,13 @@ attribute instanceSelectionId: f32; #include uniform viewProjection: mat4x4f; -uniform depthValues: vec2f; +uniform view: mat4x4f; // Output #if defined(INSTANCES) flat varying vSelectionId: f32; #endif -varying vDepthMetric: f32; +varying vViewPosZ: f32; @vertex fn main(input: VertexInputs) -> FragmentInputs { @@ -32,11 +32,7 @@ fn main(input: VertexInputs) -> FragmentInputs { var worldPos: vec4f = finalWorld * vec4f(input.position, 1.0); vertexOutputs.position = uniforms.viewProjection * worldPos; - #ifdef USE_REVERSE_DEPTHBUFFER - vertexOutputs.vDepthMetric = ((-vertexOutputs.position.z + uniforms.depthValues.x) / (uniforms.depthValues.y)); - #else - vertexOutputs.vDepthMetric = ((vertexOutputs.position.z + uniforms.depthValues.x) / (uniforms.depthValues.y)); - #endif + vertexOutputs.vViewPosZ = (uniforms.view * worldPos).z; #if defined(INSTANCES) vertexOutputs.vSelectionId = input.instanceSelectionId; From e7d570cfad3c319bf26ff058193860f782d01834 Mon Sep 17 00:00:00 2001 From: noname0310 Date: Fri, 26 Dec 2025 20:17:13 +0900 Subject: [PATCH 12/17] Update index.ts --- packages/dev/core/src/PostProcesses/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/dev/core/src/PostProcesses/index.ts b/packages/dev/core/src/PostProcesses/index.ts index d8fc7bd6dda..9e70b7e263f 100644 --- a/packages/dev/core/src/PostProcesses/index.ts +++ b/packages/dev/core/src/PostProcesses/index.ts @@ -24,7 +24,6 @@ export * from "./postProcess"; export * from "./postProcessManager"; export * from "./refractionPostProcess"; export * from "./RenderPipeline/index"; -export * from "./selectionOutlinePostProcess"; export * from "./sharpenPostProcess"; export * from "./stereoscopicInterlacePostProcess"; export * from "./tonemapPostProcess"; From 36b4cd914640155846c22682e1a40d4231af232e Mon Sep 17 00:00:00 2001 From: noname0310 Date: Fri, 26 Dec 2025 21:18:51 +0900 Subject: [PATCH 13/17] change occlusionThreshold in selection outline shaders Raised the occlusionThreshold constant from 0.0000001 to 1.0000001 in both GLSL and WGSL selection outline fragment shaders. --- packages/dev/core/src/Shaders/selectionOutline.fragment.fx | 2 +- packages/dev/core/src/ShadersWGSL/selectionOutline.fragment.fx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/dev/core/src/Shaders/selectionOutline.fragment.fx b/packages/dev/core/src/Shaders/selectionOutline.fragment.fx index 08779a07a4d..9b1d2ae15a9 100644 --- a/packages/dev/core/src/Shaders/selectionOutline.fragment.fx +++ b/packages/dev/core/src/Shaders/selectionOutline.fragment.fx @@ -42,7 +42,7 @@ void main(void) float depthY = texture2D(depthSampler, vUV + vec2(0.0, sampleOffset.y)).r; float depthXY = texture2D(depthSampler, vUV + sampleOffset).r; - const float occlusionThreshold = 0.0000001; + const float occlusionThreshold = 1.0000001; float occlusionCenter = step(occlusionThreshold, abs(centerMask.g - depthCenter)); float occlusionX = step(occlusionThreshold, abs(maskX.g - depthX)); float occlusionY = step(occlusionThreshold, abs(maskY.g - depthY)); diff --git a/packages/dev/core/src/ShadersWGSL/selectionOutline.fragment.fx b/packages/dev/core/src/ShadersWGSL/selectionOutline.fragment.fx index 6fa7c45988f..7a4e846a524 100644 --- a/packages/dev/core/src/ShadersWGSL/selectionOutline.fragment.fx +++ b/packages/dev/core/src/ShadersWGSL/selectionOutline.fragment.fx @@ -45,7 +45,7 @@ fn main(input: FragmentInputs) -> FragmentOutputs { var depthY: f32 = textureSampleLevel(depthSampler, depthSamplerSampler, input.vUV + vec2f(0.0, sampleOffset.y), 0.0).r; var depthXY: f32 = textureSampleLevel(depthSampler, depthSamplerSampler, input.vUV + sampleOffset, 0.0).r; - const occlusionThreshold: f32 = 0.0000001; + const occlusionThreshold: f32 = 1.0000001; var occlusionCenter: f32 = step(occlusionThreshold, abs(centerMask.g - depthCenter)); var occlusionX: f32 = step(occlusionThreshold, abs(maskX.g - depthX)); var occlusionY: f32 = step(occlusionThreshold, abs(maskY.g - depthY)); From 92a8050982c03e06903b22e3ad8cc3caa46e0abb Mon Sep 17 00:00:00 2001 From: noname0310 Date: Sun, 28 Dec 2025 18:44:43 +0900 Subject: [PATCH 14/17] Add custom code injection points to selection shaders --- packages/dev/core/src/Shaders/selection.fragment.fx | 7 +++++++ packages/dev/core/src/Shaders/selection.vertex.fx | 6 ++++++ .../dev/core/src/Shaders/selectionOutline.fragment.fx | 8 ++++++-- packages/dev/core/src/ShadersWGSL/selection.fragment.fx | 7 +++++++ packages/dev/core/src/ShadersWGSL/selection.vertex.fx | 6 ++++++ .../dev/core/src/ShadersWGSL/selectionOutline.fragment.fx | 5 +++++ 6 files changed, 37 insertions(+), 2 deletions(-) diff --git a/packages/dev/core/src/Shaders/selection.fragment.fx b/packages/dev/core/src/Shaders/selection.fragment.fx index 3858df07fd7..9128aeea5fb 100644 --- a/packages/dev/core/src/Shaders/selection.fragment.fx +++ b/packages/dev/core/src/Shaders/selection.fragment.fx @@ -5,7 +5,12 @@ uniform float selectionId; #endif varying float vViewPosZ; +#define CUSTOM_FRAGMENT_DEFINITIONS + void main(void) { + +#define CUSTOM_FRAGMENT_MAIN_BEGIN + #if defined(INSTANCES) float id = vSelectionId; #else @@ -13,4 +18,6 @@ void main(void) { #endif gl_FragColor = vec4(id, vViewPosZ, 0.0, 1.0); + +#define CUSTOM_FRAGMENT_MAIN_END } diff --git a/packages/dev/core/src/Shaders/selection.vertex.fx b/packages/dev/core/src/Shaders/selection.vertex.fx index b0b180184b0..58cfa6b6294 100644 --- a/packages/dev/core/src/Shaders/selection.vertex.fx +++ b/packages/dev/core/src/Shaders/selection.vertex.fx @@ -21,8 +21,12 @@ flat varying float vSelectionId; #endif varying float vViewPosZ; +#define CUSTOM_VERTEX_DEFINITIONS + void main(void) { +#define CUSTOM_VERTEX_MAIN_BEGIN + #include #include[0..maxSimultaneousMorphTargets] #include @@ -36,4 +40,6 @@ void main(void) { #if defined(INSTANCES) vSelectionId = instanceSelectionId; #endif + +#define CUSTOM_VERTEX_MAIN_END } diff --git a/packages/dev/core/src/Shaders/selectionOutline.fragment.fx b/packages/dev/core/src/Shaders/selectionOutline.fragment.fx index 9b1d2ae15a9..c4ef9e69da9 100644 --- a/packages/dev/core/src/Shaders/selectionOutline.fragment.fx +++ b/packages/dev/core/src/Shaders/selectionOutline.fragment.fx @@ -14,8 +14,10 @@ uniform float occlusionStrength; #define CUSTOM_FRAGMENT_DEFINITIONS -void main(void) -{ +void main(void) { + +#define CUSTOM_FRAGMENT_MAIN_BEGIN + vec4 screenColor = texture2D(textureSampler, vUV); vec2 texelSize = 1.0 / screenSize; @@ -55,4 +57,6 @@ void main(void) vec3 finalColor = mix(screenColor.rgb, outlineColor, finalOutlineMask); gl_FragColor = vec4(finalColor, screenColor.a); + +#define CUSTOM_FRAGMENT_MAIN_END } diff --git a/packages/dev/core/src/ShadersWGSL/selection.fragment.fx b/packages/dev/core/src/ShadersWGSL/selection.fragment.fx index f319000a9c9..c651b7dbed8 100644 --- a/packages/dev/core/src/ShadersWGSL/selection.fragment.fx +++ b/packages/dev/core/src/ShadersWGSL/selection.fragment.fx @@ -5,8 +5,13 @@ uniform selectionId: f32; #endif varying vViewPosZ: f32; +#define CUSTOM_FRAGMENT_DEFINITIONS + @fragment fn main(input: FragmentInputs) -> FragmentOutputs { + +#define CUSTOM_FRAGMENT_MAIN_BEGIN + #if defined(INSTANCES) var id: f32 = input.vSelectionId; #else @@ -14,4 +19,6 @@ fn main(input: FragmentInputs) -> FragmentOutputs { #endif fragmentOutputs.color = vec4(id, input.vViewPosZ, 0.0, 1.0); + +#define CUSTOM_FRAGMENT_MAIN_END } diff --git a/packages/dev/core/src/ShadersWGSL/selection.vertex.fx b/packages/dev/core/src/ShadersWGSL/selection.vertex.fx index 72d1bdebb30..d8f71597f4a 100644 --- a/packages/dev/core/src/ShadersWGSL/selection.vertex.fx +++ b/packages/dev/core/src/ShadersWGSL/selection.vertex.fx @@ -21,9 +21,13 @@ flat varying vSelectionId: f32; #endif varying vViewPosZ: f32; +#define CUSTOM_VERTEX_DEFINITIONS + @vertex fn main(input: VertexInputs) -> FragmentInputs { +#define CUSTOM_VERTEX_MAIN_BEGIN + #include #include[0..maxSimultaneousMorphTargets] #include @@ -37,4 +41,6 @@ fn main(input: VertexInputs) -> FragmentInputs { #if defined(INSTANCES) vertexOutputs.vSelectionId = input.instanceSelectionId; #endif + +#define CUSTOM_VERTEX_MAIN_END } diff --git a/packages/dev/core/src/ShadersWGSL/selectionOutline.fragment.fx b/packages/dev/core/src/ShadersWGSL/selectionOutline.fragment.fx index 7a4e846a524..7b39d9c0156 100644 --- a/packages/dev/core/src/ShadersWGSL/selectionOutline.fragment.fx +++ b/packages/dev/core/src/ShadersWGSL/selectionOutline.fragment.fx @@ -19,6 +19,9 @@ uniform occlusionStrength: f32; @fragment fn main(input: FragmentInputs) -> FragmentOutputs { + +#define CUSTOM_FRAGMENT_MAIN_BEGIN + var screenColor: vec4f = textureSampleLevel(textureSampler, textureSamplerSampler, input.vUV, 0.0); var texelSize: vec2f = 1.0 / uniforms.screenSize; @@ -58,4 +61,6 @@ fn main(input: FragmentInputs) -> FragmentOutputs { var finalColor: vec3f = mix(screenColor.rgb, uniforms.outlineColor, finalOutlineMask); fragmentOutputs.color = vec4f(finalColor, screenColor.a); + +#define CUSTOM_FRAGMENT_MAIN_END } From 5243e4465a51e718a2de444247f56157315d5e89 Mon Sep 17 00:00:00 2001 From: noname0310 Date: Sun, 28 Dec 2025 18:45:10 +0900 Subject: [PATCH 15/17] Refactor selection vertex shaders to use positionUpdated --- packages/dev/core/src/Shaders/selection.vertex.fx | 3 ++- packages/dev/core/src/ShadersWGSL/selection.vertex.fx | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/dev/core/src/Shaders/selection.vertex.fx b/packages/dev/core/src/Shaders/selection.vertex.fx index 58cfa6b6294..6189501f874 100644 --- a/packages/dev/core/src/Shaders/selection.vertex.fx +++ b/packages/dev/core/src/Shaders/selection.vertex.fx @@ -27,12 +27,13 @@ void main(void) { #define CUSTOM_VERTEX_MAIN_BEGIN + vec3 positionUpdated = position; #include #include[0..maxSimultaneousMorphTargets] #include #include #include - vec4 worldPos = finalWorld * vec4(position, 1.0); + vec4 worldPos = finalWorld * vec4(positionUpdated, 1.0); gl_Position = viewProjection * worldPos; vViewPosZ = (view * worldPos).z; diff --git a/packages/dev/core/src/ShadersWGSL/selection.vertex.fx b/packages/dev/core/src/ShadersWGSL/selection.vertex.fx index d8f71597f4a..e4ea12662bf 100644 --- a/packages/dev/core/src/ShadersWGSL/selection.vertex.fx +++ b/packages/dev/core/src/ShadersWGSL/selection.vertex.fx @@ -28,12 +28,13 @@ fn main(input: VertexInputs) -> FragmentInputs { #define CUSTOM_VERTEX_MAIN_BEGIN + var positionUpdated: vec3f = input.position; #include #include[0..maxSimultaneousMorphTargets] #include #include #include - var worldPos: vec4f = finalWorld * vec4f(input.position, 1.0); + var worldPos: vec4f = finalWorld * vec4f(positionUpdated, 1.0); vertexOutputs.position = uniforms.viewProjection * worldPos; vertexOutputs.vViewPosZ = (uniforms.view * worldPos).z; From 8290722f6843692daa72b3c7eb05f8d39b1f0c1c Mon Sep 17 00:00:00 2001 From: noname0310 Date: Tue, 30 Dec 2025 13:24:43 +0900 Subject: [PATCH 16/17] implement lazy build pipeline --- .../selectionOutlineRenderingPipeline.ts | 156 +++++++++++++----- .../src/Rendering/selectionMaskRenderer.ts | 18 +- 2 files changed, 126 insertions(+), 48 deletions(-) diff --git a/packages/dev/core/src/PostProcesses/RenderPipeline/Pipelines/selectionOutlineRenderingPipeline.ts b/packages/dev/core/src/PostProcesses/RenderPipeline/Pipelines/selectionOutlineRenderingPipeline.ts index 26be5b1e09f..63cc2ea8f3e 100644 --- a/packages/dev/core/src/PostProcesses/RenderPipeline/Pipelines/selectionOutlineRenderingPipeline.ts +++ b/packages/dev/core/src/PostProcesses/RenderPipeline/Pipelines/selectionOutlineRenderingPipeline.ts @@ -103,11 +103,14 @@ export class SelectionOutlineRenderingPipeline extends PostProcessRenderPipeline } private readonly _scene: Scene; + private _isDirty: boolean = true; + private _isDisposed: boolean = false; + private readonly _camerasToBeAttached: Camera[] = []; private readonly _textureType: number; - private _maskRenderer: SelectionMaskRenderer; - private _thinOutlinePostProcess: ThinSelectionOutlinePostProcess; - private _outlinePostProcess: PostProcess; + private readonly _thinOutlinePostProcess: ThinSelectionOutlinePostProcess; + private _maskRenderer: Nullable = null; + private _outlinePostProcess: Nullable = null; /** * Constructs a new selection outline rendering pipeline @@ -127,6 +130,10 @@ export class SelectionOutlineRenderingPipeline extends PostProcessRenderPipeline ) { super(scene.getEngine(), name); + this._cameras = cameras || scene.cameras; + this._cameras = this._cameras.slice(); + this._camerasToBeAttached = this._cameras.slice(); + this._scene = scene; this._textureType = textureType; if (forceGeometryBuffer instanceof GeometryBufferRenderer) { @@ -136,6 +143,8 @@ export class SelectionOutlineRenderingPipeline extends PostProcessRenderPipeline this._forceGeometryBuffer = forceGeometryBuffer; } + this._thinOutlinePostProcess = new ThinSelectionOutlinePostProcess(this._name, scene.getEngine()); + // Set up assets if (this._forceGeometryBuffer) { if (!this._forcedGeometryBuffer) { @@ -144,27 +153,6 @@ export class SelectionOutlineRenderingPipeline extends PostProcessRenderPipeline } else { scene.enablePrePassRenderer(); } - - this._maskRenderer = new SelectionMaskRenderer(name, scene); - this._thinOutlinePostProcess = new ThinSelectionOutlinePostProcess(name, scene.getEngine()); - - this._outlinePostProcess = this._createSelectionOutlinePostProcess(this._textureType); - - this.addEffect( - new PostProcessRenderEffect( - scene.getEngine(), - this.SelectionOutlineEffect, - () => { - return this._outlinePostProcess; - }, - true - ) - ); - - scene.postProcessRenderPipelineManager.addPipeline(this); - if (cameras) { - scene.postProcessRenderPipelineManager.attachCamerasToRenderPipeline(name, cameras); - } } /** @@ -175,27 +163,52 @@ export class SelectionOutlineRenderingPipeline extends PostProcessRenderPipeline return "SelectionOutlineRenderingPipeline"; } + /** + * Adds a camera to the pipeline + * @param camera the camera to be added + */ + public addCamera(camera: Camera): void { + this._camerasToBeAttached.push(camera); + this._isDirty = true; + if (this._maskRenderer !== null) { + // if pipeline is already built, build again + this._buildPipeline(); + } + } + + /** + * Removes a camera from the pipeline + * @param camera the camera to remove + */ + public removeCamera(camera: Camera): void { + const index = this._camerasToBeAttached.indexOf(camera); + this._camerasToBeAttached.splice(index, 1); + this._isDirty = true; + if (this._maskRenderer !== null) { + // if pipeline is already built, build again + this._buildPipeline(); + } + } + /** * Removes the internal pipeline assets and detaches the pipeline from the scene cameras * @param disableGeometryBufferRenderer Set to true if you want to disable the Geometry Buffer renderer */ public override dispose(disableGeometryBufferRenderer: boolean = false): void { - this.clearSelection(); - - for (let i = 0; i < this._cameras.length; i++) { - const camera = this._cameras[i]; - this._outlinePostProcess?.dispose(camera); + if (this._isDisposed) { + return; } + this._disposePipeline(); + if (disableGeometryBufferRenderer && !this._forcedGeometryBuffer) { this._scene.disableGeometryBufferRenderer(); } - this._scene.postProcessRenderPipelineManager.detachCamerasFromRenderPipeline(this._name, this._scene.cameras); - this._scene.postProcessRenderPipelineManager.removePipeline(this._name); - this._thinOutlinePostProcess.dispose(); + this._isDisposed = true; + super.dispose(); } @@ -219,6 +232,51 @@ export class SelectionOutlineRenderingPipeline extends PostProcessRenderPipeline return textureSize; } + private _buildPipeline(): void { + if (!this._isDirty || this._isDisposed) { + return; + } + this._disposePipeline(); + this._isDirty = false; + + const scene = this._scene; + this._maskRenderer = new SelectionMaskRenderer(this._name, scene); + this._outlinePostProcess = this._createSelectionOutlinePostProcess(this._textureType); + this.addEffect( + new PostProcessRenderEffect( + scene.getEngine(), + this.SelectionOutlineEffect, + () => { + return this._outlinePostProcess; + }, + true + ) + ); + + scene.postProcessRenderPipelineManager.addPipeline(this); + scene.postProcessRenderPipelineManager.attachCamerasToRenderPipeline(this._name, this._cameras); + } + + private _disposePipeline(): void { + this._maskRenderer?.dispose(); + this._maskRenderer = null; + + if (this._outlinePostProcess) { + for (let i = 0; i < this._cameras.length; i++) { + const camera = this._cameras[i]; + this._outlinePostProcess.dispose(camera); + } + this._outlinePostProcess = null; + } + + this._scene.postProcessRenderPipelineManager.detachCamerasFromRenderPipeline(this._name, this._cameras); + // get back cameras to be used to reattach pipeline + this._cameras = this._camerasToBeAttached.slice(); + this._reset(); + + this._isDirty = true; // to allow rebuilding + } + private _createSelectionOutlinePostProcess(textureType: number): PostProcess { const outlinePostProcess = new PostProcess("selectionOutline", ThinSelectionOutlinePostProcess.FragmentUrl, { engine: this._scene.getEngine(), @@ -240,7 +298,7 @@ export class SelectionOutlineRenderingPipeline extends PostProcessRenderPipeline this._thinOutlinePostProcess.textureWidth = textureSize.width; this._thinOutlinePostProcess.textureHeight = textureSize.height; - effect.setTexture("maskSampler", this._maskRenderer.getMaskTexture()); + effect.setTexture("maskSampler", this._maskRenderer!.getMaskTexture()); }); return outlinePostProcess; @@ -248,14 +306,17 @@ export class SelectionOutlineRenderingPipeline extends PostProcessRenderPipeline /** * Clears the current selection + * @param disablePipeline If true, disables the pipeline until a new selection is set (default: false) */ - public clearSelection(): void { - this._maskRenderer.clearSelection(); + public clearSelection(disablePipeline: boolean = false): void { + if (this._maskRenderer === null) { + return; + } - // if (this._outlinePostProcess && this._isOutlineProcessAttached) { - // this._camera.detachPostProcess(this._outlinePostProcess); - // this._isOutlineProcessAttached = false; - // } + this._maskRenderer.clearSelection(); + if (disablePipeline) { + this._disposePipeline(); + } } /** @@ -265,7 +326,8 @@ export class SelectionOutlineRenderingPipeline extends PostProcessRenderPipeline * @param meshes Meshes to add to the selection */ public addSelection(meshes: (AbstractMesh | AbstractMesh[])[]): void { - this._maskRenderer.addSelection(meshes); + this._buildPipeline(); + this._maskRenderer!.addSelection(meshes); } /** @@ -275,8 +337,20 @@ export class SelectionOutlineRenderingPipeline extends PostProcessRenderPipeline * @param meshes Meshes to set as the current selection */ public setSelection(meshes: (AbstractMesh | AbstractMesh[])[]): void { - this._maskRenderer.setSelection(meshes); + this._buildPipeline(); + this._maskRenderer!.setSelection(meshes); + } + + /** + * Gets the current selection + */ + public get selection(): readonly AbstractMesh[] { + if (this._maskRenderer === null) { + return []; + } + return this._maskRenderer.selection; } + /** * Serialize the rendering pipeline (Used when exporting) * @returns the serialized object diff --git a/packages/dev/core/src/Rendering/selectionMaskRenderer.ts b/packages/dev/core/src/Rendering/selectionMaskRenderer.ts index bb9da8c0d91..61a9b08bd18 100644 --- a/packages/dev/core/src/Rendering/selectionMaskRenderer.ts +++ b/packages/dev/core/src/Rendering/selectionMaskRenderer.ts @@ -155,6 +155,8 @@ export class SelectionMaskRenderer { this._resizeObserver = engine.onResizeObservable.add(() => { this._maskTexture?.resize({ width: engine.getRenderWidth(), height: engine.getRenderHeight() }); }); + + this._addRenderTargetToScene(); } /** @@ -229,9 +231,8 @@ export class SelectionMaskRenderer { /** * Clears the current selection - * @param removeRenderTargetFromScene If true, removes the render target from the scene's custom render targets */ - public clearSelection(removeRenderTargetFromScene = true): void { + public clearSelection(): void { if (this._selection.length === 0) { return; } @@ -252,9 +253,6 @@ export class SelectionMaskRenderer { } this._nextSelectionId = 1; - if (removeRenderTargetFromScene) { - this._removeRenderTargetFromScene(); - } } /** @@ -305,7 +303,6 @@ export class SelectionMaskRenderer { this._nextSelectionId = nextId; maskTexture.renderList = [...this._selection]; - this._addRenderTargetToScene(); } /** @@ -315,7 +312,7 @@ export class SelectionMaskRenderer { * @param meshes Meshes to set as the current selection */ public setSelection(meshes: (AbstractMesh | AbstractMesh[])[]): void { - this.clearSelection(false); + this.clearSelection(); this.addSelection(meshes); } @@ -326,4 +323,11 @@ export class SelectionMaskRenderer { public getMaskTexture(): Nullable { return this._maskTexture; } + + /** + * Gets the current selection + */ + public get selection(): readonly AbstractMesh[] { + return this._selection; + } } From 79f027576c1645d2e6675ebaad172f4b24377f88 Mon Sep 17 00:00:00 2001 From: noname0310 Date: Tue, 30 Dec 2025 15:27:45 +0900 Subject: [PATCH 17/17] Add empty selection handling to outline pipeline The addSelection and setSelection methods now return early if the meshes array is empty. The setSelection method also supports an optional disablePipeline parameter to dispose the pipeline when the selection is empty. --- .../Pipelines/selectionOutlineRenderingPipeline.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/dev/core/src/PostProcesses/RenderPipeline/Pipelines/selectionOutlineRenderingPipeline.ts b/packages/dev/core/src/PostProcesses/RenderPipeline/Pipelines/selectionOutlineRenderingPipeline.ts index 63cc2ea8f3e..aa96ce42c39 100644 --- a/packages/dev/core/src/PostProcesses/RenderPipeline/Pipelines/selectionOutlineRenderingPipeline.ts +++ b/packages/dev/core/src/PostProcesses/RenderPipeline/Pipelines/selectionOutlineRenderingPipeline.ts @@ -326,6 +326,9 @@ export class SelectionOutlineRenderingPipeline extends PostProcessRenderPipeline * @param meshes Meshes to add to the selection */ public addSelection(meshes: (AbstractMesh | AbstractMesh[])[]): void { + if (meshes.length === 0) { + return; + } this._buildPipeline(); this._maskRenderer!.addSelection(meshes); } @@ -335,8 +338,15 @@ export class SelectionOutlineRenderingPipeline extends PostProcessRenderPipeline * * If a group of meshes is provided, they will outline as a single unit * @param meshes Meshes to set as the current selection + * @param disablePipeline If true, disables the pipeline if the selection is empty (default: false) */ - public setSelection(meshes: (AbstractMesh | AbstractMesh[])[]): void { + public setSelection(meshes: (AbstractMesh | AbstractMesh[])[], disablePipeline: boolean = false): void { + if (meshes.length === 0) { + if (disablePipeline) { + this._disposePipeline(); + } + return; + } this._buildPipeline(); this._maskRenderer!.setSelection(meshes); }