shader编程-RayMarching与SDF结合开始三维探索(WebGL-Shader开发基础07)

1. 初识RayMarching

人们之所以可以看到物体,是因为光线从光源发出照射到物体,之后反射进入人们的眼睛,这样就看到了物体,在三维模型中使模型呈现在场景中,光线从光源出发,照射到物体,反射后到达相机,反之,同样可以从相机出发光线摄像物体之后反射到光源,RayMarching 算法就是从相机发射光线,具体请看下图
在这里插入图片描述
从相机出发,向屏幕中的每一个像素发射一条光线,以其中一条光线为例,光线开始以一定的步长前进,每前进一步判断是否达到物体表面,如果没有继续前进,一直到到达物体表面或者没有与物体相交一直到我们设定的最大距离,则光线停止前进。结束后可以得到光线与物体相交的坐标,然后通过光照信息与交点信息计算物体表面颜色,接下来一步一步实现

2. 实现过程

2.1 光线步进函数

在这里插入图片描述

如上图,光线一次一次步进,直到达到物体表面停止,每次步进的距离是当前点与物体表面的最小距离,图中的dist0是第一次步进的距离,该距离是视点与物体表面的最小距离,第二次步进的距离dist1是第一次步进结束点与物体表面的最近距离,依次类推直到沿着视线到达物体表面
开始书写光线步进函数,我们定义了三个常量分别表示:MAX_STEPS(最大步进步数)、MAX_DIST (最大步进距离)、SURF_DIST (相交检测临近表面距离),最大步进步数用来控制光线步进的次数,为了限定一个循环范围,光线可以向远处无限传播,但是我们不能无止休的计算下去,超出物体范围没有任何意义,只会浪费计算机性能,最大步进距离也是用来限制光线步进,这一次是按步进距离限制,当光线累计步进超过这个距离时也停止前进,SURF_DIST用来检测光线与物体表面的距离,光线沿着一个方向步进,与物体表面的距离越来越近,当达到某一个较小的值时,我们就可以认为它们相交了,光线同样停止前进。

具体请参照以下代码

const int MAX_STEPS = 100;//最大步进步数
const float MAX_DIST = 100.0;//最大步进距离
const float SURF_DIST = 0.01;//相交检测临近表面距离

float rayMarch(vec3 rayStart, vec3 rayDirection) {
    
    
  float depth=0.;
  
  for(int i=0; i<MAX_STEPS; i++) {
    
    
      vec3 p = rayStart + rayDirection*depth;//上一次步进结束后的坐标也就是这一次步进出发点

      float dist = getDist(p);//获取当前步进出发点与物体相交时距离
      depth += dist; //步进长度累加

      if(depth>MAX_DIST || dist<SURF_DIST) break;//步进距离大于最大步进距离或与物体表面距离小于最小表面距离(光线进入物体)停止前进
  }
  return depth;
}

2.2 获取物体表面距离

上一步中用到一个函数getDist,计算物体表面距离,在这里简单说一下,这个函数实际上是给场景中添加物体,主要添加了一个球体和一个地面,之前学习了二维了一些SDF实现,从最简单圆开始学习,三维的SDF也从比较简单的球体开始,与二维的相比,只是输入参数由二维变成三维,即通过length计算空间中点到中心点的距离,返回满足指定距离的集合,就是球体的SDF函数,还有一个平面函数,直接取y值为特定值即可,最后使用min函数对它们求并集返回,具体如下

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

float getDist(vec3 p){
    
    

  vec3 spherrCenter = vec3(0,1,5);
  float sphereRadius = 1.0;
  float dist = sdSphere(p-spherrCenter,sphereRadius);//球体
  float plane = p.y;//地面
  return min(dist,plane);// min 函数求并集然后返回
}

2.3 计算光照阴影

这里计算光照只计算一下漫反射,getLight函数的前半部分就是用来计算漫反射的,首先定义了一个光源坐标,为了让光源旋转给光源的x分量和z分量分别累加sin(u_time)和cos(u_time),光线的方向用物体用光源坐标减去物体表面坐标即可,然后通过getNormal函数获取物体表面的法线,之后用光线与法线做点乘运算,并使用clamp函数将结果限制在0~1范围内
计算阴影部分,这一次光线需要从物体表面出发,向着光源方向做步进,若步进过程中被物体物体遮挡,则计算阴影,具体实现过程如下

