shader编程-RayMarching三维场景下使用交集并集差集方法CSG建模(WebGL-Shader开发基础10)

Posted 点燃火柴

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了shader编程-RayMarching三维场景下使用交集并集差集方法CSG建模(WebGL-Shader开发基础10)相关的知识,希望对你有一定的参考价值。

1. CSG介绍

上一篇文章学习了模型的基本变换(旋转、缩放、平移),这篇简单说说CSG建模,即构造实体形状,它是一种通过交集、并集、差集的运算用简单的几何形状创建复杂几何形状的方法,下面的图片可以清楚的描述这种方式建模的思路和过程

2. demo效果

sdf-csg


这个demo按照上面的图片中的思路,使用基础形状圆柱、立方体、球体通过交集、并集、差集将它们组合而成

3. 实现过程

3.1 交集、并集、差集函数

之前在二维空间中实现过交集、并集、差集函数,转换到三维空间思路是一样的,不过这一次处理的时候是附带了材质,所以使用x分量即距离进行比较,返回的是输入的其中一个vec2变量,具体如下

//交集
vec2 opI( vec2 d1, vec2 d2 )

  return (d1.x>d2.x) ? d1 : d2;
   

//并集
vec2 opU( vec2 d1, vec2 d2 )

  return (d1.x<d2.x) ? d1 : d2;


//差集
vec2 opS( vec2 d1, vec2 d2 )

  return opI(d1,-d2);

3.2 旋转矩阵

旋转矩阵与上一篇文章中的一模一样,直接搬过来,如下

//绕z轴旋转矩阵
mat4 rotZ(float a) 
  return mat4(cos(a),-sin(a),0.0,0.0,
              sin(a),cos(a),0.0,0.0,
              0.0,0.0,1.0,0.0,
              0.0,0.0,0.0,1.0
              );


//绕x轴旋转矩阵
mat4 rotX(float a) 
  return mat4(1.0,0.0,0.0,0.0,
              0.0,cos(a),-sin(a),0.0,
              0.0,sin(a),cos(a),0.0,
              0.0,0.0,0.0,1.0
            );


//绕y轴旋转矩阵
mat4 rotY(float a) 
  return mat4(cos(a),0.0,sin(a),0.0,
              0.0,1.0,0.0,0.0,
              -sin(a),0.0,cos(a),0.0,
              0.0,0.0,0.0,1.0
            );
 

3.3 基础形状

我们这用到的基础形状是圆柱、立方体、球体,它们的sdf函数如下

//球体
float sdSphere( vec3 p, float s )

  return length(p)-s;


//立方体
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;


//圆柱
float sdCylinder( vec3 p, float h, float r )

  vec2 d = abs(vec2(length(p.xz),p.y)) - vec2(r,h);
  return min(max(d.x,d.y),0.0) + length(max(d,0.0));

3.4 组合过程

3.4.1 互相垂直圆柱实现

相交圆柱的实现就是绘制三根相互垂直的圆柱,然后用并集函数将它们合并,绘制过程是首先绘制出一根圆柱,然后将坐标系绕Z轴旋转90度绘制第二个圆柱,接着再将坐标系绕X轴旋转90度绘制第三根圆柱,最后合并它们,实现过程如下

//绘制三根垂直相交的圆柱
vec3 pos = p-vec3(0,2,5);//确定模型的中心
vec4 cylindersPos = vec4(pos,1.0);//转为其次坐标
cylindersPos*=rotY(u_time);//旋转

vec2 cylinder1 = vec2(sdCylinder(cylindersPos.xyz,2.0,0.6),1.0);

float angle = radians(90.0);//角度转为弧度

cylindersPos*=rotZ(angle);
vec2 cylinder2 = vec2(sdCylinder(cylindersPos.xyz,2.0,0.6),1.0);

cylindersPos*=rotX(angle);
vec2 cylinder3 = vec2(sdCylinder(cylindersPos.xyz,2.0,0.6),1.0);

