skip to Main Content

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?

Comparison of what I have and the desired end result
Comparison of Photoshop's Normal and Hard Light blend mode

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>

2

Answers


  1. Chosen as BEST ANSWER

    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:

    1. I created a WebGLRenderTarget to which the scene is rendered to.
    2. The WebGLRenderTarget is then passed to the ShaderMaterial's uniforms as a texture, together with the window.innerWidth, window.innerHeight and color.
    3. The respective texture coordinates, in relation to the current fragment, are calculated by dividing gl_FragCoord by the window's width and height.
    4. The fragment can now sample what is on screen from the 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>


  2. 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); 
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search