shader编程-2D基本图形SDF(有向距离场)介绍与使用(WebGL-Shader开发基础05)

  • Post author:
  • Post category:其他




1. 有向距离场介绍

有向距离场,英文名Signed Distance Field,简称SDF,把空间中与物体表面的距离进行采样,使用负值表示物体内部,使用正值表示物体外部,与物体的表面的距离为0,大于0,小于0和等于0区分物体的区域,你很快可以联想到之前使用的step函数和smoothstep函数,是的,通过与这两个函数的配合,就可以像前面样绘制图形了



2. SDF绘制圆

先从最简单的圆开始,圆的定义就是与圆心的距离小于半径的集合,这时要祭出一个大神的网站,其中一个页面罗列了各种二维SDF的绘制方案,具体请到这里查阅

2dSDF

float sdCircle( vec2 p, float r )
{
  return length(p) - r;// 到圆表面的距离
}

调用

    void main( void ) {

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

      vec3 line_color = vec3(1.0,1.0,0.0);
      vec3 color = vec3(0.6);//背景色
      float pct = 0.0;

      pct = sdCircle(st,0.5);
      
      color = mix(color,line_color,pct);
      gl_FragColor = vec4(color, 1.0);
    }

执行结果,怎么是这样,没错,开始说过物体SDF返回的距离,内部小于0,外部大于0,表面等于0,所以没毛病

在这里插入图片描述

要想展示我们熟悉的圆,使用smoothstep施加一点小小的魔法,具体如下

//color = mix(color,line_color,pct);
color = mix(color,line_color,1.0-smoothstep(0.0,0.01,pct));

再来看看结果,一个黄色的圆出现在绘图区域了

在这里插入图片描述



3. SDF绘制矩形



3.1 绘制矩形

绘制矩形的SDF函数

float sdBox( in vec2 p, in vec2 b )
{
  vec2 d = abs(p)-b;
  return length(max(d,0.0)) + min(max(d.x,d.y),0.0);
}

调用

...
pct = sdBox(st,vec2(0.3));
color = mix(color,line_color,1.0-smoothstep(0.0,0.01,pct));
gl_FragColor = vec4(color, 1.0);

执行结果

在这里插入图片描述



3.2 绘制带圆角矩形

如果想给矩形加个圆角该怎么做呢?只需要将SDF结果减去一个数,这个数就是圆角的半径,在sdBox函数的基础上多加一个参数传入圆角半径

float sdBoxRadius( in vec2 p, in vec2 b,float radius )
{
  vec2 d = abs(p)-b;
  return length(max(d,0.0)) + min(max(d.x,d.y),0.0) - radius;
}

调用

...
pct = sdBoxRadius(st,vec2(0.3),0.02);
color = mix(color,line_color,1.0-smoothstep(0.0,0.01,pct));
gl_FragColor = vec4(color, 1.0);

执行结果,直角变成了圆角

在这里插入图片描述



3.3 通过两端和宽度绘制矩形

通过两端和宽度绘制矩形的SDF函数需要输入四个参数,第一个是屏幕坐标,第二个是矩形的起点,第三个是矩形的终点,第四个参数是矩形的宽度,如下

//通过端点和宽度绘制矩形
float sdOrientedBox( in vec2 p, in vec2 a, in vec2 b, float th )
{
  float l = length(b-a);
  vec2  d = (b-a)/l;
  vec2  q = (p-(a+b)*0.5);
        q = mat2(d.x,-d.y,d.y,d.x)*q;
        q = abs(q)-vec2(l,th)*0.5;
  return length(max(q,0.0)) + min(max(q.x,q.y),0.0);    
}

调用

...
pct = sdOrientedBox(st,vec2(-0.3),vec2(0.3,0.6),0.1);
color = mix(color,line_color,1.0-smoothstep(0.0,0.01,pct));
gl_FragColor = vec4(color, 1.0);

执行结果

在这里插入图片描述



4. SDF绘制线段

绘制线段的SDF函数需要输入三个参数,第一个是屏幕坐标,第二个是线段的起点,第三个是线段的终点,具体如下

