SAO multisampling issues, instancing and postprocessing

Hello there,

I’m still in noob level with Three.js, trying to port some stuff I’ve made in other platforms to it. I have a question regarding the SAO pass and a possible way to handle these buffers manually. I encountered and issue when using the built in anti-alias, which requires the use of a Multisample Render Target to work with post-processing effects. However, that creates a problem on itself (at least on my machine). See the results in the following image, using:

THREE.WebGLMultisampleRenderTarget

I am not sure why this is and if its fixable. Any hints on that?

I also have a question regarding the passing of normals when one does instancing, as in my case this does not seem to be happening properly, unless I’m missing a flag or something. What I mean is that if I use normals information for the ao pass, all I see is that either only 1 instance information is passed, or that all instances are at the origin. I have successfully recomputed shadows for example by updating my depth buffer after transforming my instances in the vertex shader, but using the normals option with SAO doesn’t yield the expected results. See this image with SAO initialized as:

const saoPass = new SAOPass( scene, camera, false, true);

Finally, I’m wondering if there is a way to render the AO pass manually to some buffer somehow, and then simply pass that as a texture to a custom shader for manual compositing. I found out that the glitch in my first question pertains only to the beauty+AO pass, not the ao computation itself, which seems to be working fine if using depth instead of normals for calculation, even with a multisample render pass. See here, with sao outputing exclusively the AO pass, and initialized as:

THREE.WebGLMultisampleRenderTarget
const saoPass = new SAOPass( scene, camera, true, false);

I tried to create a minimal example of what I’m doing , with the questions I’ve asked in comments to make sure I’m being clear about it :wink: Very grateful if someone can point me in the right direction with these questions!

'use strict';

import * as THREE from '../../js/three/build/three.module.js';
import { OrbitControls } from '../../js/three/examples/jsm/controls/OrbitControls.js';
import { EffectComposer } from '../../js/three/examples/jsm/postprocessing/EffectComposer.js';
import { RenderPass } from  '../../js/three/examples/jsm/postprocessing/RenderPass.js';
import { SAOPass } from  '../../js/three/examples/jsm/postprocessing/SAOPass.js';
import { ShaderPass } from  '../../js/three/examples/jsm/postprocessing/ShaderPass.js';

let scene, renderer, camera, clock;
let composer;
let stats, arcBall;

function getDim()
{
	const x = window.innerWidth;
	const y = window.innerHeight; 
	return {x: x, y: y, aspect: x / y};
}

function createContainer() {
	const container = document.createElement('div');
	container.appendChild(renderer.domElement);
	document.body.appendChild(container);	
	return container;
}

function createStats() {
  var stats = new Stats();
  stats.setMode(0);
  stats.domElement.style.position = 'absolute';
  stats.domElement.style.left = '0';
  stats.domElement.style.top = '0';
  return stats;
}

function remap(v, lo, hi, nLo, nHi) {
  return nLo + (v - lo) * (nHi - nLo) / (hi - lo);
}

function r() { return Math.random(); }

function rr(a, b) { return remap(r(), 0, 1, a, b); }

