src/passes/OutlinePass.js
import {
Color,
LinearFilter,
MeshDepthMaterial,
RGBADepthPacking,
RGBFormat,
Vector2,
WebGLRenderTarget
} from "three";
import {
CopyMaterial,
DepthComparisonMaterial,
OutlineBlendMaterial,
OutlineEdgesMaterial,
KernelSize
} from "../materials";
import { BlurPass } from "./BlurPass.js";
import { Pass } from "./Pass.js";
import { RenderPass } from "./RenderPass.js";
import { ShaderPass } from "./ShaderPass.js";
/**
* An outline pass.
*/
export class OutlinePass extends Pass {
/**
* Constructs a new outline pass.
*
* @param {Scene} scene - The main scene.
* @param {Camera} camera - The main camera.
* @param {Object} [options] - Additional parameters. See {@link BlurPass}, {@link OutlineBlendMaterial} and {@link OutlineEdgesMaterial} for details.
* @param {Number} [options.pulseSpeed=0.0] - The pulse speed. A value of zero disables the pulse effect.
* @param {Boolean} [options.blur=true] - Whether the outline should be blurred.
*/
constructor(scene, camera, options = {}) {
super("OutlinePass");
/**
* The main scene.
*
* @type {Scene}
* @private
*/
this.mainScene = scene;
/**
* The main camera.
*
* @type {Camera}
* @private
*/
this.mainCamera = camera;
/**
* A render target for depth information.
*
* @type {WebGLRenderTarget}
* @private
*/
this.renderTargetDepth = new WebGLRenderTarget(1, 1, {
minFilter: LinearFilter,
magFilter: LinearFilter
});
this.renderTargetDepth.texture.name = "Outline.Depth";
this.renderTargetDepth.texture.generateMipmaps = false;
/**
* A render target for the outline mask.
*
* @type {WebGLRenderTarget}
* @private
*/
this.renderTargetMask = this.renderTargetDepth.clone();
this.renderTargetMask.texture.format = RGBFormat;
this.renderTargetMask.texture.name = "Outline.Mask";
/**
* A render target for the edge detection.
*
* @type {WebGLRenderTarget}
* @private
*/
this.renderTargetEdges = new WebGLRenderTarget(1, 1, {
minFilter: LinearFilter,
magFilter: LinearFilter,
stencilBuffer: false,
depthBuffer: false,
format: RGBFormat
});
this.renderTargetEdges.texture.name = "Outline.Edges";
this.renderTargetEdges.texture.generateMipmaps = false;
/**
* A render target for the blurred outline overlay.
*
* @type {WebGLRenderTarget}
* @private
*/
this.renderTargetBlurredEdges = this.renderTargetEdges.clone();
this.renderTargetBlurredEdges.texture.name = "Outline.BlurredEdges";
/**
* A depth pass.
*
* @type {RenderPass}
* @private
* @todo Use multiple render targets in WebGL 2.0.
*/
this.renderPassDepth = new RenderPass(this.mainScene, this.mainCamera, {
overrideMaterial: new MeshDepthMaterial({
depthPacking: RGBADepthPacking,
morphTargets: true,
skinning: true
}),
clearColor: new Color(0xffffff),
clearAlpha: 1.0
});
/**
* A depth comparison mask pass.
*
* @type {RenderPass}
* @private
* @todo Use multiple render targets in WebGL 2.0.
*/
this.renderPassMask = new RenderPass(this.mainScene, this.mainCamera, {
overrideMaterial: new DepthComparisonMaterial(this.renderTargetDepth.texture, this.mainCamera),
clearColor: new Color(0xffffff),
clearAlpha: 1.0
});
/**
* A blur pass.
*
* @type {BlurPass}
* @private
*/
this.blurPass = new BlurPass(options);
this.kernelSize = options.kernelSize;
/**
* The original resolution.
*
* @type {Vector2}
* @private
*/
this.resolution = new Vector2();
/**
* A copy pass that renders the read buffer to screen if needed.
*
* @type {ShaderPass}
* @private
*/
this.copyPass = new ShaderPass(new CopyMaterial());
this.copyPass.renderToScreen = true;
/**
* An outline edge detection material.
*
* @type {OutlineEdgesMaterial}
* @private
*/
this.outlineEdgesMaterial = new OutlineEdgesMaterial(options);
this.outlineEdgesMaterial.uniforms.tMask.value = this.renderTargetMask.texture;
/**
* An outline blend material.
*
* @type {OutlineBlendMaterial}
* @private
*/
this.outlineBlendMaterial = new OutlineBlendMaterial(options);
this.outlineBlendMaterial.uniforms.tMask.value = this.renderTargetMask.texture;
this.blur = (options.blur !== undefined) ? options.blur : true;
/**
* A list of objects to outline.
*
* @type {Object3D[]}
* @private
*/
this.selection = [];
/**
* The current animation time.
*
* @type {Number}
* @private
*/
this.time = 0.0;
/**
* The pulse speed. A value of zero disables the pulse effect.
*
* @type {Number}
*/
this.pulseSpeed = (options.pulseSpeed !== undefined) ? options.pulseSpeed : 0.0;
/**
* A dedicated render layer for selected objects.
*
* This layer is set to 10 by default. If this collides with your own custom
* layers, please change it to a free layer before rendering!
*
* @type {Number}
*/
this.selectionLayer = 10;
}
/**
* The blur kernel size.
*
* @type {KernelSize}
*/
get kernelSize() {
return this.blurPass.kernelSize;
}
/**
* @type {KernelSize}
*/
set kernelSize(value = KernelSize.VERY_SMALL) {
this.blurPass.kernelSize = value;
}
/**
* Indicates whether the outline overlay should be blurred.
*
* @type {Boolean}
*/
get blur() {
return this.blurPass.enabled;
}
/**
* @type {Boolean}
*/
set blur(value) {
this.blurPass.enabled = value;
this.outlineBlendMaterial.uniforms.tEdges.value = value ?
this.renderTargetBlurredEdges.texture :
this.renderTargetEdges.texture;
}
/**
* Indicates whether dithering is enabled.
*
* @type {Boolean}
*/
get dithering() {
return this.blurPass.dithering;
}
/**
* If enabled, the result will be dithered to remove banding artifacts.
*
* @type {Boolean}
*/
set dithering(value) {
this.blurPass.dithering = value;
}
/**
* Indicates whether the effect should be applied to the input buffer.
*
* @type {Boolean}
*/
get blend() {
return this.needsSwap;
}
/**
* If disabled, the input buffer will remain unaffected.
*
* You may use the {@link BloomPass#overlay} texture to apply the effect to
* your scene.
*
* @type {Boolean}
*/
set blend(value) {
this.needsSwap = value;
}
/**
* The effect overlay texture.
*
* @type {Texture}
*/
get overlay() {
return this.outlineBlendMaterial.uniforms.tEdges.value;
}
/**
* The resolution scale.
*
* @type {Number}
* @deprecated Use getResolutionScale() instead.
*/
get resolutionScale() {
console.warn("OutlinePass.resolutionScale has been deprecated, please use OutlinePass.getResolutionScale()");
return this.getResolutionScale();
}
/**
* @type {Number}
* @deprecated Use setResolutionScale(Number) instead.
*/
set resolutionScale(value) {
console.warn("OutlinePass.resolutionScale has been deprecated, please use OutlinePass.setResolutionScale(Number)");
this.setResolutionScale(value);
}
/**
* Returns the current resolution scale.
*
* @return {Number} The resolution scale.
*/
getResolutionScale() {
return this.blurPass.getResolutionScale();
}
/**
* Sets the resolution scale.
*
* @param {Number} scale - The new resolution scale.
*/
setResolutionScale(scale) {
this.blurPass.setResolutionScale(scale);
this.setSize(this.resolution.x, this.resolution.y);
}
/**
* Sets a pattern texture to use as an overlay for selected objects.
*
* @param {Texture} [texture=null] - A pattern texture. Set to null to disable the pattern.
*/
setPatternTexture(texture = null) {
this.outlineBlendMaterial.setPatternTexture(texture);
}
/**
* Clears the current selection and selects a list of objects.
*
* @param {Object3D[]} objects - The objects that should be outlined. This array will be copied.
* @return {OutlinePass} This pass.
*/
setSelection(objects) {
const selection = objects.slice(0);
const selectionLayer = this.selectionLayer;
let i, l;
this.clearSelection();
for(i = 0, l = selection.length; i < l; ++i) {
selection[i].layers.enable(selectionLayer);
}
this.selection = selection;
return this;
}
/**
* Clears the list of selected objects.
*
* @return {OutlinePass} This pass.
*/
clearSelection() {
const selection = this.selection;
const selectionLayer = this.selectionLayer;
let i, l;
for(i = 0, l = selection.length; i < l; ++i) {
selection[i].layers.disable(selectionLayer);
}
this.selection = [];
this.time = 0.0;
return this;
}
/**
* Selects an object.
*
* @param {Object3D} object - The object that should be outlined.
* @return {OutlinePass} This pass.
*/
selectObject(object) {
object.layers.enable(this.selectionLayer);
this.selection.push(object);
return this;
}
/**
* Deselects an object.
*
* @param {Object3D} object - The object that should no longer be outlined.
* @return {OutlinePass} This pass.
*/
deselectObject(object) {
const selection = this.selection;
const index = selection.indexOf(object);
if(index >= 0) {
selection[index].layers.disable(this.selectionLayer);
selection.splice(index, 1);
if(selection.length === 0) {
this.time = 0.0;
}
}
return this;
}
/**
* Sets the visibility of all selected objects.
*
* @private
* @param {Boolean} visible - Whether the selected objects should be visible.
*/
setSelectionVisible(visible) {
const selection = this.selection;
let i, l;
for(i = 0, l = selection.length; i < l; ++i) {
if(visible) {
selection[i].layers.enable(0);
} else {
selection[i].layers.disable(0);
}
}
}
/**
* Renders the effect.
*
* @param {WebGLRenderer} renderer - The renderer.
* @param {WebGLRenderTarget} inputBuffer - A frame buffer that contains the result of the previous pass.
* @param {WebGLRenderTarget} outputBuffer - A frame buffer that serves as the output render target unless this pass renders to screen.
* @param {Number} [delta] - The time between the last frame and the current one in seconds.
* @param {Boolean} [stencilTest] - Indicates whether a stencil mask is active.
*/
render(renderer, inputBuffer, outputBuffer, delta, stencilTest) {
const mainScene = this.mainScene;
const mainCamera = this.mainCamera;
const pulse = this.outlineBlendMaterial.uniforms.pulse;
let background, mask;
if(this.selection.length > 0) {
background = mainScene.background;
mask = mainCamera.layers.mask;
mainScene.background = null;
pulse.value = 1.0;
if(this.pulseSpeed > 0.0) {
pulse.value = 0.625 + Math.cos(this.time * this.pulseSpeed * 10.0) * 0.375;
this.time += delta;
}
// Render a custom depth texture and ignore selected objects.
this.setSelectionVisible(false);
this.renderPassDepth.render(renderer, this.renderTargetDepth);
this.setSelectionVisible(true);
// Create a mask for the selected objects using the depth information.
mainCamera.layers.mask = 1 << this.selectionLayer;
this.renderPassMask.render(renderer, this.renderTargetMask);
// Restore the camera layer mask and the scene background.
mainCamera.layers.mask = mask;
mainScene.background = background;
// Detect the outline.
this.setFullscreenMaterial(this.outlineEdgesMaterial);
renderer.render(this.scene, this.camera, this.renderTargetEdges);
if(this.blurPass.enabled) {
// Blur the edges.
this.blurPass.render(renderer, this.renderTargetEdges, this.renderTargetBlurredEdges);
}
if(this.blend) {
// Draw the final overlay onto the scene colours.
this.setFullscreenMaterial(this.outlineBlendMaterial);
this.outlineBlendMaterial.uniforms.tDiffuse.value = inputBuffer.texture;
renderer.render(this.scene, this.camera, this.renderToScreen ? null : outputBuffer);
}
} else if(this.renderToScreen) {
// Draw the read buffer to screen.
this.copyPass.render(renderer, inputBuffer);
}
}
/**
* Updates the size of this pass.
*
* @param {Number} width - The width.
* @param {Number} height - The height.
*/
setSize(width, height) {
// Remember the original resolution.
this.resolution.set(width, height);
this.renderTargetDepth.setSize(width, height);
this.renderTargetMask.setSize(width, height);
this.renderPassDepth.setSize(width, height);
this.renderPassMask.setSize(width, height);
this.blurPass.setSize(width, height);
// The blur pass applies the resolution scale.
width = this.blurPass.width;
height = this.blurPass.height;
this.renderTargetEdges.setSize(width, height);
this.renderTargetBlurredEdges.setSize(width, height);
this.outlineBlendMaterial.uniforms.aspect.value = width / height;
this.outlineEdgesMaterial.setTexelSize(1.0 / width, 1.0 / height);
}
/**
* Performs initialization tasks.
*
* @param {WebGLRenderer} renderer - The renderer.
* @param {Boolean} alpha - Whether the renderer uses the alpha channel or not.
*/
initialize(renderer, alpha) {
this.renderPassDepth.initialize(renderer, alpha);
this.renderPassMask.initialize(renderer, alpha);
this.blurPass.initialize(renderer, alpha);
}
}