I’ve been puzzled lately as I’ve been attempting to get a THREE.DepthTexture
to work with the Ambient Occlusion shader. I’ve had it working before with RGBA unpacking, but after reading about Matt Deslauriers’s project, Audiograph, I decided to attempt the method he described for a potential performance boost:
Historically in ThreeJS, you would render your scene with MeshDepthMaterial to a WebGLRenderTarget, and then unpack to a linear depth value when sampling from the depth target. This is fairly expensive and often unnecessary, since many environments support the WEBGL_depth_texture extension.
After attempting this method, I somehow ended up with this weird unwanted effect in which lines are all over the terrain:
I have setup a small example below in which I have replicated the issue. I feel it’s something very obvious that I’m simply glossing over.
I hope someone here is able to point out what I’m missing so that I can get the ambient occlusion working in a way that is a little bit more performant!
Many thanks in advance.
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth/window.innerHeight, 0.1, 2000);
const pivot = new THREE.Object3D();
pivot.add(camera);
scene.add(pivot);
camera.position.set(0, 250, 500);
camera.lookAt(pivot.position);
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.gammaInput = true;
renderer.gammaOutput = true;
renderer.gammaFactor = 2.2;
let supportsExtension = false;
if (renderer.extensions.get('WEBGL_depth_texture')) {
supportsExtension = true;
}
document.body.appendChild(renderer.domElement);
const createCube = () => {
const geo = new THREE.BoxGeometry(500, 500, 500);
const mat = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
const obj = new THREE.Mesh(geo, mat);
obj.position.y = -(obj.geometry.parameters.height / 2);
scene.add(obj);
}
const createSphere = () => {
const geo = new THREE.SphereGeometry(100, 12, 8);
const mat = new THREE.MeshBasicMaterial({ color: 0xff00ff });
const obj = new THREE.Mesh(geo, mat);
obj.position.y = obj.geometry.parameters.radius;
scene.add(obj);
}
// Create objects
createCube();
createSphere();
const composer = new THREE.EffectComposer(renderer);
const target = new THREE.WebGLRenderTarget( window.innerWidth, window.innerHeight );
target.texture.format = THREE.RGBFormat;
target.texture.minFilter = THREE.NearestFilter;
target.texture.magFilter = THREE.NearestFilter;
target.texture.generateMipmaps = false;
target.stencilBuffer = false;
target.depthBuffer = true;
target.depthTexture = new THREE.DepthTexture();
target.depthTexture.type = THREE.UnsignedShortType;
function initPostProcessing() {
composer.addPass(new THREE.RenderPass( scene, camera ));
const pass = new THREE.ShaderPass({
uniforms: {
"tDiffuse": { value: null },
"tDepth": { value: target.depthTexture },
"resolution": { value: new THREE.Vector2( 512, 512 ) },
"cameraNear": { value: 1 },
"cameraFar": { value: 100 },
"onlyAO": { value: 0 },
"aoClamp": { value: 0.5 },
"lumInfluence": { value: 0.5 }
},
vertexShader: document.getElementById('vertexShader').textContent,
fragmentShader: document.getElementById('fragmentShader').textContent,
});
pass.material.precision = 'highp';
composer.addPass(pass);
pass.uniforms.tDepth.value = target.depthTexture;
pass.uniforms.cameraNear.value = camera.near;
pass.uniforms.cameraFar.value = camera.far;
composer.passes[composer.passes.length - 1].renderToScreen = true;
}
initPostProcessing();
const animate = () => {
requestAnimationFrame( animate );
pivot.rotation.y += 0.01;
renderer.render( scene, camera, target );
composer.render();
}
animate();
html, body { margin: 0; }
canvas { display: block; width: 100%; height: 100%; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/86/three.js"></script>
<script src="https://cdn.rawgit.com/mrdoob/three.js/dev/examples/js/postprocessing/EffectComposer.js"></script>
<script src="https://cdn.rawgit.com/mrdoob/three.js/dev/examples/js/postprocessing/RenderPass.js"></script>
<script src="https://cdn.rawgit.com/mrdoob/three.js/dev/examples/js/postprocessing/ShaderPass.js"></script>
<script src="https://cdn.rawgit.com/mrdoob/three.js/dev/examples/js/shaders/CopyShader.js"></script>
<script id="vertexShader" type="x-shader/x-vertex">
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
}
</script>
<script id="fragmentShader" type="x-shader/x-fragment">
uniform float cameraNear;
uniform float cameraFar;
uniform bool onlyAO; // use only ambient occlusion pass?
uniform vec2 resolution; // texture width, height
uniform float aoClamp; // depth clamp - reduces haloing at screen edges
uniform float lumInfluence; // how much luminance affects occlusion
uniform sampler2D tDiffuse;
uniform highp sampler2D tDepth;
varying vec2 vUv;
// #define PI 3.14159265
#define DL 2.399963229728653 // PI * ( 3.0 - sqrt( 5.0 ) )
#define EULER 2.718281828459045
// user variables
const int samples = 4; // ao sample count
const float radius = 5.0; // ao radius
const bool useNoise = false; // use noise instead of pattern for sample dithering
const float noiseAmount = 0.0003; // dithering amount
const float diffArea = 0.4; // self-shadowing reduction
const float gDisplace = 0.4; // gauss bell center
highp vec2 rand( const vec2 coord ) {
highp vec2 noise;
if ( useNoise ) {
float nx = dot ( coord, vec2( 12.9898, 78.233 ) );
float ny = dot ( coord, vec2( 12.9898, 78.233 ) * 2.0 );
noise = clamp( fract ( 43758.5453 * sin( vec2( nx, ny ) ) ), 0.0, 1.0 );
} else {
highp float ff = fract( 1.0 - coord.s * ( resolution.x / 2.0 ) );
highp float gg = fract( coord.t * ( resolution.y / 2.0 ) );
noise = vec2( 0.25, 0.75 ) * vec2( ff ) + vec2( 0.75, 0.25 ) * gg;
}
return ( noise * 2.0 - 1.0 ) * noiseAmount;
}
float readDepth( const in vec2 coord ) {
float cameraFarPlusNear = cameraFar + cameraNear;
float cameraFarMinusNear = cameraFar - cameraNear;
float cameraCoef = 2.0 * cameraNear;
return cameraCoef / ( cameraFarPlusNear - texture2D( tDepth, coord ).x * cameraFarMinusNear );
}
float compareDepths( const in float depth1, const in float depth2, inout int far ) {
float garea = 2.0; // gauss bell width
float diff = ( depth1 - depth2 ) * 100.0; // depth difference (0-100)
// reduce left bell width to avoid self-shadowing
if ( diff < gDisplace ) {
garea = diffArea;
} else {
far = 1;
}
float dd = diff - gDisplace;
float gauss = pow( EULER, -2.0 * dd * dd / ( garea * garea ) );
return gauss;
}
float calcAO( float depth, float dw, float dh ) {
float dd = radius - depth * radius;
vec2 vv = vec2( dw, dh );
vec2 coord1 = vUv + dd * vv;
vec2 coord2 = vUv - dd * vv;
float temp1 = 0.0;
float temp2 = 0.0;
int far = 0;
temp1 = compareDepths( depth, readDepth( coord1 ), far );
// DEPTH EXTRAPOLATION
if ( far > 0 ) {
temp2 = compareDepths( readDepth( coord2 ), depth, far );
temp1 += ( 1.0 - temp1 ) * temp2;
}
return temp1;
}
void main() {
highp vec2 noise = rand( vUv );
float depth = readDepth( vUv );
float tt = clamp( depth, aoClamp, 1.0 );
float w = ( 1.0 / resolution.x ) / tt + ( noise.x * ( 1.0 - noise.x ) );
float h = ( 1.0 / resolution.y ) / tt + ( noise.y * ( 1.0 - noise.y ) );
float ao = 0.0;
float dz = 1.0 / float( samples );
float z = 1.0 - dz / 2.0;
float l = 0.0;
for ( int i = 0; i <= samples; i ++ ) {
float r = sqrt( 1.0 - z );
float pw = cos( l ) * r;
float ph = sin( l ) * r;
ao += calcAO( depth, pw * w, ph * h );
z = z - dz;
l = l + DL;
}
ao /= float( samples );
ao = 1.0 - ao;
vec3 color = texture2D( tDiffuse, vUv ).rgb;
vec3 lumcoeff = vec3( 0.299, 0.587, 0.114 );
float lum = dot( color.rgb, lumcoeff );
vec3 luminance = vec3( lum );
vec3 final = vec3( color * mix( vec3( ao ), vec3( 1.0 ), luminance * lumInfluence ) ); // mix( color * ao, white, luminance )
float depth2 = readDepth(vUv);
if ( onlyAO ) {
final = vec3( mix( vec3( ao ), vec3( 1.0 ), luminance * lumInfluence ) ); // ambient occlusion only
}
// gl_FragColor = vec4( vec3( readDepth( vUv) ), 1.0 ); // Depth
gl_FragColor = vec4( final, 1.0 );
}
</script>
I'd love to hear what is causing my Ambient Occlusion to not render properly!
If you are using a perspective camera and relying on the depth map for any purpose -- that includes SSAO and shadows -- be careful of your choice of camera.near
and camera.far
-- especially near
. ( That would be shadow.camera.near
if you are dealing with shadows.)
Push the near plane out as far as is reasonable for your use case. You will achieve the best results if your scene is positioned near the front of the frustum.
three.js r.86