Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support applying outlines to individual objects #3

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 109 additions & 3 deletions threejs/src/CustomOutlinePass.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,26 @@ class CustomOutlinePass extends Pass {
normalTarget.texture.magFilter = THREE.NearestFilter;
normalTarget.texture.generateMipmaps = false;
normalTarget.stencilBuffer = false;
// This stores the depth buffer containing
// only objects that will have outlines
normalTarget.depthBuffer = true;
normalTarget.depthTexture = new THREE.DepthTexture();
normalTarget.depthTexture.type = THREE.UnsignedShortType;

this.normalTarget = normalTarget;
// Create a buffer to store the depth of the scene
// we don't use the default depth buffer because
// this one includes only objects that have the outline applied
const depthTarget = new THREE.WebGLRenderTarget( this.resolution.x, this.resolution.y );
depthTarget.texture.format = THREE.RGBFormat;
depthTarget.texture.minFilter = THREE.NearestFilter;
depthTarget.texture.magFilter = THREE.NearestFilter;
depthTarget.texture.generateMipmaps = false;
depthTarget.stencilBuffer = false;
depthTarget.depthBuffer = true;
depthTarget.depthTexture = new THREE.DepthTexture();
depthTarget.depthTexture.type = THREE.UnsignedShortType;
this.depthTarget = depthTarget;

this.normalOverrideMaterial = new THREE.MeshNormalMaterial();
}
Expand All @@ -46,6 +65,67 @@ class CustomOutlinePass extends Pass {
);
}

// Helper functions for hiding/showing objects based on whether they should have outlines applied
setOutlineObjectsVisibile(bVisible) {
this.renderScene.traverse( function( node ) {
if (node.applyOutline == true && node.type == 'Mesh') {

if (!bVisible) {
node.oldVisibleValue = node.visible;
node.visible = false;
} else {
// Restore original visible value. This way objects
// that were originally hidden stay hidden
if (node.oldVisibleValue != undefined) {
node.visible = node.oldVisibleValue;
delete node.oldVisibleValue;
}
}


}
});
}

setNonOutlineObjectsVisible(bVisible) {
this.renderScene.traverse( function( node ) {
if (node.applyOutline != true && node.type == 'Mesh') {

if (!bVisible) {
node.oldVisibleValue = node.visible;
node.visible = false;
} else {
// Restore original visible value. This way objects
// that were originally hidden stay hidden
if (node.oldVisibleValue != undefined) {
node.visible = node.oldVisibleValue;
delete node.oldVisibleValue;
}
}


}
});
}

/*

This is a modified pipeline from the original outlines effect
to support outlining individual objects.

1 - Render all objects to get final color buffer, with regular depth buffer
(this is done in index.js)

2 - Render only non-outlines objects to get `nonOutlinesDepthBuffer`.
(we need this to depth test our outlines so they render behind objects)

3 - Render all outlines objects to get normal buffer & depth buffer, which are inputs for the outline effect.
This must NOT include objects that won't have outlines applied.

4 - Render outline effect, using normal and depth buffer that contains only outline objects,
use the `nonOutlinesDepthBuffer` for depth test. And finally combine with the final color buffer.
*/

