着色器 - 简单的 SSS 照明问题
Shader - Simple SSS lighting issue
我正在尝试使用着色器创建简单的次表面散射效果,但我遇到了一个小问题。
看看那些截图。这三个图像代表三种光照状态(在表面之上、真正接近表面、地下),具有不同的光照颜色(红色和蓝色),并且始终具有相同的地下颜色(红色)。
正如您可能注意到的那样,当光线在表面上方并且非常靠近该表面时,它的影响似乎会最小化,这是预期的行为。但问题是它对于次表面部分的行为是相同的,根据我的着色器代码这是正常的,但在我看来,当接近表面时,次表面光的影响应该更高。我建议你看一下屏幕截图以获得预期结果。
我该怎么做?
这是简化的着色器代码。
half ndotl = max(0.0f, dot(normalWorld, lightDir));
half inversendotl = max(0.0f, dot(normalWorld, -lightDir));
half3 lightColor = _LightColor0.rgb * ndotl; // This is the normal light color calculation
half3 subsurfacecolor = translucencyColor.rgb * inversendotl; // This is the subsurface color
half3 topsubsurfacecolor = translucencyColor.rgb; // This is used for adding subsurface color to top surface
final = subsurfacescolor + lerp(lightColor, topsubsurfacecolor * 0.5, 1 - ndotl - inversendotl);
顺便问一下,你实现次表面散射效果的方法很粗糙。使用如此简单的方法很难获得好的结果。
在选定的方法内,我会向您推荐以下内容:
根据平方反比定律考虑到光源的距离。这适用于两个组件,直射光和次表面。
一旦光线在表面后面,最好忽略内部法线和光线方向的点积,因为你永远不知道光线如何穿过物体。还有一个原因是因为折射定律(假设物体的折射系数高于空气之一)使得这个点积的影响较小。一旦光源在表面后面,您可以只使用 step 函数打开次表面组件。
因此,修改后的着色器版本如下:
half3 toLightVector = u_lightPos - v_fragmentPos;
half lightDistanceSQ = dot(toLightVector, toLightVector);
half3 lightDir = normalize(toLightVector);
half ndotl = max(0.0, dot(v_normal, lightDir));
half inversendotl = step(0.0, dot(v_normal, -lightDir));
half3 lightColor = _LightColor0.rgb * ndotl / lightDistanceSQ * _LightIntensity0;
half3 subsurfacecolor = translucencyColor.rgb * inversendotl / lightDistanceSQ * _LightIntensity0;
half3 final = subsurfacecolor + lightColor;
其中 u_lightPos - 包含光源位置的统一,v_fragmentPos - 包含的变化片段的位置。
这是一个在 glsl 中使用 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>
有大量不同的 SSS 模拟技术。
Texture-space diffusion 和 shadowmap-based translucency 是最常用的技术。
查看来自 GPU Gems 的 this 文章,它描述了提到的技术。
此外,您还可以从 EA 找到有趣的 this 演示文稿。它提到了与您非常接近的渲染植物的方法。
球谐函数也适用于静态几何,但这种方法非常复杂,需要预先计算辐照度传递。查看 this 文章,其中显示了使用球谐函数来近似 SSS。
我正在尝试使用着色器创建简单的次表面散射效果,但我遇到了一个小问题。
看看那些截图。这三个图像代表三种光照状态(在表面之上、真正接近表面、地下),具有不同的光照颜色(红色和蓝色),并且始终具有相同的地下颜色(红色)。
正如您可能注意到的那样,当光线在表面上方并且非常靠近该表面时,它的影响似乎会最小化,这是预期的行为。但问题是它对于次表面部分的行为是相同的,根据我的着色器代码这是正常的,但在我看来,当接近表面时,次表面光的影响应该更高。我建议你看一下屏幕截图以获得预期结果。
我该怎么做?
这是简化的着色器代码。
half ndotl = max(0.0f, dot(normalWorld, lightDir));
half inversendotl = max(0.0f, dot(normalWorld, -lightDir));
half3 lightColor = _LightColor0.rgb * ndotl; // This is the normal light color calculation
half3 subsurfacecolor = translucencyColor.rgb * inversendotl; // This is the subsurface color
half3 topsubsurfacecolor = translucencyColor.rgb; // This is used for adding subsurface color to top surface
final = subsurfacescolor + lerp(lightColor, topsubsurfacecolor * 0.5, 1 - ndotl - inversendotl);
顺便问一下,你实现次表面散射效果的方法很粗糙。使用如此简单的方法很难获得好的结果。 在选定的方法内,我会向您推荐以下内容:
根据平方反比定律考虑到光源的距离。这适用于两个组件,直射光和次表面。
一旦光线在表面后面,最好忽略内部法线和光线方向的点积,因为你永远不知道光线如何穿过物体。还有一个原因是因为折射定律(假设物体的折射系数高于空气之一)使得这个点积的影响较小。一旦光源在表面后面,您可以只使用 step 函数打开次表面组件。
因此,修改后的着色器版本如下:
half3 toLightVector = u_lightPos - v_fragmentPos;
half lightDistanceSQ = dot(toLightVector, toLightVector);
half3 lightDir = normalize(toLightVector);
half ndotl = max(0.0, dot(v_normal, lightDir));
half inversendotl = step(0.0, dot(v_normal, -lightDir));
half3 lightColor = _LightColor0.rgb * ndotl / lightDistanceSQ * _LightIntensity0;
half3 subsurfacecolor = translucencyColor.rgb * inversendotl / lightDistanceSQ * _LightIntensity0;
half3 final = subsurfacecolor + lightColor;
其中 u_lightPos - 包含光源位置的统一,v_fragmentPos - 包含的变化片段的位置。
这是一个在 glsl 中使用 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>
有大量不同的 SSS 模拟技术。 Texture-space diffusion 和 shadowmap-based translucency 是最常用的技术。
查看来自 GPU Gems 的 this 文章,它描述了提到的技术。 此外,您还可以从 EA 找到有趣的 this 演示文稿。它提到了与您非常接近的渲染植物的方法。
球谐函数也适用于静态几何,但这种方法非常复杂,需要预先计算辐照度传递。查看 this 文章,其中显示了使用球谐函数来近似 SSS。