Skip to content

Commit

Permalink
OutlineNode: Improve approach (#29583)
Browse files Browse the repository at this point in the history
* improve outline example

* rev
  • Loading branch information
sunag authored Oct 8, 2024
1 parent 35f4555 commit 1ae4fce
Show file tree
Hide file tree
Showing 2 changed files with 90 additions and 112 deletions.
104 changes: 53 additions & 51 deletions examples/jsm/tsl/display/OutlineNode.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Color, DepthTexture, FloatType, RenderTarget, Vector2 } from 'three';
import { add, Loop, int, exp, min, float, mul, uv, vec2, Fn, textureSize, orthographicDepthToViewZ, QuadMesh, screenUV, TempNode, nodeObject, NodeUpdateType, uniform, vec4, NodeMaterial, passTexture, texture, perspectiveDepthToViewZ, positionView } from 'three/tsl';
import { Loop, int, exp, min, float, mul, uv, vec2, vec3, Fn, textureSize, orthographicDepthToViewZ, QuadMesh, screenUV, TempNode, nodeObject, NodeUpdateType, uniform, vec4, NodeMaterial, passTexture, texture, perspectiveDepthToViewZ, positionView } from 'three/tsl';

const _quadMesh = /*@__PURE__*/ new QuadMesh();
const _currentClearColor = /*@__PURE__*/ new Color();
Expand All @@ -15,20 +15,23 @@ class OutlineNode extends TempNode {

}