render(renderer, writeBuffer, readBuffer) {
// Turn off writing to the depth buffer
// because we need to read from it in the subsequent passes.
Expand All @@ -59,18 +139,31 @@ class CustomOutlinePass extends Pass {

const overrideMaterialValue = this.renderScene.overrideMaterial;
this.renderScene.overrideMaterial = this.normalOverrideMaterial;
// Only include objects that have the "applyOutline" property.
// We do this by hiding all other objects temporarily.
this.setNonOutlineObjectsVisible(false);
renderer.render(this.renderScene, this.renderCamera);
this.setNonOutlineObjectsVisible(true);

this.renderScene.overrideMaterial = overrideMaterialValue;

this.fsQuad.material.uniforms["depthBuffer"].value =
readBuffer.depthTexture;
// 2. Re-render the scene to capture depth of objects that do NOT have outlines
renderer.setRenderTarget(this.depthTarget);

this.setOutlineObjectsVisibile(false);
renderer.render(this.renderScene, this.renderCamera);
this.setOutlineObjectsVisibile(true);

this.fsQuad.material.uniforms["depthBuffer"].value = this.normalTarget.depthTexture;

this.fsQuad.material.uniforms[
"normalBuffer"
].value = this.normalTarget.texture;
this.fsQuad.material.uniforms["sceneColorBuffer"].value =
readBuffer.texture;
this.fsQuad.material.uniforms["nonOutlinesDepthBuffer"].value = this.depthTarget.depthTexture;

// 2. Draw the outlines using the depth texture and normal texture
// 3. Draw the outlines using the depth texture and normal texture
// and combine it with the scene color
if (this.renderToScreen) {
// If this is the last effect, then renderToScreen is true.
Expand Down Expand Up @@ -104,6 +197,7 @@ class CustomOutlinePass extends Pass {
uniform sampler2D sceneColorBuffer;
uniform sampler2D depthBuffer;
uniform sampler2D normalBuffer;
uniform sampler2D nonOutlinesDepthBuffer;
uniform float cameraNear;
uniform float cameraFar;
uniform vec4 screenSize;
Expand Down Expand Up @@ -144,6 +238,7 @@ class CustomOutlinePass extends Pass {
void main() {
vec4 sceneColor = texture2D(sceneColorBuffer, vUv);
float depth = getPixelDepth(0, 0);
float nonOutlinesDepth = readDepth(nonOutlinesDepthBuffer, vUv + screenSize.zw);
vec3 normal = getPixelNormal(0, 0);

// Get the difference between depth of neighboring pixels and current.
Expand Down Expand Up @@ -181,6 +276,13 @@ class CustomOutlinePass extends Pass {


float outline = normalDiff + depthDiff;

// Don't render outlines if they are behind something
// in the original depth buffer
// we find this out by comparing the depth value of current pixel
if ( depth > nonOutlinesDepth && debugVisualize != 4) {
outline = 0.0;
}

// Combine outline with scene color.
vec4 outlineColor = vec4(outlineColor, 1.0);
Expand All @@ -193,6 +295,9 @@ class CustomOutlinePass extends Pass {
if (debugVisualize == 2) {
gl_FragColor = vec4(vec3(depth), 1.0);
}
if (debugVisualize == 5) {
gl_FragColor = vec4(vec3(nonOutlinesDepth), 1.0);
}
if (debugVisualize == 3) {
gl_FragColor = vec4(normal, 1.0);
}
Expand All @@ -210,6 +315,7 @@ class CustomOutlinePass extends Pass {
sceneColorBuffer: {},
depthBuffer: {},
normalBuffer: {},
nonOutlinesDepthBuffer: {},
outlineColor: { value: new THREE.Color(0xffffff) },
//4 scalar values packed in one uniform: depth multiplier, depth bias, and same for normals.
multiplierParameters: { value: new THREE.Vector4(1, 1, 1, 1) },
Expand Down
44 changes: 33 additions & 11 deletions threejs/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,20 +28,12 @@ const light = new THREE.DirectionalLight(0xffffff, 1);
scene.add(light);
light.position.set(1.7, 1, -1);

// Set up post processing
// Create a render target that holds a depthTexture so we can use it in the outline pass
// See: https://threejs.org/docs/index.html#api/en/renderers/WebGLRenderTarget.depthBuffer
const depthTexture = new THREE.DepthTexture();
const renderTarget = new THREE.WebGLRenderTarget(
window.innerWidth,
window.innerHeight,
{
depthTexture: depthTexture,
depthBuffer: true,
}
window.innerHeight
);

// Initial render pass.
// Regular scene render pass. This is step 1 in the pipeline described in CustomOutlinePass.js
const composer = new EffectComposer(renderer, renderTarget);
const pass = new RenderPass(scene, camera);
composer.addPass(pass);
Expand All @@ -65,7 +57,27 @@ composer.addPass(effectFXAA);
// Load model
const loader = new GLTFLoader();
loader.load("box.glb", (gltf) => {
scene.add(gltf.scene);
// Create a second mesh to test
// selectively applying outline effect
const mesh1 = gltf.scene; scene.add(mesh1);

const mesh2 = mesh1.clone(); scene.add(mesh2);
mesh2.traverse(node => {
if (node.material) {
node.material = new THREE.MeshStandardMaterial({ color: 0xff0000 });
}
})

mesh2.position.x = -2;
mesh2.position.y = 1;
mesh2.position.z = 2;
mesh2.rotateZ(5);
mesh2.rotateY(5);

window.mesh1 = mesh1;
window.mesh2 = mesh2;

mesh2.traverse(node => node.applyOutline = true);
});

// Set up orbital camera controls.
Expand Down Expand Up @@ -100,6 +112,8 @@ const params = {
depthMult: 1,
normalBias: 1,
normalMult: 1.0,
object1: true,
object2: false
};

const uniforms = customOutline.fsQuad.material.uniforms;
Expand All @@ -108,6 +122,7 @@ gui
Outlines: 0,
"Original scene": 1,
"Depth buffer": 2,
"Non-outlines depth": 5,
"Normal buffer": 3,
"Outlines only": 4,
})
Expand All @@ -132,6 +147,13 @@ gui.add(params, "normalMult", 0.0, 10).onChange(function (value) {
uniforms.multiplierParameters.value.w = value;
});

gui.add(params, "object1").onChange(function (value) {
mesh2.traverse(node => node.applyOutline = value);
});
gui.add(params, "object2").onChange(function (value) {
mesh1.traverse(node => node.applyOutline = value);
});

// Toggling this causes the outline shader to fail sometimes. Not sure why.
// gui.add(params, 'FXAA').onChange( function ( value ) {
// effectFXAA.enabled = value;
Expand Down