I am currently using the MeshPhongMaterial
provided by Three.js to create a simple scene with basic water. I would like for the water material to have the Hard Light
blending mode that can be found in applications such as Photoshop. How can I achieve the Hard Light
blending modes below on the right?
The right halves of the images above are set to Hard Light
in Photoshop. I am trying to recreate that Hard Light
blend mode in Three.js.
One lead I have come across is to completely reimplement the MeshPhongMaterial
's fragment and vertex shader, but this will take me some time as I am quite new to this.
What is the way to implement a Hard Light
blending mode for a material in Three.js?
/*
* Scene config
**/
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 10000);
var renderer = new THREE.WebGLRenderer({
antialias: true
});
renderer.setClearColor(0xffffff);
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
camera.position.set(0, 500, 1000);
camera.lookAt(scene.position);
/*
* Scene lights
**/
var spotlight = new THREE.SpotLight(0x999999, 0.1);
spotlight.castShadow = true;
spotlight.shadowDarkness = 0.75;
spotlight.position.set(0, 500, 0);
scene.add(spotlight);
var pointlight = new THREE.PointLight(0x999999, 0.5);
pointlight.position.set(75, 50, 0);
scene.add(pointlight);
var hemiLight = new THREE.HemisphereLight(0xffce7a, 0x000000, 1.25);
hemiLight.position.y = 75;
hemiLight.position.z = 500;
scene.add(hemiLight);
/*
* Scene objects
*/
/* Water */
var waterGeo = new THREE.PlaneGeometry(1000, 1000, 50, 50);
var waterMat = new THREE.MeshPhongMaterial({
color: 0x00aeff,
emissive: 0x0023b9,
shading: THREE.FlatShading,
shininess: 60,
specular: 30,
transparent: true
});
for (var j = 0; j < waterGeo.vertices.length; j++) {
waterGeo.vertices[j].x = waterGeo.vertices[j].x + ((Math.random() * Math.random()) * 30);
waterGeo.vertices[j].y = waterGeo.vertices[j].y + ((Math.random() * Math.random()) * 20);
}
var waterObj = new THREE.Mesh(waterGeo, waterMat);
waterObj.rotation.x = -Math.PI / 2;
scene.add(waterObj);
/* Floor */
var floorGeo = new THREE.PlaneGeometry(1000, 1000, 50, 50);
var floorMat = new THREE.MeshPhongMaterial({
color: 0xe9b379,
emissive: 0x442c10,
shading: THREE.FlatShading
});
for (var j = 0; j < floorGeo.vertices.length; j++) {
floorGeo.vertices[j].x = floorGeo.vertices[j].x + ((Math.random() * Math.random()) * 30);
floorGeo.vertices[j].y = floorGeo.vertices[j].y + ((Math.random() * Math.random()) * 20);
floorGeo.vertices[j].z = floorGeo.vertices[j].z + ((Math.random() * Math.random()) * 20);
}
var floorObj = new THREE.Mesh(floorGeo, floorMat);
floorObj.rotation.x = -Math.PI / 2;
floorObj.position.y = -75;
scene.add(floorObj);
/*
* Scene render
**/
var count = 0;
function render() {
requestAnimationFrame(render);
var particle, i = 0;
for (var ix = 0; ix < 50; ix++) {
for (var iy = 0; iy < 50; iy++) {
waterObj.geometry.vertices[i++].z = (Math.sin((ix + count) * 2) * 3) +
(Math.cos((iy + count) * 1.5) * 6);
waterObj.geometry.verticesNeedUpdate = true;
}
}
count += 0.05;
renderer.render(scene, camera);
}
render();
html,
body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r73/three.min.js"></script>
I don't think you're going to get the effect you want.
How do you generate the first image? I assume you just made fuzzy oval in photoshop and picked "hard light"?
If you want the same thing in three.js you'll need to generate a fuzzy oval and apply it in 2d using a post processing effect in three.js
You could generate such an oval by making a 2nd scene in three.js, adding the lights and shining them on a black plane that has no waves that's at the same position as the water is in the original scene. Render that to a rendertarget. You probably want only the spotlight and maybe point light in that scene. In your current scene remove the spotlight for sure. Render that to another render target.
When you're done combine the scenes using a post processing effect that implements hard light
// pseudo code
vec3 partA = texture2D(sceneTexture, texcoord);
vec3 partB = texture2D(lightTexture, texcoord);
vec3 line1 = 2.0 * partA * partB;
vec3 line2 = 1.0 - (1.0 - partA) * (1.0 - partB);
gl_FragCoord = vec4(mix(line2, line1, step(0.5, partA)), 1);
I ended up doing it in the following way thanks to gman's excellent answer. View the code snippet below to see it in action.
As gman described:
WebGLRenderTarget
to which the scene is rendered to.WebGLRenderTarget
is then passed to the ShaderMaterial
's uniforms as a texture, together with the window.innerWidth
, window.innerHeight
and color
.gl_FragCoord
by the window's width and height.WebGLRenderTarget
texture and combine that with the color
of the object to output the correct gl_FragColor
.So far it works great. The only thing I am currently looking into is to create a separate scene containing only the objects that are necessary for blending, perhaps cloned. I assume that would be more performant. Currently I am toggling the visibility of the object to be blended in the render loop, before and after it is sent to the WebGLRenderTarget
. For a larger scene with more objects, that probably doesn't make much sense and would complicate things.
var conf = {
'Color A': '#cc6633',
'Color B': '#0099ff'
};
var GUI = new dat.GUI();
var A_COLOR = GUI.addColor(conf, 'Color A');
A_COLOR.onChange(function(val) {
A_OBJ.material.uniforms.color = {
type: "c",
value: new THREE.Color(val)
};
A_OBJ.material.needsUpdate = true;
});
var B_COLOR = GUI.addColor(conf, 'Color B');
B_COLOR.onChange(function(val) {
B_OBJ.material.uniforms.color = {
type: "c",
value: new THREE.Color(val)
};
B_OBJ.material.needsUpdate = true;
});
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 100);
var renderer = new THREE.WebGLRenderer();
renderer.setClearColor(0x888888);
renderer.setSize(window.innerWidth, window.innerHeight);
var target = new THREE.WebGLRenderTarget(window.innerWidth, window.innerHeight, {format: THREE.RGBFormat});
document.body.appendChild(renderer.domElement);
camera.position.set(0, 0, 50);
camera.lookAt(scene.position);
var A_GEO = new THREE.PlaneGeometry(20, 20);
var B_GEO = new THREE.PlaneGeometry(20, 20);
var A_MAT = new THREE.ShaderMaterial({
uniforms: {
color: {
type: "c",
value: new THREE.Color(0xcc6633)
}
},
vertexShader: document.getElementById('vertexShaderA').innerHTML,
fragmentShader: document.getElementById('fragmentShaderA').innerHTML
});
var B_MAT = new THREE.ShaderMaterial({
uniforms: {
color: {
type: "c",
value: new THREE.Color(0x0099ff)
},
window: {
type: "v2",
value: new THREE.Vector2(window.innerWidth, window.innerHeight)
},
target: {
type: "t",
value: target
}
},
vertexShader: document.getElementById('vertexShaderB').innerHTML,
fragmentShader: document.getElementById('fragmentShaderB').innerHTML
});
var A_OBJ = new THREE.Mesh(A_GEO, A_MAT);
var B_OBJ = new THREE.Mesh(B_GEO, B_MAT);
A_OBJ.position.set(-5, -5, 0);
B_OBJ.position.set(5, 5, 0);
scene.add(A_OBJ);
scene.add(B_OBJ);
function render() {
requestAnimationFrame(render);
B_OBJ.visible = false;
renderer.render(scene, camera, target, true);
B_OBJ.visible = true;
renderer.render(scene, camera);
}
render();
body { margin: 0 }
canvas { display: block }
<script src="https://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.5.1/dat.gui.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r74/three.min.js"></script>
<script type="x-shader/x-vertex" id="vertexShaderA">
uniform vec3 color;
void main() {
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
</script>
<script type="x-shader/x-fragment" id="fragmentShaderA">
uniform vec3 color;
void main() {
gl_FragColor = vec4(color, 1.0);
}
</script>
<script type="x-shader/x-vertex" id="vertexShaderB">
uniform vec3 color;
void main() {
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
</script>
<script type="x-shader/x-fragment" id="fragmentShaderB">
uniform vec3 color;
uniform vec2 window;
uniform sampler2D target;
void main() {
vec2 targetCoords = gl_FragCoord.xy / window.xy;
vec4 a = texture2D(target, targetCoords);
vec4 b = vec4(color, 1.0);
vec4 multiply = 2.0 * a * b;
vec4 screen = 1.0 - 2.0 * (1.0 - a) * (1.0 - b);
gl_FragColor = vec4(mix(screen, multiply, step(0.5, a)));
}
</script>
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With