float sdSegment( in vec2 p, in vec2 a, in vec2 b )
{
    vec2 pa = p-a, ba = b-a;
    float h = clamp( dot(pa,ba)/dot(ba,ba), 0.0, 1.0 );
    return length( pa - ba*h );
}

调用

...
pct = sdSegment(st,vec2(-0.3),vec2(0.3,0.6));
color = mix(color,line_color,1.0-smoothstep(0.0,0.01,pct));
gl_FragColor = vec4(color, 1.0);

执行结果

在这里插入图片描述



5. SDF绘制菱形

绘制菱形的SDF函数需要输入两个参数,第一个是屏幕坐标,第二个描述菱形的尺寸,是一个二维向量,x分量表示菱形顶点x轴上距离坐标中心的距离,y分量表示y轴上距离坐标中心的距离,如下

float ndot(vec2 a, vec2 b ) { return a.x*b.x - a.y*b.y; }
float sdRhombus( in vec2 p, in vec2 b ) 
{
  p = abs(p);
  float h = clamp( ndot(b-2.0*p,b)/dot(b,b), -1.0, 1.0 );
  float d = length( p-0.5*b*vec2(1.0-h,1.0+h) );
  return d * sign( p.x*b.y + p.y*b.x - b.x*b.y );
}

调用

...
pct = sdRhombus(st,vec2(0.5,0.3));
color = mix(color,line_color,1.0-smoothstep(0.0,0.01,pct));
gl_FragColor = vec4(color, 1.0);

执行结果

在这里插入图片描述



6. SDF绘制梯形

绘制梯形的SDF函数需要输入四个参数,第一个是屏幕坐标,第二个参数为梯形下底在x轴正方向的宽度第三个参数为梯形上底在x轴正方向的宽度,第四个参数表示梯形在y轴正方向的高度,具体实现如下

float dot2(in vec2 v ) { return dot(v,v); }
float sdTrapezoid( in vec2 p, in float r1, float r2, float he )
{
  vec2 k1 = vec2(r2,he);
  vec2 k2 = vec2(r2-r1,2.0*he);
  p.x = abs(p.x);
  vec2 ca = vec2(p.x-min(p.x,(p.y<0.0)?r1:r2), abs(p.y)-he);
  vec2 cb = p - k1 + k2*clamp( dot(k1-p,k2)/dot2(k2), 0.0, 1.0 );
  float s = (cb.x<0.0 && ca.y<0.0) ? -1.0 : 1.0;
  return s*sqrt( min(dot2(ca),dot2(cb)) );
}

调用

...
pct = sdTrapezoid(st,0.9,0.3,0.5);
color = mix(color,line_color,1.0-smoothstep(0.0,0.01,pct));
gl_FragColor = vec4(color, 1.0);

执行结果

在这里插入图片描述



7. SDF绘制三角形

绘制三角形的SDF函数需要输入四个参数,第一个是屏幕坐标,第二个参数为三角形左下角顶点坐标,第三个参数为三角形右下角顶点坐标,第四个参数表示三角形顶部顶点坐标,具体实现如下

float sdTriangle( in vec2 p, in vec2 p0, in vec2 p1, in vec2 p2 )
{
  vec2 e0 = p1-p0, e1 = p2-p1, e2 = p0-p2;
  vec2 v0 = p -p0, v1 = p -p1, v2 = p -p2;
  vec2 pq0 = v0 - e0*clamp( dot(v0,e0)/dot(e0,e0), 0.0, 1.0 );
  vec2 pq1 = v1 - e1*clamp( dot(v1,e1)/dot(e1,e1), 0.0, 1.0 );
  vec2 pq2 = v2 - e2*clamp( dot(v2,e2)/dot(e2,e2), 0.0, 1.0 );
  float s = sign( e0.x*e2.y - e0.y*e2.x );
  vec2 d = min(min(vec2(dot(pq0,pq0), s*(v0.x*e0.y-v0.y*e0.x)),
                  vec2(dot(pq1,pq1), s*(v1.x*e1.y-v1.y*e1.x))),
                  vec2(dot(pq2,pq2), s*(v2.x*e2.y-v2.y*e2.x)));
  return -sqrt(d.x)*sign(d.y);
}