function init()
{

	const dim = getDim();
	scene = new THREE.Scene();

	renderer = new THREE.WebGLRenderer({antialias: true});
	renderer.setSize(dim.x, dim.y);
	renderer.powerPreference = 'high-performance';
	renderer.physicallyCorrectLights = true;
	renderer.shadowMap.enabled 	= true;
	renderer.shadowMap.type = THREE.PCFSoftShadowMap;
	renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
	const container = createContainer();

	camera = new THREE.PerspectiveCamera(45, dim.aspect);
	// camera = new THREE.OrthographicCamera(-1*dim.aspect, 1*dim.aspect, 1, -1, 0.1, 10);
	camera.position.set(0, 1., 2.);
	camera.lookAt(new THREE.Vector3());

	const light = new THREE.DirectionalLight(0xffffff, 1.);
	light.position.set(0, 5, 5);
	light.shadow.mapSize.set(2048, 2048);
	light.castShadow = true;
	scene.add(light);

	const instances = 100;

	const texSize 	= Math.ceil(Math.sqrt(instances));
	const cubeTextureLoader = new THREE.CubeTextureLoader();
  const add = "https://threejs.org/examples/textures/cube/Bridge2/";
  const urls = [add + "posx.jpg", add + "negx.jpg", add + "posy.jpg", add + "negy.jpg", add + "posz.jpg", add + "negz.jpg"];
	const environmentMap = cubeTextureLoader.load(urls);

	const floor = new THREE.PlaneGeometry(5, 5);
	const mat 	= new THREE.MeshStandardMaterial();
	const plane = new THREE.Mesh(floor, mat);
	plane.rotation.x = Math.PI * -0.5;
	plane.position.y = -0.5;
	plane.receiveShadow = true;
	scene.add(plane);

	// Instancing
	const geo = new THREE.SphereBufferGeometry(0.1, 25, 25);
	const material = new THREE.MeshStandardMaterial();
	material.environmentMap = environmentMap;
	material.envMapIntensity = 1.;
	material.roughness = .7;
	material.metalness = .5;
	material.opacity = 0.8;
	material.transparent = true;

	const common = `
	#include <common>

	uniform int uWidth;

	attribute int aID;
	attribute vec3 aPos;
	attribute vec3 aCol;

	varying vec4 vColor; 
	`;

	const vertexPartA = `
	int id 		= aID;
	ivec2 xy 	= ivec2(id % uWidth, id / uWidth);

	float iW 	= float(uWidth);
	vec3 pos 	= vec3( (vec2(xy) - iW*0.5) / iW, 0. ); // Grid
	pos 			+= aPos;
	`;

	const vertexPartB = `
	transformed += pos.xyz;
	`;

	const uniforms = {
		uWidth 		: {value: texSize},
	};

	material.onBeforeCompile = (shader) =>
		{

			for(const k in uniforms)
			{
				shader.uniforms[k] = uniforms[k];
			}

			shader.vertexShader = shader.vertexShader.replace(`#include <common>`, common)
			
			shader.vertexShader = shader.vertexShader.replace(`#include <beginnormal_vertex>`,
			`
				#include <beginnormal_vertex>

				${vertexPartA}
			`
			);

			shader.vertexShader = shader.vertexShader.replace(`#include <begin_vertex>`, 
			`
				#include <begin_vertex>

				${vertexPartB}

				vColor = vec4(aCol, 1.);
			`
			);

			shader.fragmentShader = shader.fragmentShader.replace(`#include <common>`, 
			`
				#include <common>
				varying vec4 vColor; 
			`);

			shader.fragmentShader = shader.fragmentShader.replace(`vec4 diffuseColor = vec4( diffuse, opacity );`,
				`vec4 diffuseColor = vec4( vColor.rgb, opacity );`
			);

		}

	// Recompute depth for shadows
	const depthMaterial = new THREE.MeshDepthMaterial({
		depthPacking: THREE.RGBADepthPacking,
	});
	depthMaterial.onBeforeCompile = (shader) =>
	{
		for(const k in uniforms)
		{
			shader.uniforms[k] = uniforms[k];
		}
		shader.vertexShader = shader.vertexShader.replace(`#include <common>`, common);
		shader.vertexShader = shader.vertexShader.replace(`#include <begin_vertex>`, 
			`
				#include <begin_vertex>
				${vertexPartA}
				${vertexPartB}
			`
		);
	}

	const mesh = new THREE.InstancedMesh(geo, material, instances);
	mesh.customDepthMaterial = depthMaterial;
	mesh.receiveShadow 	= true;
	mesh.castShadow 		= true;
	mesh.frustumCulled 	= false;
	mesh.needsUpdate 		= true;
	scene.add(mesh);

	const dummy = new THREE.Object3D(); // to use built-in matrices
	const idsArray = new Int32Array( instances );
	const posArray = new Float32Array(instances * 3);
	const colorsArray = new Float32Array(instances * 3);
	
	for ( let i = 0, i3 = 0, l = instances; i < l; i ++, i3 += 3 ) {

		idsArray[i] = i;
		
		posArray[i3 + 0] = rr(-1, 1);
		posArray[i3 + 1] = 0.25;
		posArray[i3 + 2] = rr(-1, 1);

		colorsArray[i3 + 0] = r();
		colorsArray[i3 + 1] = r();
		colorsArray[i3 + 2] = r();

		// Matrices MUST be set for all instances
		dummy.position.set(0, 0, 0);
		dummy.updateMatrix();
		mesh.setMatrixAt(i, dummy.matrix);
	}

	geo.setAttribute('aID', new THREE.InstancedBufferAttribute( idsArray, 1 ));
	geo.setAttribute('aPos', new THREE.InstancedBufferAttribute( posArray, 3 ));
	geo.setAttribute('aCol', new THREE.InstancedBufferAttribute( colorsArray, 3 ));

	let targetType 	= 'NORMAL';
	let aoOptions 	= {'depth': true, 'normals': false};
	let aoOutput 		= 0;

	/*
	Can't use built-in antialias unless using Multisample Render Target.
	
	1) However, if doing so, then AO effect does not work properly.
	What is the reason for it not working? Is there a way I could fix that?
	
	Uncomment to see:
	*/
	
	// targetType = 'MULTI';
	// aoOptions 	= {'depth': true, 'normals': false};
	// aoOutput = 0;

	/*		
		2) Furthermore, I noticed that normals information for instances does not 
		seem to be passed properly, unless I'm missing a flag or something. What 
		is the reason for that? What I mean is that if I use normals information for
		the ao pass all I see is that either only 1 instance information is passed, or
		that all instnaces are at the origin, even though I have moved them in my shader. 

		Uncomment to see:
	*/

	// targetType = 'MULTI';
	// aoOptions = {'depth': false, 'normals': true};
	// aoOutput = 2;

	/*
		3) Finally, if these two points are not fixable, I was thinking that I could
		render the AO pass to another buffer and simply pass it into a custom shader
		as texture to do the AO operation manually, since it seems that AO computation
		does in fact work and only the mixture with the beauty pass causes issues:

		Uncomment to see:		
	*/

	// targetType = 'MULTI';
	// aoOptions = {'depth': true, 'normals': false};
	// aoOutput = 2;

	/*
		However, I don't seem to figure out how to pass a buffered version of the ao 
		as uniform. I have tried a couple things by fetching the texture of the ao renderpass
		and passing it, but I only get a black screen. Probably I'm doing it wrong, so would
		appreciate it if someone can show if how that's possible to do.

		I was thinking something like:

		const saoTex = saoPass.renderTarget.texture
		(...)
		uniforms{'tAO': {value: saoTex }}
		(...)
		color = tDiffuse.rgb * (1. - sao.rgb);
	*/

	let renderTargetClass = (targetType == 'NORMAL') ?  THREE.WebGLRenderTarget : THREE.WebGLMultisampleRenderTarget;
	const renderTarget = new renderTargetClass(dim.x, dim.y, {
		minFilter: THREE.LinearFilter,
		magFilter: THREE.LinearFilter,
		format: THREE.RGBAFormat

	});

	composer = new EffectComposer(renderer, renderTarget);	
	const renderPass = new RenderPass( scene, camera );
	composer.addPass( renderPass );

	const saoPass = new SAOPass( scene, camera, aoOptions['depth'], aoOptions['normals']);
	saoPass.params.output = aoOutput
	saoPass.params.saoBias = 0.5;
	saoPass.params.saoIntensity = .002;
	saoPass.params.saoScale = 2.;
	saoPass.params.saoKernelRadius = 100;
	saoPass.params.saoMinResolution = 0;
	saoPass.params.saoBlurRadius = 8;
	saoPass.params.saoBlurStdDev = 4;
	saoPass.params.saoBlurDepthCutoff = 0.01;

	saoPass.enabled = true;
	composer.addPass( saoPass );

	const customShader = {
			uniforms: {
				tDiffuse: {value : null },
				uRes: {value : new THREE.Vector2(dim.x, dim.y)},
			},
			vertexShader:
				`
				varying vec2 vUv;

				void main() 
				{
					gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.);
					vUv = uv;
				}
				`
			,
			fragmentShader:
				`
				uniform sampler2D tDiffuse;
				uniform sampler2D tAO;
				varying vec2 vUv;
				uniform vec2 uRes;

				void main()
				{
					vec2 uv = vUv;
					vec4 color = texture2D(tDiffuse, uv);

					// Something like this for ao?
					// vec4 ao = texture2D(tAO, uv);
					// color.rgb *= (1. - ao.rgb);

					float gamma = 2.2;
					color.rgb = pow(color.rgb, vec3(1. / gamma));
					gl_FragColor = color;
				}
				`
		}

	const customPass = new ShaderPass(customShader);
	customPass.enabled = true;  
	composer.addPass(customPass);

	arcBall = new OrbitControls(camera, container);
	arcBall.target.set(0, 0, 0);
	arcBall.enableDamping = true;

	clock = new THREE.Clock();
	stats = createStats();
	document.body.appendChild(stats.domElement);

	window.addEventListener('resize', () => {
		const dim = getDim();
		camera.aspect = dim.aspect;
		camera.updateProjectionMatrix();
		renderer.setSize(dim.x, dim.y);
		composer.setSize(dim.x, dim.y);

	});

}

function render()
{
	composer.render();
}

function animate()
{
	const time = clock.getElapsedTime();
	render();
	arcBall.update();
	stats.update(); 
	window.requestAnimationFrame(animate);
}

init();
animate();