shader Programming-RayMarching and Blinn-Phong Lighting Models to Build 3D Scenes (WebGL-Shader Development Foundation 08)

Posted by genix2011 on Fri, 10 Dec 2021 18:57:00 +0100

1. demo overview and effects

The last article studied RayMarching, but the effect of the scene was black and white. Based on the previous article, this article improves it by using the Blinn-Phong lighting model to render objects of different colors. Below is the final effect of demo. Different objects have their own colors

shader-blinn-phong

2. Introduction of Blinn-Phong lighting model

There are three types of light that shine on the surface of an object in a scene

  • Ambient light
  • Diffuse reflectance
  • Mirror Reflection (High Light Reflection)

Surface color = ambient light + diffuse light + specular light
Replace with the following formula

3. demo implementation

3.1 Scene object preparation

Previously, I learned the functions of SDF graphics in two-dimensional scenes. The following functions are simple three-dimensional SDF functions needed in demo

//sphere
float sdSphere( vec3 p, float s )
{
  return length(p)-s;
}

//Cube
float sdBox( vec3 p, vec3 b,float rad )
{
  vec3 d = abs(p) - b;
  return min(max(d.x,max(d.y,d.z)),0.0) + length(max(d,0.0)) - rad;
}

//Doughnut
float sdTorus( vec3 p, vec2 t )
{
  return length( vec2(length(p.xz)-t.x,p.y) )-t.y;
}

3.2 rayMarching Adjustment

In the main function, the original rayMarch returns a float data representing the distance from the viewpoint at which light intersects the object. This time, in addition to returning this distance, it also returns a material ID, so it returns a two-dimensional vector

vec2 res = rayMarch(ro,rd);//Inverse ray tracing for intersection distance and material ID

float d = res.x;//Distance between object and viewpoint
float m = res.y;//Material ID

The light step function also needs to be adjusted, the return value becomes vec2, the x component represents distance, and the y component represents material ID, as follows

vec2 rayMarch(vec3 rayStart, vec3 rayDirection) {
  float depth=0.;
  float material=0.;
  for(int i=0; i<MAX_STEPS; i++) {
    vec3 p = rayStart + rayDirection*depth;//The coordinates after the last step are the starting point of this step

    vec2 dm = getDistandMaterial(p);
    float dist = dm.x;//Gets the distance when the current starting point of the step intersects the object
    material = dm.y;
    depth += dist; //Step Length Accumulation
    if(depth>MAX_DIST || dist<SURF_DIST) break;//Step distance greater than maximum step distance or less than minimum surface distance from object surface (light entering object) stops moving
  }
  return vec2(depth,material);
}

Getting the distance function of the intersection of a light and an object is done the same way, as follows

vec2 opU( vec2 d1, vec2 d2 )
{
  return (d1.x<d2.x) ? d1 : d2;
}

vec2 getDistandMaterial(vec3 p){

  float plane = p.y;//ground

  vec2 res = vec2(plane,0.0);

  vec2 sphere = vec2(sdSphere(p-vec3(0,1,5),0.8),1.0);//Sphere, material ID 1.0
  vec2 box = vec2(sdBox(p-vec3(2,1,5),vec3(0.8,0.3,0.6),0.06),2.0);//Square, Material ID 2.0
  vec2 torus = vec2(sdTorus(p-vec3(-2,1,5),vec2(0.6,0.2)),3.0);//Doughnut, material ID 3.0
  
  res = opU(res,sphere);
  res = opU(res,box);
  res = opU(res,torus);

  return res;
}

The function adjustment to get the normal vector, which used to subtract directly from the return value of the computed distance function, is now adjusted to subtract using the x component of the return value

vec3 getNormal(vec3 p){
  return normalize(vec3(
    getDistandMaterial(vec3(p.x + SURF_DIST, p.y, p.z)).x - getDistandMaterial(vec3(p.x - SURF_DIST, p.y, p.z)).x,
    getDistandMaterial(vec3(p.x, p.y + SURF_DIST, p.z)).x - getDistandMaterial(vec3(p.x, p.y - SURF_DIST, p.z)).x,
    getDistandMaterial(vec3(p.x, p.y, p.z  + SURF_DIST)).x - getDistandMaterial(vec3(p.x, p.y, p.z - SURF_DIST)).x
  ));
}