调用

...
pct = sdTriangle(st,vec2(-0.3,0.0),vec2(0.3,0.0),vec2(0.0,0.5));
color = mix(color,line_color,1.0-smoothstep(0.0,0.01,pct));
gl_FragColor = vec4(color, 1.0);

执行结果

在这里插入图片描述



8. SDF绘制水滴形

绘制水滴形的SDF函数需要输入四个参数,分别表示屏幕坐标,底圆半径,上圆半径,高度

,具体实现如下

float sdUnevenCapsule( vec2 p, float r1, float r2, float h )
{
  p.x = abs(p.x);
  float b = (r1-r2)/h;
  float a = sqrt(1.0-b*b);
  float k = dot(p,vec2(-b,a));
  if( k < 0.0 ) return length(p) - r1;
  if( k > a*h ) return length(p-vec2(0.0,h)) - r2;
  return dot(p, vec2(a,b) ) - r1;
}

调用

...
pct = sdUnevenCapsule(st,0.3,0.2,0.6);
color = mix(color,line_color,1.0-smoothstep(0.0,0.01,pct));
gl_FragColor = vec4(color, 1.0);

执行结果

在这里插入图片描述



9. SDF绘制扇形

绘制扇形的SDF函数需要输入三个参数,第一个是屏幕坐标,第二个参数用来确定扇形的大小,由x分量和y分量可以确定极坐标下第一象限的角度,第三个参数为扇形圆的半径,具体实现如下

float sdPie( in vec2 p, in vec2 c, in float r )
{
  p.x = abs(p.x);
  float l = length(p) - r;
  float m = length(p-c*clamp(dot(p,c),0.0,r)); // c=sin/cos of aperture
  return max(l,m*sign(c.y*p.x-c.x*p.y));
}

调用

...
pct = sdPie(st,vec2(0.3,0.1),0.8);
color = mix(color,line_color,1.0-smoothstep(0.0,0.01,pct));
gl_FragColor = vec4(color, 1.0);

执行结果

在这里插入图片描述



10. SDF绘制图形圆角

前面的图形中我们绘制了一个圆角矩形,那么SDF图形是否都具有这个性质呢,肯定的都具有这个性质,即只将SDF结果减去一个数,这个数就是对应图形圆角的半径,例如我们给绘制三角的SDF函数加多一个参数表示圆角,返回时减去它

float sdTriangleRadius( in vec2 p, in vec2 p0, in vec2 p1, in vec2 p2,float cornerRadius )
{
  vec2 e0 = p1-p0, e1 = p2-p1, e2 = p0-p2;
  vec2 v0 = p -p0, v1 = p -p1, v2 = p -p2;
  vec2 pq0 = v0 - e0*clamp( dot(v0,e0)/dot(e0,e0), 0.0, 1.0 );
  vec2 pq1 = v1 - e1*clamp( dot(v1,e1)/dot(e1,e1), 0.0, 1.0 );
  vec2 pq2 = v2 - e2*clamp( dot(v2,e2)/dot(e2,e2), 0.0, 1.0 );
  float s = sign( e0.x*e2.y - e0.y*e2.x );
  vec2 d = min(min(vec2(dot(pq0,pq0), s*(v0.x*e0.y-v0.y*e0.x)),
                  vec2(dot(pq1,pq1), s*(v1.x*e1.y-v1.y*e1.x))),
                  vec2(dot(pq2,pq2), s*(v2.x*e2.y-v2.y*e2.x)));
  return -sqrt(d.x)*sign(d.y) -cornerRadius;
}

调用

...
pct = sdTriangleRadius(st,vec2(-0.3,0.0),vec2(0.3,0.0),vec2(0.0,0.5),0.05);
color = mix(color,line_color,1.0-smoothstep(0.0,0.01,pct));
gl_FragColor = vec4(color, 1.0);