// 三根圆柱取交集
vec2 cylinderRes  = opU(cylinder1,cylinder2);
cylinderRes  = opU(cylinderRes,cylinder3);

绘制结果

3.4.2 相交的球体与立方体

这一步比较简单,就是绘制球体与立方体然后求交集

pos = p-vec3(0,2,5);//屏幕坐标重置
vec4 boxPos = vec4(pos,1.0);//转为其次坐标
boxPos*=rotY(u_time);//旋转

vec2 box = vec2(sdBox(boxPos.xyz,vec3(1.0),0.06),2.0);//方块,材质ID为2.0

vec2 sphere = vec2(sdSphere(boxPos.xyz,1.4),3.0);//方块,材质ID为2.0

vec2 boxSphere = opI(box,sphere);//方块和球取交集

绘制结果

3.4.3 二者取差集

这一步只需要将上面两步取差集,返回即可

 vec2 res = opS(boxSphere,cylinderRes);//方块和球交集结果与三个垂直圆柱取差集

 res = opU(plane,res); //差集结果与地板取交集

 return res;

绘制结果

你会发现被掏空的地方是粉色,这个色来自哪里呢,来自main函数中的默认材质色

vec3 materialColor = vec3(1.0, 0.0, 1.0);//默认材质色,使用差集计算出来的内壁会使用该色填充

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;//相交检测临近表面距离

    //绕z轴旋转矩阵
    mat4 rotZ(float a) 
      return mat4(cos(a),-sin(a),0.0,0.0,
                  sin(a),cos(a),0.0,0.0,
                  0.0,0.0,1.0,0.0,
                  0.0,0.0,0.0,1.0
                  );
    
    
    //绕x轴旋转矩阵
    mat4 rotX(float a) 
      return mat4(1.0,0.0,0.0,0.0,
                  0.0,cos(a),-sin(a),0.0,
                  0.0,sin(a),cos(a),0.0,
                  0.0,0.0,0.0,1.0
                );
    
    
    //绕y轴旋转矩阵
    mat4 rotY(float a) 
      return mat4(cos(a),0.0,sin(a),0.0,
                  0.0,1.0,0.0,0.0,
                  -sin(a),0.0,cos(a),0.0,
                  0.0,0.0,0.0,1.0
                );
     


    //交集
    vec2 opI( vec2 d1, vec2 d2 )
    
      return (d1.x>d2.x) ? d1 : d2;
       

    //并集
    vec2 opU( vec2 d1, vec2 d2 )
    
      return (d1.x<d2.x) ? d1 : d2;
    

    //差集
    vec2 opS( vec2 d1, vec2 d2 )
    
      return opI(d1,-d2);
    

    //球体
    float sdSphere( vec3 p, float s )
    
      return length(p)-s;
    

    //立方体
    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;
    

    //圆柱
    float sdCylinder( vec3 p, float h, float r )
    
      vec2 d = abs(vec2(length(p.xz),p.y)) - vec2(r,h);
      return min(max(d.x,d.y),0.0) + length(max(d,0.0));
    

    vec2 getDistandMaterial(vec3 p)

      vec2 plane = vec2(p.y,0.0);//地面
 
      //绘制三根垂直相交的圆柱
      vec3 pos = p-vec3(0,2,5);//确定模型的中心
      vec4 cylindersPos = vec4(pos,1.0);//转为其次坐标
      cylindersPos*=rotY(u_time);//旋转

      vec2 cylinder1 = vec2(sdCylinder(cylindersPos.xyz,2.0,0.6),1.0);

      float angle = radians(90.0);//角度转为弧度

      cylindersPos*=rotZ(angle);
      vec2 cylinder2 = vec2(sdCylinder(cylindersPos.xyz,2.0,0.6),1.0);

      cylindersPos*=rotX(angle);
      vec2 cylinder3 = vec2(sdCylinder(cylindersPos.xyz,2.0,0.6),1.0);

      // 三根圆柱取交集
      vec2 cylinderRes  = opU(cylinder1,cylinder2);
      cylinderRes  = opU(cylinderRes,cylinder3);


      pos = p-vec3(0,2,5);//屏幕坐标重置
      vec4 boxPos = vec4(pos,1.0);//转为其次坐标
      boxPos*=rotY(u_time);//旋转

      vec2 box = vec2(sdBox(boxPos.xyz,vec3(1.0),0.06),2.0);//方块,材质ID为2.0

      vec2 sphere = vec2(sdSphere(boxPos.xyz,1.4),3.0);//方块,材质ID为2.0

      vec2 boxSphere = opI(box,sphere);//方块和球取交集

      vec2 res = opS(boxSphere,cylinderRes);//方块和球交集结果与三个垂直圆柱取差集

      res = opU(plane,res); //差集结果与地板取交集

      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;//上一次步进结束后的坐标也就是这一次步进出发点

        vec2 dm = getDistandMaterial(p);
        float dist = dm.x;//获取当前步进出发点与物体相交时距离
        material = dm.y;
        depth += dist; //步进长度累加

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


    //Blinn-Phong模型光照计算
    vec3 calcBlinnPhongLight( vec3 materialColor, vec3 p, vec3 ro) 

      vec3 lightPos = vec3(5.0 * sin(u_time), 20.0, 10.0*cos(u_time)-18.);//光源坐标

      //计算环境光
      float k_a = 0.3;//环境光反射系数
      vec3 ambientLight = 0.6 * vec3(1.0, 1.0, 1.0);
      vec3 ambient = k_a*ambientLight;
      
      vec3 N = getNormal(p); //法线
      vec3 L = normalize(lightPos - p); //光照方向
      vec3 V = normalize(ro - p); //视线
      vec3 H = normalize(V+L); //半程向量

      float r = length(lightPos - p);


      //计算漫反射光
      float k_d = 0.6;//漫反射系数
      float dotLN = clamp(dot(L, N),0.0,1.0);//点乘,并将结果限定在0~1
      vec3 diffuse = k_d * (materialColor/r*r) * dotLN;


      //计算高光反射光
      float k_s = 0.8;//镜面反射系数
      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);//计算高光
  
      
      //计算阴影
      vec2 res = rayMarch(p + N*SURF_DIST*2.0,L); 
      if(res.x<length(lightPos-p)-0.001)
        diffuse*=0.1;
      
        
      //颜色 =  环境光 + 漫反射光 + 镜面反射光
      return ambient +diffuse + specular;
    

    void main( void ) 

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


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

      vec2 res = rayMarch(ro,rd);//反向光线追踪求交点距离与材质ID

      float d = res.x;//物体与视点的距离
      float m = res.y;//材质ID
  
      vec3 p = ro + rd * d;

      vec3 materialColor = vec3(1.0, 0.0, 1.0);//默认材质色,使用差集计算出来的内壁会使用该色填充
      
      
      //为不同物体设置不同的材质颜色
      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);

      //使用Blinn-Phong模型计算光照
      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>

以上是关于shader编程-RayMarching三维场景下使用交集并集差集方法CSG建模(WebGL-Shader开发基础10)的主要内容,如果未能解决你的问题,请参考以下文章

shader编程-RayMarching三维场景下模型基本变换旋转缩放平移(WebGL-Shader开发基础09)

shader编程-RayMarching三维场景下使用交集并集差集方法CSG建模(WebGL-Shader开发基础10)

shader编程-RayMarching三维场景下使用交集并集差集方法CSG建模(WebGL-Shader开发基础10)

shader编程-RayMarching三维场景下使用交集并集差集方法CSG建模(WebGL-Shader开发基础10)

shader编程-RayMarching与Blinn-Phong光照模型搭建三维场景(WebGL-Shader开发基础08)

shader编程-RayMarching与SDF搭建三维场景实现Blinn-Phong光照(WebGL-Shader开发基础08)