constructor( scene, camera, selectedObjects = [] ) {
constructor( scene, camera, params = {} ) {

super( 'vec4' );

const {
selectedObjects = [],
edgeThickness = float( 1 ),
edgeGlow = float( 0 ),
downSampleRatio = 2
} = params;

this.scene = scene;
this.camera = camera;
this.selectedObjects = selectedObjects;
this.downSampleRatio = 2;
this.visibleEdgeColor = new Color( 1, 1, 1 );
this.hiddenEdgeColor = new Color( 0.1, 0.04, 0.02 );
this.edgeThickness = 1;
this.edgeStrength = 3.0;
this.edgeGlow = 0;
this.pulsePeriod = 0;
this.downSampleRatio = downSampleRatio;
this.edgeThickness = edgeThickness;
this.edgeGlow = edgeGlow;

this.updateBeforeType = NodeUpdateType.FRAME;

Expand All @@ -50,13 +53,7 @@ class OutlineNode extends TempNode {

this._cameraNear = uniform( camera.near );
this._cameraFar = uniform( camera.far );
this._resolution = uniform( new Vector2() );
this._visibleEdgeColor = uniform( new Color() );
this._hiddenEdgeColor = uniform( new Color() );
this._blurDirection = uniform( new Vector2() );
this._kernelRadius = uniform( 1 );
this._edgeGlow = uniform( 0 );
this._edgeStrength = uniform( 0 );

this._depthTextureUniform = texture( this._renderTargetDepthBuffer.depthTexture );
this._maskTextureUniform = texture( this._renderTargetMaskBuffer.texture );
Expand All @@ -65,6 +62,11 @@ class OutlineNode extends TempNode {
this._edge2TextureUniform = texture( this._renderTargetEdgeBuffer2.texture );
this._blurColorTextureUniform = texture( this._renderTargetEdgeBuffer1.texture );

// constants

this._visibleEdgeColor = vec3( 1, 0, 0 );
this._hiddenEdgeColor = vec3( 0, 1, 0 );

// materials

this._depthMaterial = new NodeMaterial();
Expand All @@ -83,6 +85,9 @@ class OutlineNode extends TempNode {
this._separableBlurMaterial = new NodeMaterial();
this._separableBlurMaterial.name = 'OutlineNode.separableBlur';

this._separableBlurMaterial2 = new NodeMaterial();
this._separableBlurMaterial2.name = 'OutlineNode.separableBlur2';

this._compositeMaterial = new NodeMaterial();
this._compositeMaterial.name = 'OutlineNode.composite';

Expand All @@ -98,6 +103,18 @@ class OutlineNode extends TempNode {

}

get visibleEdge() {

return this.r;

}

get hiddenEdge() {

return this.g;

}

getTextureNode() {

return this._textureNode;
Expand All @@ -106,8 +123,6 @@ class OutlineNode extends TempNode {

setSize( width, height ) {

this._resolution.value.set( width, height );

this._renderTargetDepthBuffer.setSize( width, height );
this._renderTargetMaskBuffer.setSize( width, height );
this._renderTargetComposite.setSize( width, height );
Expand Down Expand Up @@ -199,20 +214,6 @@ class OutlineNode extends TempNode {

// 4. Perform edge detection (half resolution)

this._tempPulseColor1.copy( this.visibleEdgeColor );
this._tempPulseColor2.copy( this.hiddenEdgeColor );

if ( this.pulsePeriod > 0 ) {

const scalar = ( 1 + 0.25 ) / 2 + Math.cos( performance.now() * 0.01 / this.pulsePeriod ) * ( 1.0 - 0.25 ) / 2;
this._tempPulseColor1.multiplyScalar( scalar );
this._tempPulseColor2.multiplyScalar( scalar );

}

this._visibleEdgeColor.value.copy( this._tempPulseColor1 );
this._hiddenEdgeColor.value.copy( this._tempPulseColor2 );

_quadMesh.material = this._edgeDetectionMaterial;
renderer.setRenderTarget( this._renderTargetEdgeBuffer1 );
_quadMesh.render( renderer );
Expand All @@ -221,7 +222,6 @@ class OutlineNode extends TempNode {

this._blurColorTextureUniform.value = this._renderTargetEdgeBuffer1.texture;
this._blurDirection.value.copy( _BLUR_DIRECTION_X );
this._kernelRadius.value = this.edgeThickness;

_quadMesh.material = this._separableBlurMaterial;
renderer.setRenderTarget( this._renderTargetBlurBuffer1 );
Expand All @@ -237,8 +237,8 @@ class OutlineNode extends TempNode {

this._blurColorTextureUniform.value = this._renderTargetEdgeBuffer1.texture;
this._blurDirection.value.copy( _BLUR_DIRECTION_X );
this._kernelRadius.value = this.edgeThickness;

_quadMesh.material = this._separableBlurMaterial2;
renderer.setRenderTarget( this._renderTargetBlurBuffer2 );
_quadMesh.render( renderer );

Expand All @@ -250,9 +250,6 @@ class OutlineNode extends TempNode {

// 7. Composite

this._edgeGlow.value = this.edgeGlow;
this._edgeStrength.value = this.edgeStrength;

_quadMesh.material = this._compositeMaterial;
renderer.setRenderTarget( this._renderTargetComposite );
_quadMesh.render( renderer );
Expand Down Expand Up @@ -312,13 +309,13 @@ class OutlineNode extends TempNode {
const c3 = this._maskTextureDownsSampleUniform.uv( uvNode.add( uvOffset.yw ) ).toVar();
const c4 = this._maskTextureDownsSampleUniform.uv( uvNode.sub( uvOffset.yw ) ).toVar();

const diff1 = mul( c1.r.sub( c2.r ), float( 0.5 ) );
const diff2 = mul( c3.r.sub( c4.r ), float( 0.5 ) );
const diff1 = mul( c1.r.sub( c2.r ), 0.5 );
const diff2 = mul( c3.r.sub( c4.r ), 0.5 );
const d = vec2( diff1, diff2 ).length();
const a1 = min( c1.g, c2.g );
const a2 = min( c3.g, c4.g );
const visibilityFactor = min( a1, a2 );
const edgeColor = float( 1.0 ).sub( visibilityFactor ).greaterThan( float( 0.001 ) ).select( this._visibleEdgeColor, this._hiddenEdgeColor );
const edgeColor = visibilityFactor.oneMinus().greaterThan( 0.001 ).select( this._visibleEdgeColor, this._hiddenEdgeColor );
return vec4( edgeColor, 1 ).mul( d );

} );
Expand All @@ -328,36 +325,36 @@ class OutlineNode extends TempNode {

// seperable blur material

const MAX_RADIUS = 4;

const gaussianPdf = Fn( ( [ x, sigma ] ) => {

return float( 0.39894 ).mul( exp( float( - 0.5 ).mul( x ).mul( x ).div( sigma.mul( sigma ) ) ).div( sigma ) );

} );

const seperableBlur = Fn( () => {

const MAX_RADIUS = 4;
const seperableBlur = Fn( ( [ kernelRadius ] ) => {

const resolution = textureSize( this._maskTextureDownsSampleUniform );
const invSize = vec2( 1 ).div( resolution ).toVar();
const uvNode = uv();

const sigma = this._kernelRadius.div( 2 ).toVar();
const sigma = kernelRadius.div( 2 ).toVar();
const weightSum = gaussianPdf( 0, sigma ).toVar();
const diffuseSum = this._blurColorTextureUniform.uv( uvNode ).mul( weightSum ).toVar();
const delta = this._blurDirection.mul( invSize ).mul( this._kernelRadius ).div( MAX_RADIUS ).toVar();
const delta = this._blurDirection.mul( invSize ).mul( kernelRadius ).div( MAX_RADIUS ).toVar();

const uvOffset = delta.toVar();

Loop( { start: int( 0 ), end: int( MAX_RADIUS ), type: 'int', condition: '<=' }, ( { i } ) => {
Loop( { start: int( 1 ), end: int( MAX_RADIUS ), type: 'int', condition: '<=' }, ( { i } ) => {

const x = this._kernelRadius.mul( float( i ) ).div( MAX_RADIUS );
const x = kernelRadius.mul( float( i ) ).div( MAX_RADIUS );
const w = gaussianPdf( x, sigma );
const sample1 = this._blurColorTextureUniform.uv( uvNode.add( uvOffset ) );
const sample2 = this._blurColorTextureUniform.uv( uvNode.sub( uvOffset ) );

diffuseSum.addAssign( add( sample1, sample2 ).mul( w ) );
weightSum.addAssign( float( 2 ).mul( w ) );
diffuseSum.addAssign( sample1.add( sample2 ).mul( w ) );
weightSum.addAssign( w.mul( 2 ) );
uvOffset.addAssign( delta );

} );
Expand All @@ -366,9 +363,12 @@ class OutlineNode extends TempNode {

} );

this._separableBlurMaterial.fragmentNode = seperableBlur();
this._separableBlurMaterial.fragmentNode = seperableBlur( this.edgeThickness );
this._separableBlurMaterial.needsUpdate = true;

this._separableBlurMaterial2.fragmentNode = seperableBlur( MAX_RADIUS );
this._separableBlurMaterial2.needsUpdate = true;

// composite material

const composite = Fn( () => {
Expand All @@ -377,8 +377,9 @@ class OutlineNode extends TempNode {
const edgeValue2 = this._edge2TextureUniform;
const maskColor = this._maskTextureUniform;

const edgeValue = edgeValue1.add( edgeValue2.mul( this._edgeGlow ) );
return this._edgeStrength.mul( maskColor.r ).mul( edgeValue );
const edgeValue = edgeValue1.add( edgeValue2.mul( this.edgeGlow ) );

return maskColor.r.mul( edgeValue );

} );

Expand Down Expand Up @@ -407,6 +408,7 @@ class OutlineNode extends TempNode {
this._materialCopy.dispose();
this._edgeDetectionMaterial.dispose();
this._separableBlurMaterial.dispose();
this._separableBlurMaterial2.dispose();
this._compositeMaterial.dispose();

}
Expand Down
98 changes: 37 additions & 61 deletions examples/webgpu_postprocessing_outline.html
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
<script type="module">

import * as THREE from 'three';
import { pass } from 'three/tsl';
import { pass, uniform, time, oscSine } from 'three/tsl';
import { outline } from 'three/addons/tsl/display/OutlineNode.js';

import Stats from 'three/addons/libs/stats.module.js';
Expand All @@ -45,62 +45,6 @@
const obj3d = new THREE.Object3D();
const group = new THREE.Group();

const params = {
edgeStrength: 3.0,
edgeGlow: 0.0,
edgeThickness: 1.0,
pulsePeriod: 0
};

// Init gui

const gui = new GUI( { width: 280 } );

gui.add( params, 'edgeStrength', 0.01, 10 ).onChange( function ( value ) {

outlinePass.edgeStrength = Number( value );

} );

gui.add( params, 'edgeGlow', 0.0, 1 ).onChange( function ( value ) {

outlinePass.edgeGlow = Number( value );

} );

gui.add( params, 'edgeThickness', 1, 4 ).onChange( function ( value ) {

outlinePass.edgeThickness = Number( value );

} );

gui.add( params, 'pulsePeriod', 0.0, 5 ).onChange( function ( value ) {

outlinePass.pulsePeriod = Number( value );

} );

function Configuration() {

this.visibleEdgeColor = '#ffffff';
this.hiddenEdgeColor = '#190a05';

}

const conf = new Configuration();

gui.addColor( conf, 'visibleEdgeColor' ).onChange( function ( value ) {

outlinePass.visibleEdgeColor.set( value );

} );

gui.addColor( conf, 'hiddenEdgeColor' ).onChange( function ( value ) {

outlinePass.hiddenEdgeColor.set( value );

} );

init();

function init() {
Expand Down Expand Up @@ -227,15 +171,47 @@
stats = new Stats();
container.appendChild( stats.dom );

// outline pass

const edgeStrength = uniform( 3.0 );
const edgeGlow = uniform( 0.0 );
const edgeThickness = uniform( 1.0 );
const pulsePeriod = uniform( 0 );
const visibleEdgeColor = uniform( new THREE.Color( 0xffffff ) );
const hiddenEdgeColor = uniform( new THREE.Color( 0x190a05 ) );

outlinePass = outline( scene, camera, {
selectedObjects,
edgeGlow,
edgeThickness
} );

const { visibleEdge, hiddenEdge } = outlinePass;

const period = time.div( pulsePeriod ).mul( 2 );
const osc = oscSine( period ).mul( .5 ).add( .5 ); // osc [ 0.5, 1.0 ]

const outlineColor = visibleEdge.mul( visibleEdgeColor ).add( hiddenEdge.mul( hiddenEdgeColor ) ).mul( edgeStrength );
const outlinePulse = pulsePeriod.greaterThan( 0 ).select( outlineColor.mul( osc ), outlineColor );

// postprocessing

const scenePass = pass( scene, camera );

postProcessing = new THREE.PostProcessing( renderer );
postProcessing.outputNode = outlinePulse.add( scenePass );

const scenePass = pass( scene, camera );
const scenePassColor = scenePass.getTextureNode( 'output' );
outlinePass = outline( scene, camera, scene );
// gui

postProcessing.outputNode = outlinePass.getTextureNode().add( scenePassColor );
const gui = new GUI( { width: 280 } );
gui.add( edgeStrength, 'value', 0.01, 10 ).name( 'edgeStrength' );
gui.add( edgeGlow, 'value', 0.0, 1 ).name( 'edgeGlow' );
gui.add( edgeThickness, 'value', 1, 4 ).name( 'edgeThickness' );
gui.add( pulsePeriod, 'value', 0.0, 5 ).name( 'pulsePeriod' );
gui.addColor( visibleEdgeColor, 'value' ).name( 'visibleEdgeColor' );
gui.addColor( hiddenEdgeColor, 'value' ).name( 'hiddenEdgeColor' );

//

window.addEventListener( 'resize', onWindowResize );

Expand Down

0 comments on commit 1ae4fce

Please sign in to comment.