结果如下,与原三角形相比多出了圆角,其他图形请大家自行尝试

在这里插入图片描述



11. SDF绘制空心图形

如果你想绘制一个空心的SDF图形,可以参照以下的格式来实现,只需要将绘制图形的SDF函数替换为下面的sdShape

float opOnion( in vec2 p, in float r )
{
  return abs(sdShape(p)) - r;
}

例如我们绘制一个空心梯形,这次直接使用刚刚绘制梯形的函数

float opOnion( in vec2 p, in float r )
{
  return abs(sdTrapezoid(p,0.9,0.3,0.5)) - r;
}

调用

...
pct = opOnion(st,0.02);
color = mix(color,line_color,1.0-smoothstep(0.0,0.01,pct));
gl_FragColor = vec4(color, 1.0);

执行结果

在这里插入图片描述



12. 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;


    float sdCircle( vec2 p, float r )
    {
      return length(p) - r;// 到圆表面的距离
    }

    float sdBox( in vec2 p, in vec2 b )
    {
      vec2 d = abs(p)-b;
      return length(max(d,0.0)) + min(max(d.x,d.y),0.0);
    }

    float sdBoxRadius( in vec2 p, in vec2 b,float radius )
    {
      vec2 d = abs(p)-b;
      return length(max(d,0.0)) + min(max(d.x,d.y),0.0) - radius;
    }
    //通过端点和宽度绘制矩形
    float sdOrientedBox( in vec2 p, in vec2 a, in vec2 b, float th )
    {
      float l = length(b-a);
      vec2  d = (b-a)/l;
      vec2  q = (p-(a+b)*0.5);
            q = mat2(d.x,-d.y,d.y,d.x)*q;
            q = abs(q)-vec2(l,th)*0.5;
      return length(max(q,0.0)) + min(max(q.x,q.y),0.0);    
    }

    //线段
    float sdSegment( in vec2 p, in vec2 a, in vec2 b )
    {
      vec2 pa = p-a, ba = b-a;
      float h = clamp( dot(pa,ba)/dot(ba,ba), 0.0, 1.0 );
      return length( pa - ba*h );
    }

    //菱形
    float ndot(vec2 a, vec2 b ) { return a.x*b.x - a.y*b.y; }
    float sdRhombus( in vec2 p, in vec2 b ) 
    {
      p = abs(p);
      float h = clamp( ndot(b-2.0*p,b)/dot(b,b), -1.0, 1.0 );
      float d = length( p-0.5*b*vec2(1.0-h,1.0+h) );
      return d * sign( p.x*b.y + p.y*b.x - b.x*b.y );
    }

    //梯形
    float dot2(in vec2 v ) { return dot(v,v); }
    float sdTrapezoid( in vec2 p, in float r1, float r2, float he )
    {
      vec2 k1 = vec2(r2,he);
      vec2 k2 = vec2(r2-r1,2.0*he);
      p.x = abs(p.x);
      vec2 ca = vec2(p.x-min(p.x,(p.y<0.0)?r1:r2), abs(p.y)-he);
      vec2 cb = p - k1 + k2*clamp( dot(k1-p,k2)/dot2(k2), 0.0, 1.0 );
      float s = (cb.x<0.0 && ca.y<0.0) ? -1.0 : 1.0;
      return s*sqrt( min(dot2(ca),dot2(cb)) );
    }

    //三角形
    float sdTriangle( in vec2 p, in vec2 p0, in vec2 p1, in vec2 p2 )
    {
      vec2 e0 = p1-p0, e1 = p2-p1, e2 = p0-p2;
      vec2 v0 = p -p0, v1 = p -p1, v2 = p -p2;
      vec2 pq0 = v0 - e0*clamp( dot(v0,e0)/dot(e0,e0), 0.0, 1.0 );
      vec2 pq1 = v1 - e1*clamp( dot(v1,e1)/dot(e1,e1), 0.0, 1.0 );
      vec2 pq2 = v2 - e2*clamp( dot(v2,e2)/dot(e2,e2), 0.0, 1.0 );
      float s = sign( e0.x*e2.y - e0.y*e2.x );
      vec2 d = min(min(vec2(dot(pq0,pq0), s*(v0.x*e0.y-v0.y*e0.x)),
                      vec2(dot(pq1,pq1), s*(v1.x*e1.y-v1.y*e1.x))),
                      vec2(dot(pq2,pq2), s*(v2.x*e2.y-v2.y*e2.x)));
      return -sqrt(d.x)*sign(d.y);
    }

    float sdTriangleRadius( in vec2 p, in vec2 p0, in vec2 p1, in vec2 p2,float cornerRadius )
    {
      vec2 e0 = p1-p0, e1 = p2-p1, e2 = p0-p2;
      vec2 v0 = p -p0, v1 = p -p1, v2 = p -p2;
      vec2 pq0 = v0 - e0*clamp( dot(v0,e0)/dot(e0,e0), 0.0, 1.0 );
      vec2 pq1 = v1 - e1*clamp( dot(v1,e1)/dot(e1,e1), 0.0, 1.0 );
      vec2 pq2 = v2 - e2*clamp( dot(v2,e2)/dot(e2,e2), 0.0, 1.0 );
      float s = sign( e0.x*e2.y - e0.y*e2.x );
      vec2 d = min(min(vec2(dot(pq0,pq0), s*(v0.x*e0.y-v0.y*e0.x)),
                      vec2(dot(pq1,pq1), s*(v1.x*e1.y-v1.y*e1.x))),
                      vec2(dot(pq2,pq2), s*(v2.x*e2.y-v2.y*e2.x)));
      return -sqrt(d.x)*sign(d.y) -cornerRadius;
    }

    float sdUnevenCapsule( vec2 p, float r1, float r2, float h )
    {
      p.x = abs(p.x);
      float b = (r1-r2)/h;
      float a = sqrt(1.0-b*b);
      float k = dot(p,vec2(-b,a));
      if( k < 0.0 ) return length(p) - r1;
      if( k > a*h ) return length(p-vec2(0.0,h)) - r2;
      return dot(p, vec2(a,b) ) - r1;
    }

    //扇形
    float sdPie( in vec2 p, in vec2 c, in float r )
    {
      p.x = abs(p.x);
      float l = length(p) - r;
      float m = length(p-c*clamp(dot(p,c),0.0,r)); // c=sin/cos of aperture
      return max(l,m*sign(c.y*p.x-c.x*p.y));
    }


    float opOnion( in vec2 p, in float r )
    {
      return abs(sdTrapezoid(p,0.9,0.3,0.5)) - r;
    }

    void main( void ) {

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

      vec3 line_color = vec3(1.0,1.0,0.0);
      vec3 color = vec3(0.6);//背景色
      float pct = 0.0;


      pct = sdCircle(st,0.5);

      pct = sdBox(st,vec2(0.3));

      pct = sdBoxRadius(st,vec2(0.3),0.02);

      pct = sdOrientedBox(st,vec2(-0.3),vec2(0.3,0.6),0.1);

      pct = sdSegment(st,vec2(-0.3),vec2(0.3,0.6));

      pct = sdRhombus(st,vec2(0.5,0.3));

      pct = sdTrapezoid(st,0.9,0.3,0.5);

      pct = sdTriangle(st,vec2(-0.3,0.0),vec2(0.3,0.0),vec2(0.0,0.5));

      pct = sdUnevenCapsule(st,0.3,0.2,0.6);

      pct = sdPie(st,vec2(0.3,0.1),0.8);

      pct = sdTriangleRadius(st,vec2(-0.3,0.0),vec2(0.3,0.0),vec2(0.0,0.5),0.05);
      
      pct = opOnion(st,0.02);

      //color = mix(color,line_color,pct);
      color = mix(color,line_color,1.0-smoothstep(0.0,0.01,pct));
      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>



版权声明:本文为qw8704149原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。