import * as THREE from "three"; import { Pass } from "three/addons/postprocessing/Pass.js"; import { FullScreenQuad } from "three/addons/postprocessing/Pass.js"; import { getSurfaceIdMaterial } from "./FindSurfaces.js"; // Follows the structure of // https://github.com/mrdoob/three.js/blob/master/examples/jsm/postprocessing/OutlinePass.js class CustomOutlinePass extends Pass { constructor(resolution, scene, camera, outlineColor, edgeThickness) { super(); this.renderScene = scene; this.renderCamera = camera; this.resolution = new THREE.Vector2(resolution.x, resolution.y); this.outlineColor = outlineColor; this.edgeThickness = edgeThickness; // If rendering at N times final size, set to N this.fsQuad = new FullScreenQuad(null); this.fsQuad.material = this.createOutlinePostProcessMaterial(); // Create a buffer to surface ids const surfaceBuffer = new THREE.WebGLRenderTarget( this.resolution.x, this.resolution.y ); surfaceBuffer.texture.format = THREE.RGBAFormat; surfaceBuffer.texture.type = THREE.HalfFloatType; surfaceBuffer.texture.minFilter = THREE.NearestFilter; surfaceBuffer.texture.magFilter = THREE.NearestFilter; surfaceBuffer.texture.generateMipmaps = false; surfaceBuffer.stencilBuffer = false; this.surfaceBuffer = surfaceBuffer; this.normalOverrideMaterial = new THREE.MeshNormalMaterial(); this.surfaceIdOverrideMaterial = getSurfaceIdMaterial(); } dispose() { this.surfaceBuffer.dispose(); this.fsQuad.dispose(); } updateEdgeThickness(edgeThickness) { this.edgeThickness = edgeThickness; console.log("Updating edge thickness to", this.edgeThickness); this.fsQuad.material.uniforms.edgeThickness.value = edgeThickness; } updateMaxSurfaceId(maxSurfaceId) { this.surfaceIdOverrideMaterial.uniforms.maxSurfaceId.value = maxSurfaceId; } setSize(width, height) { this.surfaceBuffer.setSize(width, height); this.resolution.set(width, height); this.fsQuad.material.uniforms.screenSize.value.set( this.resolution.x, this.resolution.y, 1 / this.resolution.x, 1 / this.resolution.y ); } render(renderer, writeBuffer, readBuffer) { // Turn off writing to the depth buffer // because we need to read from it in the subsequent passes. const depthBufferValue = writeBuffer.depthBuffer; writeBuffer.depthBuffer = false; // 1. Re-render the scene to capture all suface IDs in a texture. renderer.setRenderTarget(this.surfaceBuffer); const overrideMaterialValue = this.renderScene.overrideMaterial; this.renderScene.overrideMaterial = this.surfaceIdOverrideMaterial; renderer.render(this.renderScene, this.renderCamera); this.renderScene.overrideMaterial = overrideMaterialValue; this.fsQuad.material.uniforms["depthBuffer"].value = readBuffer.depthTexture; this.fsQuad.material.uniforms["surfaceBuffer"].value = this.surfaceBuffer.texture; this.fsQuad.material.uniforms["sceneColorBuffer"].value = readBuffer.texture; // 2. 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. // So we should render to the screen by setting target null // Otherwise, just render into the writeBuffer that the next effect will use as its read buffer. renderer.setRenderTarget(null); this.fsQuad.render(renderer); } else { renderer.setRenderTarget(writeBuffer); this.fsQuad.render(renderer); } // Reset the depthBuffer value so we continue writing to it in the next render. writeBuffer.depthBuffer = depthBufferValue; } get vertexShader() { return ` varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); } `; } get fragmentShader() { return ` #include // The above include imports "perspectiveDepthToViewZ" // and other GLSL functions from ThreeJS we need for reading depth. uniform sampler2D sceneColorBuffer; uniform sampler2D depthBuffer; uniform sampler2D surfaceBuffer; uniform float cameraNear; uniform float cameraFar; uniform vec4 screenSize; uniform vec3 outlineColor; uniform vec3 multiplierParameters; // How many pixels away to sample for edges // Larger value give thicker lines // If rendering at N times the final display size // Set this to N for lines whose thickness doesn't depend on N uniform int edgeThickness; uniform int debugVisualize; varying vec2 vUv; // Helper functions for reading from depth buffer. float readDepth (sampler2D depthSampler, vec2 coord) { float fragCoordZ = texture2D(depthSampler, coord).x; float viewZ = perspectiveDepthToViewZ( fragCoordZ, cameraNear, cameraFar ); return viewZToOrthographicDepth( viewZ, cameraNear, cameraFar ); } float getLinearDepth(vec3 pos) { return -(viewMatrix * vec4(pos, 1.0)).z; } float getLinearScreenDepth(sampler2D map) { vec2 uv = gl_FragCoord.xy * screenSize.zw; return readDepth(map,uv); } // Helper functions for reading normals and depth of neighboring pixels. float getPixelDepth(int x, int y) { // screenSize.zw is pixel size // vUv is current position return readDepth(depthBuffer, vUv + screenSize.zw * vec2(x, y)); } // "surface value" is either the normal or the "surfaceID" vec3 getSurfaceValue(int x, int y) { vec3 val = texture2D(surfaceBuffer, vUv + screenSize.zw * vec2(x, y)).rgb; return val; } float saturateValue(float num) { return clamp(num, 0.0, 1.0); } float getSurfaceIdDiff(vec3 surfaceValue) { float surfaceIdDiff = 0.0; int e = edgeThickness; surfaceIdDiff += any(notEqual(surfaceValue, getSurfaceValue(e, 0))) ? 1.0 : 0.0; surfaceIdDiff += any(notEqual(surfaceValue, getSurfaceValue(0, e))) ? 1.0 : 0.0; surfaceIdDiff += any(notEqual(surfaceValue, getSurfaceValue(-e, 0))) ? 1.0 : 0.0; surfaceIdDiff += any(notEqual(surfaceValue, getSurfaceValue(0, -e))) ? 1.0 : 0.0; surfaceIdDiff += any(notEqual(surfaceValue, getSurfaceValue(e, e))) ? 1.0 : 0.0; surfaceIdDiff += any(notEqual(surfaceValue, getSurfaceValue(e, -e))) ? 1.0 : 0.0; surfaceIdDiff += any(notEqual(surfaceValue, getSurfaceValue(-e, e))) ? 1.0 : 0.0; surfaceIdDiff += any(notEqual(surfaceValue, getSurfaceValue(-e, -e))) ? 1.0 : 0.0; return surfaceIdDiff; } float getDepthDiff(float depth) { float depthDiff = 0.0; depthDiff += abs(depth - getPixelDepth(1, 0)); depthDiff += abs(depth - getPixelDepth(-1, 0)); depthDiff += abs(depth - getPixelDepth(0, 1)); depthDiff += abs(depth - getPixelDepth(0, -1)); depthDiff += abs(depth - getPixelDepth(1, 1)); depthDiff += abs(depth - getPixelDepth(1, -1)); depthDiff += abs(depth - getPixelDepth(-1, 1)); depthDiff += abs(depth - getPixelDepth(-1, -1)); return depthDiff; } const uint k = 1103515245U; // GLIB C vec3 hash( vec3 f ) { uvec3 x = floatBitsToUint(f); x = ((x>>8U)^x.yzx)*k; x = ((x>>8U)^x.yzx)*k; x = ((x>>8U)^x.yzx)*k; return vec3(x)/float(0xffffffffU); } void main() { vec4 sceneColor = texture2D(sceneColorBuffer, vUv); float depth = getPixelDepth(0, 0); vec3 surfaceValue = getSurfaceValue(0, 0); // Get the difference between depth of neighboring pixels and current. float depthDiff = getDepthDiff(depth); // Get the difference between surface values of neighboring pixels // and current float surfaceValueDiff = getSurfaceIdDiff(surfaceValue); // Apply multiplier & bias to each float depthBias = multiplierParameters.x; float depthMultiplier = multiplierParameters.y; float lerp = multiplierParameters.z; depthDiff = saturateValue(depthDiff * depthMultiplier); depthDiff = pow(depthDiff, depthBias); if (debugVisualize == 7) { // Surface ID difference gl_FragColor = vec4(vec3(surfaceValueDiff), 1.0); } if (surfaceValueDiff != 0.0) surfaceValueDiff = 1.0; float outline; vec4 outlineColor = vec4(outlineColor, 1.0);; // Normal mode, use the surface ids to draw outlines and add the shaded scene in too if (debugVisualize == 0) { outline = saturateValue(surfaceValueDiff); gl_FragColor = vec4(mix(sceneColor, outlineColor, outline)); } // Depth mode, use the depth to draw outlines only at the outside if (debugVisualize == 1) { outline = saturateValue(depthDiff); gl_FragColor = vec4(mix(sceneColor, outlineColor, outline)); } //Scene color no outline if (debugVisualize == 2) { gl_FragColor = sceneColor; } if (debugVisualize == 3) { gl_FragColor = vec4(vec3(depth), 1.0); } if (debugVisualize == 4) { // 4 visualizes the surfaceID buffer gl_FragColor = vec4(hash(surfaceValue), 1.0); } if (debugVisualize == 5) { // Outlines only outline = mix(surfaceValueDiff, depthDiff, lerp); outline = saturateValue(outline); gl_FragColor = mix(vec4(0,0,0,0), outlineColor, outline); } if (debugVisualize == 6) { // Depth difference gl_FragColor = vec4(vec3(depthDiff), 1.0); } } `; } createOutlinePostProcessMaterial() { return new THREE.ShaderMaterial({ uniforms: { debugVisualize: { value: 0 }, sceneColorBuffer: {}, depthBuffer: {}, surfaceBuffer: {}, outlineColor: { value: new THREE.Color(this.outlineColor) }, edgeThickness: { value: this.edgeThickness }, multiplierParameters: { value: new THREE.Vector3(0.9, 20, 0.5), }, cameraNear: { value: this.renderCamera.near }, cameraFar: { value: this.renderCamera.far }, screenSize: { value: new THREE.Vector4( this.resolution.x, this.resolution.y, 1 / this.resolution.x, 1 / this.resolution.y ), }, }, vertexShader: this.vertexShader, fragmentShader: this.fragmentShader, }); } } export { CustomOutlinePass };