Shader - Simple SSS lighting issue

夙愿已清 提交于 2020-01-02 09:28:07

问题


I am trying to create a simple subsurface scattering effect using a shader but I am facing a small issue.

Look at those screenshots. The three images represents three lighting states (above surface, really close to surface, subsurface) with various lighting colors (red and blue) and always the same subsurface color (red).

As you might notice when the light is above the surface and really close to this surface its influence appears to minimize which is the expected behavior. But the problem is that is behaves the same for the subsurface part, this is normal according to my shader code but in my opinion the subsurface light influence should be higher when going close to the surface. I suggest you to look at the screenshot for the expected result.

How can I do that ?

Here is the simplified shader code.

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

回答1:


The way, how you have implemented subsurface scattering effect is very rough. It is hard to achieve nice result using so simple approach. Staying within selected approach, I would recommend you the following things:

  • Take into account distance to the light source accordingly to the inverse square law. This applies to both components, direct light and subsurface.

  • Once the light is behind the surface, it is better to ignore the dot product of the inner normal and direction to the light, because you never know how the light would travel through the object. One more reason is that because of the law of refraction (assuming that refraction coefficient of the object is higher than one of the air) makes this dot product less influential. You may just use a step function to turn on subsurface component once the light source is behind the surface.

So, the modified version of your shader would be as follows:

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;

Where u_lightPos - uniform that contains position of the light source, v_fragmentPos - varying that contains position of the fragment.

Here is an example in glsl using 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>

There are large amount of different techniques of simulation of SSS. Texture-space diffusion and shadowmap-based translucency are the most frequently used techniques.

Check this article from GPU Gems, it describes mentioned techniques. Also, you can find interesting this presentation from EA. It mentions approach that is very close to yours for rendering plants.

Spherical harmonics also works well for static geometry, but this approach is very complicated and it needs precomputed irradiance transfer. Check this article, that shows use of spherical harmonics to approximate SSS.



来源:https://stackoverflow.com/questions/30119553/shader-simple-sss-lighting-issue

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!