Three.js: Черные артефакты с использованием шейдера SubSurface Scattering
Я хотел бы использовать ShaderMaterial в Three.js, который позволяет немного света пройти. Как я недавно узнал, эффект, который мне нужен, называется "рассеивание под поверхностью". Я нашел несколько примеров, но только некоторые из них позволяют выполнять вычисления в реальном времени (без дополнительного отображения). Ближайший показан в этом фрагменте:
var container;
var camera, scene, renderer;
var sssMesh;
var lightSourceMesh;
var sssUniforms;
var clock = new THREE.Clock();
init();
animate();
function init() {
container = document.getElementById('container');
camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 1, 3000);
camera.position.z = 4;
camera.position.y = 2;
camera.rotation.x = -0.45;
scene = new THREE.Scene();
var boxGeometry = new THREE.CubeGeometry(0.75, 0.75, 0.75);
var lightSourceGeometry = new THREE.CubeGeometry(0.1, 0.1, 0.1);
sssUniforms = {
u_lightPos: {
type: "v3",
value: new THREE.Vector3()
}
};
var sssMaterial = new THREE.ShaderMaterial({
uniforms: sssUniforms,
vertexShader: document.getElementById('vertexShader').textContent,
fragmentShader: document.getElementById('fragment_shader').textContent
});
var lightSourceMaterial = new THREE.MeshBasicMaterial();
sssMesh = new THREE.Mesh(boxGeometry, sssMaterial);
sssMesh.position.x = 0;
sssMesh.position.y = 0;
scene.add(sssMesh);
lightSourceMesh = new THREE.Mesh(lightSourceGeometry, lightSourceMaterial);
lightSourceMesh.position.x = 0;
lightSourceMesh.position.y = 0;
scene.add(lightSourceMesh);
renderer = new THREE.WebGLRenderer();
container.appendChild(renderer.domElement);
onWindowResize();
window.addEventListener('resize', onWindowResize, false);
}
function onWindowResize(event) {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
function animate() {
requestAnimationFrame(animate);
render();
}
function render() {
var delta = clock.getDelta();
var lightHeight = Math.sin(clock.elapsedTime * 1.0) * 0.5 + 0.7;
lightSourceMesh.position.y = lightHeight;
sssUniforms.u_lightPos.value.y = lightHeight;
sssMesh.rotation.y += delta * 0.5;
renderer.render(scene, camera);
}
body {
color: #ffffff;
background-color: #050505;
margin: 0px;
overflow: hidden;
}
<script src="http://threejs.org/build/three.min.js"></script>
<div id="container"></div>
<script id="fragment_shader" type="x-shader/x-fragment">
varying vec3 v_fragmentPos;
varying vec3 v_normal;
uniform vec3 u_lightPos;
void main(void)
{
vec3 _LightColor0 = vec3(1.0,0.5,0.5);
float _LightIntensity0 = 0.2;
vec3 translucencyColor = vec3(0.8,0.2,0.2);
vec3 toLightVector = u_lightPos - v_fragmentPos;
float lightDistanceSQ = dot(toLightVector, toLightVector);
vec3 lightDir = normalize(toLightVector);
float ndotl = max(0.0, dot(v_normal, lightDir));
float inversendotl = step(0.0, dot(v_normal, -lightDir));
vec3 lightColor = _LightColor0.rgb * ndotl / lightDistanceSQ * _LightIntensity0;
vec3 subsurfacecolor = translucencyColor.rgb * inversendotl / lightDistanceSQ * _LightIntensity0;
vec3 final = subsurfacecolor + lightColor;
gl_FragColor=vec4(final,1.0);
}
</script>
<script id="vertexShader" type="x-shader/x-vertex">
varying vec3 v_fragmentPos;
varying vec3 v_normal;
void main()
{
vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );
v_fragmentPos = (modelMatrix * vec4( position, 1.0 )).xyz;
v_normal = (modelMatrix * vec4( normal, 0.0 )).xyz;
gl_Position = projectionMatrix * mvPosition;
}
</script>
Приведенный выше код прекрасно работает с простой геометрией (в данном случае с кубом). Но он рисует некоторые странные черные лица на более сложных сетках. Последняя цель - "высветить" ландшафты карты высот, но, применяя этот код, я получаю следующее:
Как вы можете видеть, "эффект стекла" очень хорош на освещенных областях, но на более темных (где свет не так хорошо светится) он рисует некоторые полные черные лица, которых я не знаю, как избежать,
Я считаю себя пользователем Three.js среднего уровня, но я впервые играю с шейдерами, и это не так просто, если честно. Каждый раз, когда я что-то изменяю в коде шейдеров, результатом является визуальный мусор. Единственное, что я изменил правильно, это светлые цвета (как спереди, так и сзади). Я также пытался увеличить интенсивность света, но это не работает, оно только "сжигает" больше шейдера, заставляя появляться еще больше черных лиц.
Кто-нибудь может указать мне правильное направление? Как этот эффект называется на первом месте? Я даже не знаю, как искать об этом на ресурсах WebGL. Любая помощь будет оценена!
РЕДАКТИРОВАТЬ: кажется, что увеличение толщины местности (с более высокой шкалой Z) решает проблему. Может быть, это как-то связано с углом света к лицу? Это также происходит в оригинальном фрагменте, когда свет попадает в куб (вы можете видеть полностью черное лицо в кадре). Или, может, я просто говорю глупости. Это легко самый сложный кусок кода, с которым я сталкивался за последние годы, и это всего лишь 10 строк! Я просто хочу, чтобы шейдер выглядел так же хорошо в исходном масштабе, как и в более толстом. Но сложность физики, используемой в этих формулах, мне недоступна.
1 ответ
Есть несколько мест, где вы можете делить на ноль в вашем пиксельном шейдере, который часто отображается черным (строки 3 и 4):
float ndotl = max(0.0, dot(v_normal, lightDir));
float inversendotl = step(0.0, dot(v_normal, -lightDir));
vec3 lightColor = _LightColor0.rgb * ndotl / lightDistanceSQ * _LightIntensity0;
vec3 subsurfacecolor = translucencyColor.rgb * inversendotl / lightDistanceSQ * _LightIntensity0;
vec3 final = subsurfacecolor + lightColor;
и ndotl, и inversendotl могут иметь значение 0 (и довольно часто, поскольку вы зажимаете их в обоих случаях). Любой из последующих членов может быть затем разделен на ноль, делая их черными. Попробуйте закомментировать либо suburfacecolor, либо lightColor, чтобы изолировать неисправный (может быть оба).
max () может быть ненулевым, просто имея маленькую дельту: max (0.0001, x)
Шаг () можно решить, заменив его статическим ветвлением, что также поможет избавиться от этого лишнего точечного произведения, так как вы просто отрицаете вектор, в результате получается противоположный результат.