//计算表面法线
vec3 getNormal(vec3 p){
    
    
    return normalize(vec3(
      getDist(vec3(p.x + SURF_DIST, p.y, p.z)) - getDist(vec3(p.x - SURF_DIST, p.y, p.z)),
      getDist(vec3(p.x, p.y + SURF_DIST, p.z)) - getDist(vec3(p.x, p.y - SURF_DIST, p.z)),
      getDist(vec3(p.x, p.y, p.z  + SURF_DIST)) - getDist(vec3(p.x, p.y, p.z - SURF_DIST))
    ));

}

float getLight(vec3 p){
    
    
  vec3 lightPos = vec3(0,5,7);
  lightPos.xz += vec2(sin(u_time),cos(u_time));
  vec3 light = normalize(lightPos-p);
  vec3 normal = getNormal(p);

  float diffuse = clamp(dot(normal,light),0.0,1.0);//限制在0~1

  //计算阴影
  float d = rayMarch(p + normal*SURF_DIST*2.0,light); 
  if(d<length(lightPos-p)){
    
    
    diffuse*=0.1;
  }
  return diffuse;
}

2.4 物体表面距离计算

物体表面距离在main函数中实现,首先需要定义视点(摄像机) 坐标ro,与视线方向rd,然后调用写好的步进函数rayMarch(ro,rd),然后通过返回的结果计算出光线与物体的相交点坐标,用相交点坐标调用getLight函数计算光照和阴影,具体如下

void main( void ) {
    
    

  //窗口坐标调整为[-1,1],坐标原点在屏幕中心
  vec2 st = (gl_FragCoord.xy * 2. - u_resolution) / u_resolution.y;

  vec3 color = vec3(0.6);//背景色

  vec3 ro = vec3(0.0,1.0,0.0);//视点
  vec3 rd = normalize(vec3(st.x,st.y,1.0));//光线方向

  float d = rayMarch(ro,rd);

  vec3 p = ro + rd * d;
  float diffuse = getLight(p);
  color = vec3(diffuse);

  gl_FragColor = vec4(color, 1.0);

}

3 demo运行结果

在这里插入图片描述
运行结果贴了图片,实际上光照会旋转,结果比较简单,但也算是从二维转向三维了,这只是揭开了序幕,只是三维shader的hello world版,革命尚未成功,同志仍需努力啊

4 demo代码

<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;//最大步进步数
    const float MAX_DIST = 100.0;//最大步进距离
    const float SURF_DIST = 0.01;//相交检测临近表面距离

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

    float getDist(vec3 p){

      vec3 spherrCenter = vec3(0,1,5);
      float sphereRadius = 1.0;
      float dist = sdSphere(p-spherrCenter,sphereRadius);//球体
      float plane = p.y;//地面
      return min(dist,plane);// min 函数求并集然后返回
    }

    float rayMarch(vec3 rayStart, vec3 rayDirection) {
      float depth=0.;
      
      for(int i=0; i<MAX_STEPS; i++) {
          vec3 p = rayStart + rayDirection*depth;//上一次步进结束后的坐标也就是这一次步进出发点

          float dist = getDist(p);//获取当前步进出发点与物体相交时距离
          depth += dist; //步进长度累加

          if(depth>MAX_DIST || dist<SURF_DIST) break;//步进距离大于最大步进距离或与物体表面距离小于最小表面距离(光线进入物体)停止前进
      }
      return depth;
    }

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

    }

    float getLight(vec3 p){
      vec3 lightPos = vec3(0,5,7);
      lightPos.xz += vec2(sin(u_time),cos(u_time));
      vec3 light = normalize(lightPos-p);
      vec3 normal = getNormal(p);

      float diffuse = clamp(dot(normal,light),0.0,1.0);//限制在0~1

      //计算阴影
      float d = rayMarch(p + normal*SURF_DIST*2.0,light); 
      if(d<length(lightPos-p)){
        diffuse*=0.1;
      }
      return diffuse;
    }

  

    void main( void ) {

      //窗口坐标调整为[-1,1],坐标原点在屏幕中心
      vec2 st = (gl_FragCoord.xy * 2. - u_resolution) / u_resolution.y;

      vec3 color = vec3(0.6);//背景色

      vec3 ro = vec3(0.0,1.0,0.0);//视点
      vec3 rd = normalize(vec3(st.x,st.y,1.0));//视线方向

      float d = rayMarch(ro,rd);
  
      vec3 p = ro + rd * d;
      float diffuse = getLight(p);//漫反射光计算
      color = vec3(diffuse);

      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>

猜你喜欢

转载自blog.csdn.net/qw8704149/article/details/121551045