3.3 Blinn-Phong Lighting Calculation

This part is completely different from the process in the previous article. Using a new calculation method, first set different surface colors for different objects in the main function based on the material ID returned by the light step function, as follows:

void main( void ) {

  //Window coordinates adjusted to [-1,1], coordinate origin at the center of the screen
  vec2 st = (gl_FragCoord.xy * 2. - u_resolution) / u_resolution.y;


  vec3 ro = vec3(0.0,2.0,0.0);//viewpoint
  vec3 rd = normalize(vec3(st.x,st.y,1.0));//Line of sight direction

  vec2 res = rayMarch(ro,rd);//Inverse ray tracing for intersection distance and material ID

  float d = res.x;//Distance between object and viewpoint
  float m = res.y;//Material ID

  vec3 p = ro + rd * d;

  vec3 materialColor = vec3(1.0, 1.0, 1.0);
  
  
  //Set different material colors for different objects
  if(m==0.0){
    materialColor = vec3(.2, 0.0, 0.0);
  }
  if(m==1.0){
    materialColor = vec3(.2, 0.0, 1.0);
  }
  if(m==2.0){
    materialColor = vec3(.7, 0.2, 0.0);
  }
  if(m==3.0){
    materialColor = vec3(.8, .9, 0.0);
  }

  vec3 color = vec3(1.0,1.0,1.0);

  //Calculating illumination using Blinn-Phong model
  color *= calcBlinnPhongLight( materialColor, p, ro);

  gl_FragColor = vec4(color, 1.0);

}

This step is the most important part. First, paste a diagram of the diffuse reflection model.

Blinn-Phong model calculates highlight diagrams

To illustrate the above two diagrams as a whole,
This is used to calculate the decay of light. A point light source diffuses energy like a growing sphere in three-dimensional space starting from the point where the light source emits. The farther it is from the center of the sphere, the less energy it reaches the surface of the object. This relationship is inversely proportional to the square of the distance.

max function In order to eliminate light greater than 90 degrees, we use clamp function to eliminate

When calculating highlights, there is also an exponential p. When you see highlights at the viewpoint, the angle between the reflected light and the line of sight is very close. Usually, as the angle increases slowly, you will no longer see highlights. This exponential p accelerates the attenuation, that is, the larger the p value, the smaller the range of highlights you see.

The implementation process is as follows

//Illumination calculation of Blinn-Phong model
vec3 calcBlinnPhongLight( vec3 materialColor, vec3 p, vec3 ro) {

  vec3 lightPos = vec3(4.0 * sin(u_time), 20.0, 4.0*cos(u_time));//Light source coordinates

  //Computing ambient light
  float k_a = 0.2;//Ambient light reflection coefficient
  vec3 ambientLight = 0.6 * vec3(1.0, 1.0, 1.0);
  vec3 ambient = k_a*ambientLight;
  
  vec3 N = getNormal(p); //normal
  vec3 L = normalize(lightPos - p); //Illumination direction
  vec3 V = normalize(ro - p); //sight
  vec3 H = normalize(V+L); //Half-range vector

  float r = length(lightPos - p);

  //Calculating diffuse reflectance
  float k_d = 0.6;//Diffuse reflectance
  float dotLN = clamp(dot(L, N),0.0,1.0);//Point multiplies and limits results to 0~1
  vec3 diffuse = k_d * (materialColor/r*r) * dotLN;


  //Calculating high-light reflection
  float k_s = 0.8;//Mirror Reflectance
  float shininess = 160.0;
  vec3 specularColor = vec3(1.0, 1.0, 1.0);
  vec3 specular = k_s * (specularColor/r*r)* pow(clamp(dot(N, H), 0.0, 1.0), shininess);//Calculate highlights

  
  //Calculate Shadow
  vec2 res = rayMarch(p + N*SURF_DIST*2.0,L); 
  if(res.x<length(lightPos-p)-0.001){
    diffuse*=0.1;
  }
    
  //Color = ambient light + diffuse light + specular light
  return ambient +diffuse + specular;
}

