Режим наложения материала Hard Light в Three.js?

В настоящее время я использую MeshPhongMaterial, предоставленный компанией Three.js, чтобы создать простую сцену с базовой водой. Я бы хотел, чтобы материал для воды имел режим смешивания Hard Light, который можно найти в таких приложениях, как Photoshop. Как я могу достичь режимов смешивания Hard Light ниже справа?

Сравнение того, что у меня есть и желаемый конечный результат Сравнение режима смешивания Normal и Hard Light в Photoshop

Правые половины вышеприведенных изображений установлены в Hard Light в Photoshop. Я пытаюсь воссоздать этот режим Hard Light в Three.js.

Одним из моментов, с которыми я столкнулся, является полное переопределение MeshPhongMaterial фрагмента и вершинного шейдера, но это займет у меня некоторое время, поскольку я я совершенно новичок в этом.

Каким образом реализуется режим смешивания Hard Light для материала в 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>

Ответ 1

Я не думаю, что вы получите тот эффект, который вам нужен.

Как вы создаете первое изображение? Я предполагаю, что вы просто сделали нечеткий овал в фотошопе и выбрали "жесткий свет"?

Если вам нужно то же самое в three.js, вам нужно создать нечеткий овал и применить его в 2d, используя эффект пост-обработки в файле three.js

Вы можете создать такой овал, сделав вторую сцену в three.js, добавив свет и сияя их на черной плоскости, у которой нет волн, которые находятся в том же положении, что и вода в исходной сцене. Отнесите это к rendertarget. Вы, вероятно, хотите только прожектора и, возможно, указать свет в этой сцене. На вашей нынешней сцене вы можете точно определить прожектор. Отметьте это для другой цели рендеринга.

Когда вы закончите объединить сцены, используя эффект пост-обработки, который реализует жесткий свет

// 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); 

Ответ 2

Я закончил делать это следующим образом благодаря превосходному ответу gman. Просмотрите фрагмент кода ниже, чтобы увидеть его в действии.

Как описано gman:

  • Я создал WebGLRenderTarget, на который визуализируется сцена.
  • WebGLRenderTarget затем передается ShaderMaterial в качестве текстуры вместе с window.innerWidth, window.innerHeight и color.
  • Соответствующие текстурные координаты относительно текущего фрагмента вычисляются путем деления gl_FragCoord на ширину и высоту окна.
  • Теперь фрагмент может отображать то, что находится на экране, из текстуры WebGLRenderTarget и сочетать это с color объекта с выведите правильный gl_FragColor.

Пока это отлично работает. Единственное, что я сейчас изучаю, это создать отдельную сцену, содержащую только объекты, необходимые для смешивания, возможно, клонированные. Я предполагаю, что это будет более показательно. В настоящее время я переключаю видимость объекта, который должен быть смешан в цикле рендеринга, до и после его отправки в WebGLRenderTarget. Для большей сцены с большим количеством объектов это, вероятно, не имеет большого смысла и усложнит ситуацию.

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>