Skip to content

Commit

Permalink
ToonOutlinePassNode: Add FX pass for toon outlines. (mrdoob#29483)
Browse files Browse the repository at this point in the history
* ToonOutlineNode: Add FX pass for toon outlines.

* ToonOutlinePassNode: Refactor code.

* E2E: Update screenshot.

* ToonOutlinePassNode: Clean up.

* ToonOutlinePassNode: More clean up.

* ToonOutlinePassNode: More clean up.
  • Loading branch information
Mugen87 authored Sep 25, 2024
1 parent a8b6c17 commit be667f1
Show file tree
Hide file tree
Showing 5 changed files with 123 additions and 2 deletions.
Binary file modified examples/screenshots/webgpu_materials_toon.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 10 additions & 2 deletions examples/webgpu_materials_toon.html
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
<script type="module">

import * as THREE from 'three';
import { toonOutlinePass } from 'three/tsl';

import Stats from 'three/addons/libs/stats.module.js';

Expand All @@ -33,7 +34,7 @@

let container, stats;

let camera, scene, renderer;
let camera, scene, renderer, postProcessing;
let particleLight;

const loader = new FontLoader();
Expand Down Expand Up @@ -64,6 +65,13 @@
renderer.setAnimationLoop( render );
container.appendChild( renderer.domElement );

//

postProcessing = new THREE.PostProcessing( renderer );

postProcessing.outputNode = toonOutlinePass( scene, camera );


// Materials

const cubeWidth = 400;
Expand Down Expand Up @@ -184,7 +192,7 @@

stats.begin();

renderer.render( scene, camera );
postProcessing.render();

stats.end();

Expand Down
1 change: 1 addition & 0 deletions src/nodes/Nodes.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ export { default as StereoPassNode } from './display/StereoPassNode.js';
export { default as AnaglyphPassNode } from './display/AnaglyphPassNode.js';
export { default as ParallaxBarrierPassNode } from './display/ParallaxBarrierPassNode.js';
export { default as PassNode } from './display/PassNode.js';
export { default as ToonOutlinePassNode } from './display/ToonOutlinePassNode.js';

// code
export { default as ExpressionNode } from './code/ExpressionNode.js';
Expand Down
1 change: 1 addition & 0 deletions src/nodes/TSL.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ export * from './display/AnaglyphPassNode.js';
export * from './display/ParallaxBarrierPassNode.js';
export * from './display/BleachBypass.js';
export * from './display/Sepia.js';
export * from './display/ToonOutlinePassNode.js';

export * from './display/PassNode.js';

Expand Down
111 changes: 111 additions & 0 deletions src/nodes/display/ToonOutlinePassNode.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { float, nodeObject, normalize, vec4 } from '../tsl/TSLBase.js';
import { Color } from '../../math/Color.js';
import NodeMaterial from '../../materials/nodes/NodeMaterial.js';
import { cameraProjectionMatrix } from '../../nodes/accessors/Camera.js';
import { modelViewMatrix } from '../../nodes/accessors/ModelNode.js';
import { positionLocal } from '../../nodes/accessors/Position.js';
import { normalLocal } from '../../nodes/accessors/Normal.js';
import { BackSide } from '../../constants.js';
import PassNode from './PassNode.js';

class ToonOutlinePassNode extends PassNode {

static get type() {

return 'ToonOutlinePassNode';

}

constructor( scene, camera, colorNode, thicknessNode, alphaNode ) {

super( PassNode.COLOR, scene, camera );

this.colorNode = colorNode;
this.thicknessNode = thicknessNode;
this.alphaNode = alphaNode;

this._materialCache = new WeakMap();

}

updateBefore( frame ) {

const { renderer } = frame;

const currentRenderObjectFunction = renderer.getRenderObjectFunction();

renderer.setRenderObjectFunction( ( object, scene, camera, geometry, material, group, lightsNode ) => {

// only render outline for supported materials

if ( material.isMeshToonMaterial || material.isMeshToonNodeMaterial ) {

if ( material.wireframe === false ) {

const outlineMaterial = this._getOutlineMaterial( material );
renderer.renderObject( object, scene, camera, geometry, outlineMaterial, group, lightsNode );

}

}

// default

renderer.renderObject( object, scene, camera, geometry, material, group, lightsNode );

} );

super.updateBefore( frame );

renderer.setRenderObjectFunction( currentRenderObjectFunction );

}

_createMaterial() {

const material = new NodeMaterial();
material.isMeshToonOutlineMaterial = true;
material.name = 'Toon_Outline';
material.side = BackSide;

// vertex node

const outlineNormal = normalLocal.negate();
const mvp = cameraProjectionMatrix.mul( modelViewMatrix );

const ratio = float( 1.0 ); // TODO: support outline thickness ratio for each vertex
const pos = mvp.mul( vec4( positionLocal, 1.0 ) );
const pos2 = mvp.mul( vec4( positionLocal.add( outlineNormal ), 1.0 ) );
const norm = normalize( pos.sub( pos2 ) ); // NOTE: subtract pos2 from pos because BackSide objectNormal is negative

material.vertexNode = pos.add( norm.mul( this.thicknessNode ).mul( pos.w ).mul( ratio ) );

// color node

material.colorNode = vec4( this.colorNode, this.alphaNode );

return material;

}

_getOutlineMaterial( originalMaterial ) {

let outlineMaterial = this._materialCache.get( originalMaterial );

if ( outlineMaterial === undefined ) {

outlineMaterial = this._createMaterial();

this._materialCache.set( originalMaterial, outlineMaterial );

}

return outlineMaterial;

}

}

export default ToonOutlinePassNode;

export const toonOutlinePass = ( scene, camera, color = new Color( 0, 0, 0 ), thickness = 0.003, alpha = 1 ) => nodeObject( new ToonOutlinePassNode( scene, camera, nodeObject( color ), nodeObject( thickness ), nodeObject( alpha ) ) );

0 comments on commit be667f1

Please sign in to comment.