4. demo code

Old rules, paste all code

<body>
  <div id="container"></div>
  <script src="http://www.yanhuangxueyuan.com/versions/threejsR92/build/three.js"></script>
  <script>
    var container;
    var camera, scene, renderer;
    var uniforms;
    var vertexShader = `
      void main() {
        gl_Position = vec4( position, 1.0 );
      } 
    `
    var fragmentShader = `
    #ifdef GL_ES
    precision mediump float;
    #endif
    uniform float u_time;
    uniform vec2 u_mouse;
    uniform vec2 u_resolution;

    const int MAX_STEPS = 100;//Maximum Step Number
    const float MAX_DIST = 100.0;//Maximum Step Distance
    const float SURF_DIST = 0.01;//Intersection Detection Near Surface Distance


    vec2 opU( vec2 d1, vec2 d2 )
    {
      return (d1.x<d2.x) ? d1 : d2;
    }

    //sphere
    float sdSphere( vec3 p, float s )
    {
      return length(p)-s;
    }

    //Cube
    float sdBox( vec3 p, vec3 b,float rad )
    {
      vec3 d = abs(p) - b;
      return min(max(d.x,max(d.y,d.z)),0.0) + length(max(d,0.0)) - rad;
    }

    //Doughnut
    float sdTorus( vec3 p, vec2 t )
    {
      return length( vec2(length(p.xz)-t.x,p.y) )-t.y;
    }


    vec2 getDistandMaterial(vec3 p){

      float plane = p.y;//ground

      vec2 res = vec2(plane,0.0);

      vec2 sphere = vec2(sdSphere(p-vec3(0,1,5),0.8),1.0);//Sphere, material ID 1.0
      vec2 box = vec2(sdBox(p-vec3(2,1,5),vec3(0.8,0.3,0.6),0.06),2.0);//Square, Material ID 2.0
      vec2 torus = vec2(sdTorus(p-vec3(-2,1,5),vec2(0.6,0.2)),3.0);//Doughnut, material ID 3.0
      
      res = opU(res,sphere);
      res = opU(res,box);
      res = opU(res,torus);

      return res;
    }

    vec2 rayMarch(vec3 rayStart, vec3 rayDirection) {
      float depth=0.;
      float material=0.;
      for(int i=0; i<MAX_STEPS; i++) {
        vec3 p = rayStart + rayDirection*depth;//The coordinates after the last step are the starting point of this step

        vec2 dm = getDistandMaterial(p);
        float dist = dm.x;//Gets the distance when the current starting point of the step intersects the object
        material = dm.y;
        depth += dist; //Step Length Accumulation

        if(depth>MAX_DIST || dist<SURF_DIST) break;//Step distance greater than maximum step distance or less than minimum surface distance from object surface (light entering object) stops moving
      }
      return vec2(depth,material);
    }

    vec3 getNormal(vec3 p){
      return normalize(vec3(
        getDistandMaterial(vec3(p.x + SURF_DIST, p.y, p.z)).x - getDistandMaterial(vec3(p.x - SURF_DIST, p.y, p.z)).x,
        getDistandMaterial(vec3(p.x, p.y + SURF_DIST, p.z)).x - getDistandMaterial(vec3(p.x, p.y - SURF_DIST, p.z)).x,
        getDistandMaterial(vec3(p.x, p.y, p.z  + SURF_DIST)).x - getDistandMaterial(vec3(p.x, p.y, p.z - SURF_DIST)).x
      ));
    }


    //Illumination calculation of Blinn-Phong model
    vec3 calcBlinnPhongLight( vec3 materialColor, vec3 p, vec3 ro) {

      vec3 lightPos = vec3(4.0 * sin(u_time), 20.0, 4.0*cos(u_time));//Light source coordinates

      //Computing ambient light
      float k_a = 0.2;//Ambient light reflection coefficient
      vec3 ambientLight = 0.6 * vec3(1.0, 1.0, 1.0);
      vec3 ambient = k_a*ambientLight;
      
      vec3 N = getNormal(p); //normal
      vec3 L = normalize(lightPos - p); //Illumination direction
      vec3 V = normalize(ro - p); //sight
      vec3 H = normalize(V+L); //Half-range vector

      float r = length(lightPos - p);


      //Calculating diffuse reflectance
      float k_d = 0.6;//Diffuse reflectance
      float dotLN = clamp(dot(L, N),0.0,1.0);//Point multiplies and limits results to 0~1
      vec3 diffuse = k_d * (materialColor/r*r) * dotLN;


      //Calculating high-light reflection
      float k_s = 0.8;//Mirror Reflectance
      float shininess = 160.0;
      vec3 specularColor = vec3(1.0, 1.0, 1.0);
      vec3 specular = k_s * (specularColor/r*r)* pow(clamp(dot(N, H), 0.0, 1.0), shininess);//Calculate highlights
  
      
      //Calculate Shadow
      vec2 res = rayMarch(p + N*SURF_DIST*2.0,L); 
      if(res.x<length(lightPos-p)-0.001){
        diffuse*=0.1;
      }
        
      //Color = ambient light + diffuse light + specular light
      return ambient +diffuse + specular;
    }

    void main( void ) {

      //Window coordinates adjusted to [-1,1], coordinate origin at the center of the screen
      vec2 st = (gl_FragCoord.xy * 2. - u_resolution) / u_resolution.y;


      vec3 ro = vec3(0.0,2.0,0.0);//viewpoint
      vec3 rd = normalize(vec3(st.x,st.y,1.0));//Line of sight direction

      vec2 res = rayMarch(ro,rd);//Inverse ray tracing for intersection distance and material ID

      float d = res.x;//Distance between object and viewpoint
      float m = res.y;//Material ID
  
      vec3 p = ro + rd * d;

      vec3 materialColor = vec3(1.0, 1.0, 1.0);
      
      
      //Set different material colors for different objects
      if(m==0.0){
        materialColor = vec3(.2, 0.0, 0.0);
      }
      if(m==1.0){
        materialColor = vec3(.2, 0.0, 1.0);
      }
      if(m==2.0){
        materialColor = vec3(.7, 0.2, 0.0);
      }
      if(m==3.0){
        materialColor = vec3(.8, .9, 0.0);
      }

      vec3 color = vec3(1.0,1.0,1.0);

      //Calculating illumination using Blinn-Phong model
      color *= calcBlinnPhongLight( materialColor, p, ro);

      gl_FragColor = vec4(color, 1.0);

    }
    `

    init();
    animate();

    function init() {
      container = document.getElementById('container');

      camera = new THREE.Camera();
      camera.position.z = 1;

      scene = new THREE.Scene();

      var geometry = new THREE.PlaneBufferGeometry(2, 2);

      uniforms = {
        u_time: {
          type: "f",
          value: 1.0
        },
        u_resolution: {
          type: "v2",
          value: new THREE.Vector2()
        },
        u_mouse: {
          type: "v2",
          value: new THREE.Vector2()
        }
      };

      var material = new THREE.ShaderMaterial({
        uniforms: uniforms,
        vertexShader: vertexShader,
        fragmentShader: fragmentShader
      });

      var mesh = new THREE.Mesh(geometry, material);
      scene.add(mesh);

      renderer = new THREE.WebGLRenderer();
      //renderer.setPixelRatio(window.devicePixelRatio);

      container.appendChild(renderer.domElement);

      onWindowResize();
      window.addEventListener('resize', onWindowResize, false);

      document.onmousemove = function (e) {
        uniforms.u_mouse.value.x = e.pageX
        uniforms.u_mouse.value.y = e.pageY
      }
    }

    function onWindowResize(event) {
      renderer.setSize(800, 800);
      uniforms.u_resolution.value.x = renderer.domElement.width;
      uniforms.u_resolution.value.y = renderer.domElement.height;
    }

    function animate() {
      requestAnimationFrame(animate);
      render();
    }

    function render() {
      uniforms.u_time.value += 0.02;
      renderer.render(scene, camera);
    }
  </script>
</body>

Topics: webgl GLSL