So, I am trying to dynamically mask an UnrealBloomPass with the EffectComposer and I am getting unexpected results. I am not sure if I am missing a key concept here, or if I should be trying to achieve this in a different way. Any input would be appreciated.
The composer is set up into these layers:
I've been following tutorials and examples as closely as possible and still no dice. Thanks in advance.
<!DOCTYPE html>
<html lang="en">
<head>
<title>BloomShader Mask</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
<style>
body {
color: #fff;
font-family:Monospace;
font-size:13px;
text-align:center;
background-color: #fff;
margin: 0px;
overflow: hidden;
}
#info {
position: absolute;
top: 0px;
width: 100%;
padding: 5px;
}
#info p {
max-width: 600px;
margin-left: auto;
margin-right: auto;
padding: 0 2em;
}
a {
color: #2983ff;
}
</style>
</head>
<body>
<div id="container"></div>
<script src="./build/three.js"></script>
<script src="js/libs/stats.min.js"></script>
<script src="js/libs/dat.gui.min.js"></script>
<script src="js/controls/OrbitControls.js"></script>
<script src="js/loaders/GLTFLoader.js"></script>
<script src="js/postprocessing/EffectComposer.js"></script>
<script src="js/postprocessing/RenderPass.js"></script>
<script src="js/postprocessing/ShaderPass.js"></script>
<script src="js/postprocessing/MaskPass.js "></script>
<script src="js/shaders/CopyShader.js"></script>
<script src="js/shaders/LuminosityHighPassShader.js"></script>
<script src="js/postprocessing/UnrealBloomPass.js"></script>
<script id="gradientShader" type="shader-code">
uniform vec2 resolution;
uniform vec2 mousePosition;
void main() {
vec2 pos = (gl_FragCoord.xy - mousePosition.xy * 1.2) / resolution.xy;
gl_FragColor = vec4(1.0, pos.x, pos.y, 1.0);
}
</script>
<script>
var clock = new THREE.Clock();
var stats;
var gui;
var sceneHexGradient;
var sceneMask;
var camera;
var orbitControls;
var pointLight;
var composer;
var mixer;
var maskerMesh;
var bloomPass;
var width = 0;
var height = 0;
var cameraZoom = 120;
var mousePositionY = 0;
var params = {
exposure: 2,
bloomStrength: .81,
bloomThreshold: 0,
bloomRadius: .05,
cameraZoom: cameraZoom
};
function init() {
// Init vars (DOM needed)
gui = initGui();
stats = initStats();
width = window.innerWidth;
height = window.innerHeight;
pixelRatio = window.devicePixelRatio;
// Renderer
var webGLRenderer = new THREE.WebGLRenderer({ antialias: true });
webGLRenderer.setClearColor(new THREE.Color(0x000000, 1.0));
webGLRenderer.setPixelRatio(pixelRatio);
webGLRenderer.setSize(width , height);
webGLRenderer.toneMapping = THREE.ReinhardToneMapping;
webGLRenderer.shadowMap.enabled = true;
appendChild(webGLRenderer.domElement);
// Scene
sceneHexGradient = new THREE.Scene();
sceneMasker = new THREE.Scene();
// Camera
camera = new THREE.OrthographicCamera(-width, width, height, -height, -1000, 1000);
camera.zoom = cameraZoom;
camera.lookAt(new THREE.Vector3(0, 0, 0));
camera.updateProjectionMatrix();
sceneHexGradient.add(camera);
sceneMasker.add(camera);
// Orbit
orbitControls = new THREE.OrbitControls(camera, webGLRenderer.domElement);
orbitControls.maxPolarAngle = Math.PI * 0.5;
orbitControls.minDistance = 1;
orbitControls.maxDistance = 10;
// Draw onto Scenes
drawGradientToScene(sceneHexGradient);
drawHexToScene(sceneHexGradient);
drawMaskerToScene(sceneMasker);
// ShaderPass
var hexgradientPass = new THREE.RenderPass(sceneHexGradient, camera);
hexgradientPass.clear = false;
var effectCopyPass = new THREE.ShaderPass(THREE.CopyShader);
effectCopyPass.renderToScreen = true;
var maskingPass = new THREE.MaskPass(sceneMasker, camera);
maskingPass.clear = true;
// var maskingPass = new THREE.RenderPass(sceneMasker, camera);
// maskingPass.renderToScreen = true;
bloomPass = new THREE.UnrealBloomPass(new THREE.Vector2(width, height), 1.5, 0.4, 0.85);
bloomPass.renderToScreen = true;
bloomPass.threshold = params.bloomThreshold;
bloomPass.strength = params.bloomStrength;
bloomPass.radius = params.bloomRadius;
var clearMaskPass = new THREE.ClearMaskPass();
composer = new THREE.EffectComposer(webGLRenderer);
composer.renderTarget1.stencilBuffer = true;
composer.renderTarget2.stencilBuffer = true;
composer.setSize(width, height);
composer.addPass(hexgradientPass);
composer.addPass(maskingPass);
// composer.addPass(maskingVerticalBlurPass);
composer.addPass(bloomPass);
composer.addPass(clearMaskPass);
composer.addPass(effectCopyPass);
render();
function initStats() { // Debug only
var stats = new Stats();
stats.setMode(0);
stats.domElement.style.position = 'absolute';
stats.domElement.style.left = '0px';
stats.domElement.style.top = '0px';
appendChild(stats.dom);
return stats;
}
function initGui() { // Debug only
gui = new dat.GUI();
gui.add(params, 'exposure', 0.1, 2 ).onChange((value) => {
webGLRenderer.toneMappingExposure = Math.pow(value, 4.0);
});
gui.add(params, 'bloomThreshold', 0.0, 1.0).onChange((value) => {
bloomPass.threshold = Number(value);
});
gui.add(params, 'bloomStrength', 0.0, 3.0).onChange((value) => {
bloomPass.strength = Number(value);
});
gui.add(params, 'bloomRadius', 0.0, 1.0).step(0.01).onChange((value) => {
bloomPass.radius = Number(value);
});
gui.add(params, 'cameraZoom', 100, 150).onChange((value) => {
camera.zoom = Number(value);
camera.updateProjectionMatrix();
});
return gui;
}
function appendChild(domElement) {
var container = document.getElementById('container');
if (container) {
return container.appendChild(domElement);
}
return false;
}
function drawGradientToScene(scene) {
var gradientUniforms = {};
gradientUniforms["resolution"] = { type:'v2', value:new THREE.Vector2(width, height)};
gradientUniforms['mousePosition'] = { type:'v2', value:new THREE.Vector2(0, 0) };
var shaderCode = document.getElementById('gradientShader').innerHTML;
var material = new THREE.ShaderMaterial({ uniforms:gradientUniforms, fragmentShader:shaderCode });
var geometry = new THREE.PlaneBufferGeometry(width, height);
var mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
}
function drawHexToScene(scene) {
var hexes = [];
var colCount = 100;
var rowCount = 100;
var hexDiameter = 1;
var xStart = -(colCount) * hexDiameter * 0.5;
var rowSpace = Math.sqrt(3) * (hexDiameter * 0.5);
var yStart = (rowCount - 1) * (rowSpace * 0.5);
var hexGeom = new THREE.CylinderGeometry(hexDiameter * .55, hexDiameter * .55, 0.0625, 6, 1);
hexGeom.rotateX(Math.PI * 0.5);
for (let j = 0; j < rowCount; j++) {
for (let i = 0; i < colCount + (j % 2 === 0 ? 0 : 1); i++) {
let hex = new THREE.Mesh(hexGeom, new THREE.MeshBasicMaterial({
color: 0x000000,
wireframe: false,
}));
var x = (xStart + i * hexDiameter + (j % 2 === 0 ? 0.5 * hexDiameter : 0));
var y = (yStart - j * rowSpace);
hex.position.set(x, y, 0);
hexes.push(hex);
scene.add(hex);
}
}
}
function drawMaskerToScene(scene) {
var boxGeometry = new THREE.BoxGeometry(width, height, 10);
var basicMaterial = new THREE.MeshBasicMaterial({ color: 0xffffff });
maskerMesh = new THREE.Mesh(boxGeometry, basicMaterial);
maskerMesh.position.y = -height;
scene.add(maskerMesh);
}
function onResze() {
camera.position.set(0, 0, -15);
camera.zoom = cameraZoom;
camera.updateProjectionMatrix();
composer.setSize(width, height);
}
function onMouseMove(event) {
mousePositionY = event.clientY;
}
function render() {
webGLRenderer.autoClear = false;
stats.update();
var delta = clock.getDelta();
// orbitControls.update(delta);
requestAnimationFrame(render);
maskerMesh.position.y = mousePositionY;
// composer.setSize(window.innerWidth, window.innerHeight);
composer.render();
}
window.addEventListener('mousemove', onMouseMove);
} // End - init
// Listeners
window.onload = init;
window.onresize = init.onResze;
</script>
</body>
</html>
It was actually a combination of issues. The main issue was setting the flags on the passes, to clear or to render to screen. This wasn't an easy thing to debug:
<!DOCTYPE html>
<html lang="en">
<head>
<title>BloomShader Mask</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=no,
minimum-scale=1.0, maximum-scale=1.0">
<style>
body {
color: #fff;
font-family:Monospace;
font-size:13px;
text-align:center;
background-color: #fff;
margin: 0px;
overflow: hidden;
}
#info {
position: absolute;
top: 0px;
width: 100%;
padding: 5px;
}
#info p {
max-width: 600px;
margin-left: auto;
margin-right: auto;
padding: 0 2em;
}
a {
color: #2983ff;
}
</style>
</head>
<body>
<div id="container"></div>
<script src="./build/three.js"></script>
<script src="js/libs/stats.min.js"></script>
<script src="js/libs/dat.gui.min.js"></script>
<script src="js/controls/OrbitControls.js"></script>
<script src="js/loaders/GLTFLoader.js"></script>
<script src="js/postprocessing/EffectComposer.js"></script>
<script src="js/postprocessing/ClearPass.js"></script>
<script src="js/postprocessing/RenderPass.js"></script>
<script src="js/postprocessing/ShaderPass.js"></script>
<script src="js/postprocessing/MaskPass.js "></script>
<script src="js/shaders/CopyShader.js"></script>
<script src="js/shaders/LuminosityHighPassShader.js"></script>
<script src="js/postprocessing/UnrealBloomPass.js"></script>
<script id="gradientShader" type="shader-code">
uniform vec2 resolution;
uniform vec2 mousePosition;
void main() {
vec2 pos = ((gl_FragCoord.xy - mousePosition.xy * 1.3) / resolution.xy);
gl_FragColor = vec4(1.0, pos.x, pos.y, 1.0);
}
</script>
<script>
var clock = new THREE.Clock();
var stats;
var gui;
var sceneHexGradient;
var sceneBloomMasker;
var sceneMasker;
var camera;
var orbitControls;
var pointLight;
var composer;
var mixer;
var maskerMesh;
var bloomPass;
var width = 0;
var height = 0;
var cameraZoom = 120;
var mousePositionY = 0;
var params = {
exposure: 2, //1.1
bloomStrength: 1.75,
bloomThreshold: 0,
bloomRadius: .16,
cameraZoom: cameraZoom
};
function init() {
// Init vars (DOM needed)
gui = initGui();
stats = initStats();
width = window.innerWidth;
height = window.innerHeight;
pixelRatio = window.devicePixelRatio;
// Camera
camera = new THREE.OrthographicCamera(-width, width, height, -height, -1000, 1000);
camera.zoom = cameraZoom;
camera.lookAt(new THREE.Vector3(0, 0, 0));
camera.updateProjectionMatrix();
// Renderer
var webGLRenderer = new THREE.WebGLRenderer({ antialias: true });
webGLRenderer.setClearColor(new THREE.Color(0x000000, 1.0));
webGLRenderer.setPixelRatio(pixelRatio);
webGLRenderer.setSize(width , height);
webGLRenderer.toneMapping = THREE.ReinhardToneMapping;
webGLRenderer.shadowMap.enabled = true;
webGLRenderer.autoClear = false;
appendChild(webGLRenderer.domElement);
// Scene
sceneHexGradient = new THREE.Scene();
sceneBloom = new THREE.Scene();
sceneMasker = new THREE.Scene();
sceneHexGradient.add(camera);
sceneBloom.add(camera);
sceneMasker.add(camera);
// Lights (are they needed ? )
sceneBloom.add(new THREE.AmbientLight(0xffffff));
// Orbit
orbitControls = new THREE.OrbitControls(camera, webGLRenderer.domElement);
orbitControls.maxPolarAngle = Math.PI * 0.5;
orbitControls.minDistance = 1;
orbitControls.maxDistance = 10;
// Draw onto Scenes
drawGradientToScene(sceneHexGradient);
drawHexToScene(sceneHexGradient);
drawMaskerToScene(sceneMasker);
bloomPass = new THREE.UnrealBloomPass(new THREE.Vector2(width, height), 1.5, 0.4, 0.85);
bloomPass.threshold = params.bloomThreshold;
bloomPass.strength = params.bloomStrength;
bloomPass.radius = params.bloomRadius;
// ShaderPasses
var clearPass = new THREE.ClearPass();
var clearMaskPass = new THREE.ClearMaskPass();
var hexgradientPass = new THREE.RenderPass(sceneHexGradient, camera);
var maskingPass = new THREE.MaskPass(sceneMasker, camera);
var outputPass = new THREE.ShaderPass(THREE.CopyShader);
outputPass.renderToScreen = true;
var parameters = {
minFilter: THREE.LinearFilter,
magFilter: THREE.LinearFilter,
format: THREE.RGBFormat,
stencilBuffer: true
};
var renderTarget = new THREE.WebGLRenderTarget(width, height, parameters);
composer = new THREE.EffectComposer(webGLRenderer, renderTarget);
composer.addPass(clearPass);
composer.addPass(hexgradientPass);
composer.addPass(maskingPass);
composer.addPass(bloomPass);
composer.addPass(clearMaskPass);
composer.addPass(outputPass);
function initStats() { // Debug only
var stats = new Stats();
stats.setMode(0);
stats.domElement.style.position = 'absolute';
stats.domElement.style.left = '0px';
stats.domElement.style.top = '0px';
appendChild(stats.dom);
return stats;
}
function initGui() { // Debug only
gui = new dat.GUI();
gui.add(params, 'exposure', 0.1, 2 ).onChange((value) => {
webGLRenderer.toneMappingExposure = Math.pow(value, 4.0);
});
gui.add(params, 'bloomThreshold', 0.0, 1.0).onChange((value) => {
bloomPass.threshold = Number(value);
});
gui.add(params, 'bloomStrength', 0.0, 3.0).onChange((value) => {
bloomPass.strength = Number(value);
});
gui.add(params, 'bloomRadius', 0.0, 1.0).step(0.01).onChange((value) => {
bloomPass.radius = Number(value);
});
gui.add(params, 'cameraZoom', 100, 150).onChange((value) => {
camera.zoom = Number(value);
camera.updateProjectionMatrix();
});
return gui;
}
function appendChild(domElement) {
var container = document.getElementById('container');
if (container) {
return container.appendChild(domElement);
}
return false;
}
function getRandomNumber(min = 1, max = 100) {
return Math.floor(Math.random() * (+max - +min)) + +min;
}
function getArrayOfRandomNumbers(count = 1, min = 1, max = 100) {
const outArr = [];
for( i = 0; i < count; ++i) {
outArr.push(getRandomNumber(min, max));
}
return outArr.sort();
}
function drawBlackoutMask(scene) {
// var x = 0, y = 0;
// var hexDiameter = 2;
// var shapeWidth = 10;
// var facets = getRandomNumber(1, shapeWidth);
// var xValuesArr = getArrayOfRandomNumbers(facets, 0, shapeWidth);
// var yValuesArr = getArrayOfRandomNumbers(facets, 0, shapeWidth);
// var hexGeom = new THREE.CylinderGeometry(hexDiameter * .55, hexDiameter * .55, 0.0625, 6, 1);
// hexGeom.rotateX(Math.PI * 0.5);
// var boxGeometry = new THREE.BoxGeometry(10, 10, 10);
// var mesh = new THREE.Mesh(boxGeometry, new THREE.MeshBasicMaterial({ color: 0xff0000 }));
// mesh.position.x = 0;
// mesh.position.y = 0;
// scene.add(mesh);
// for(var i = 0; i < facets; i++) {
// var hexGeom = new THREE.CylinderGeometry(hexDiameter * .55, hexDiameter * .55, 0.0625, 6, 1);
// var mesh = new THREE.Mesh(hexGeom, new THREE.MeshBasicMaterial({
// color: 0xff0000,
// wireframe: false,
// }));
// mesh.position.x = xValuesArr[i];
// mesh.position.y = yValuesArr[i];
// scene.add(mesh);
// }
}
function drawMaskerToScene(scene) {
var boxGeometry = new THREE.CylinderBufferGeometry(width, height, 10);
// var basicMaterial = new THREE.MeshBasicMaterial({ color: 0xffffff });
boxGeometry.computeBoundingBox();
var material = new THREE.ShaderMaterial({
uniforms: {
color1: {
value: new THREE.Color(0xffffff)
},
color2: {
value: new THREE.Color(0x000000)
},
bboxMin: {
value: boxGeometry.boundingBox.min
},
bboxMax: {
value: boxGeometry.boundingBox.max
}
},
vertexShader: `
uniform vec3 bboxMin;
uniform vec3 bboxMax;
varying vec2 vUv;
void main() {
vUv.y = (position.y - bboxMin.y) / (bboxMax.y - bboxMin.y);
gl_Position = projectionMatrix * modelViewMatrix * vec4(position,1.0);
}
`,
fragmentShader: `
uniform vec3 color1;
uniform vec3 color2;
varying vec2 vUv;
void main() {
gl_FragColor = vec4(mix(color1, color2, vUv.y), 1.0);
}
`,
wireframe: false
});
maskerMesh = new THREE.Mesh(boxGeometry, material); //basicMaterial);
// maskerMesh.scale.set(1,1,1);
// maskerMesh.position.y = -height;
scene.add(maskerMesh);
}
function drawGradientToScene(scene) {
var gradientUniforms = {};
gradientUniforms["resolution"] = { type:'v2', value:new THREE.Vector2(width, height)};
gradientUniforms['mousePosition'] = { type:'v2', value:new THREE.Vector2(0, 0) };
var shaderCode = document.getElementById('gradientShader').innerHTML;
var material = new THREE.ShaderMaterial({ uniforms:gradientUniforms, fragmentShader:shaderCode });
var geometry = new THREE.PlaneBufferGeometry(width, height);
var mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
}
function drawHexToScene(scene) {
var hexes = [];
var colCount = 100;
var rowCount = 100;
var hexDiameter = 1;
var xStart = -(colCount) * hexDiameter * 0.5;
var rowSpace = Math.sqrt(3) * (hexDiameter * 0.5);
var yStart = (rowCount - 1) * (rowSpace * 0.5);
var hexGeom = new THREE.CylinderGeometry(hexDiameter * .55, hexDiameter * .55, 0.0625, 6, 1);
hexGeom.rotateX(Math.PI * 0.5);
for (let j = 0; j < rowCount; j++) {
for (let i = 0; i < colCount + (j % 2 === 0 ? 0 : 1); i++) {
let hex = new THREE.Mesh(hexGeom, new THREE.MeshBasicMaterial({
color: 0x000000,
wireframe: false,
}));
var x = (xStart + i * hexDiameter + (j % 2 === 0 ? 0.5 * hexDiameter : 0));
var y = (yStart - j * rowSpace);
hex.position.set(x, y, 0);
hexes.push(hex);
scene.add(hex);
}
}
}
function onResze() {
camera.position.set(0, 0, -15);
camera.zoom = cameraZoom;
camera.updateProjectionMatrix();
composer.setSize(width, height);
}
function onMouseMove(event) {
mousePositionY = event.clientY;
}
function render() {
webGLRenderer.autoClear = false;
stats.update();
var delta = clock.getDelta();
// orbitControls.update(delta);
requestAnimationFrame(render);
maskerMesh.position.y = mousePositionY;
webGLRenderer.clear();
composer.render(delta);
}
window.addEventListener('mousemove', onMouseMove);
render();
} // End - init
// Listeners
window.onload = init;
window.onresize = init.onResze;
</script>
</body>