WebGL Shader 造型平面函数(一)

1,120 阅读5分钟

先唠嗑

吾今日见天高气爽,百里无云,大喜。平日事儿以ShaderToy为趣之见阅,LeetCode 配之。虽常入其闻之,但未尝知其所以也。友曰,君何其惑所极其精力也?趣之非普众所受,遂答之。

ShaderToy - 着色器玩具

作为一个时常在ShaderToy潜水的“巨佬”,在看到社区上各位巨巨佬的作品时,总不禁感叹,图形的世界真奇妙。只有你想不到的,没有他们搞不定的。特别是上面的各种效果,你能想象,不用模型,只用纯数学的方式,就能实现惊艳的效果。对于痴迷其中的我,完全是不能自拔。为了带更多的人入坑,我决定,分享一下我经常玩的shader撸图函数,还有后处理效果。让更多的小伙伴为图形学的社区活力增添一丝丝浪花。

准备工作

我希望你们充分并了解webgl 的基本知识,还有glsl 的语法基础,还有 高中的发霉的数学,好好拿洗洁精擦擦。盘顺了再来玩这个盘。可在WebGL基本基础翻阅。

巨佬们的展示

光线追踪

image.png

赛博朋克风

image.png

图形噪声

image.png

正式开始

有向距离场 SDF 的概念

对于一些基本的图形造型,例如圆形,矩形,间隔面,抑或是相对复杂的几何面的组合图形。其实本质上都是SDF 即距离场的实现,在这里,我们主要讨论的是平面2D的距离场。所以我们的造型函数一半有且只有连个维度分量的计算,至于涉及到更高维度的变换,既有可能是高维在拍平到二维的高维过渡变换。

基本图形

矩形推导思路

假如一个篮球场 100米 * 100米,里面有一个30 * 30 的正方形区域,起始点为(10,10),我们可以简单理解为下面这个图例。

image.png

假设你是区域内其中任意一个像素点,你应该如何判断自己是否在这个白色区域中间呢? 对于 聪明的我们来说,我们假设那任意一点坐标为 P=(x,y)P = (x,y),整个区域为MM,如果定义白色的区域就有下面的理解

对于 任意的P(x,y)M \forall P(x,y) \in M10<=x<=4010<=y<=40 10 <= x <= 40 \cap 10 <= y <= 40

这样好像就可以了,我们把10 转换下单位,有了这样的代码跟效果。

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    // Normalized pixel coordinates (from 0 to 1)
    vec2 uv =  fragCoord/iResolution.xy;
    uv.x /= iResolution.y/iResolution.x;
    uv.y = 1. -uv.y;
    
    
    if(uv.x >= 0.1 && uv.y >= 0.1 && uv.x <= 0.4 && uv.y <= 0.4){
        fragColor = vec4(1.,1.,1.,1.);
    }else{
         fragColor = vec4(0.,0.,0.,1.0);
    }

    // Output to screen
   
}

image.png 但是从GPU的角度来说,这段代码还可以继续优化。由于GPU并不擅长逻辑判断运算。我们看看能不能不用判断就可以实现这种效果呢? 答案当然是可以的,我们这边介绍glsl 第一个造型函数 step

step() 函数

step()函数的定义,如果x < edge,返回0.0,否则返回1.0

genType step (genType edge, genType x),genType step (float edge, genType x)

我们使用第一个重载的函数 定义左边的边界

float left = step(0.1,uv.x);

定义右边的边界

float right = step(uv.x,0.4);

定义上边的边界

float top = step(0.1,uv.y);

定义底边的边界

float bottom = step(uv.y,0.4);

然后组合起来

float pixelValue = top + right + left + bottom;
```glsl
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    // Normalized pixel coordinates (from 0 to 1)
    vec2 uv =  fragCoord/iResolution.xy;
    uv.x /= iResolution.y/iResolution.x;
    uv.y = 1. -uv.y;
    
    float left = step(0.1,uv.x);
    float right = step(uv.x,0.4);
    float top = step(0.1,uv.y);
    float bottom = step(uv.y,0.4);
    float pixelValue = top * right * left * bottom;
    fragColor = vec4(vec3(pixelValue),1.0);

    // Output to screen
   
}

image.png 当然了,这样也是可以的。

圆形推导思路

同样道理,高中我们就已经知道了圆的解析式 (x2+y2)=r2(x^2 + y^2) = r^2 ,所以我们可以直接写出这样的代码

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    // Normalized pixel coordinates (from 0 to 1)
    vec2 uv =  fragCoord/iResolution.xy;
    uv.x /= iResolution.y/iResolution.x;
    uv.y = 1. -uv.y;
    
      // 圆心(0.4,0.4)
      uv -= vec2(.40);
      if(uv.x * uv.x + uv.y * uv.y <= .05){
      
          fragColor = vec4(vec3(1.),1.0);
      }else{
          fragColor = vec4(vec3(0.),1.0);
      }
    
    // Output to screen
   
}

image.png 或许你已经看到了if ,我们通过距离场的思想来理解圆,你会发现凡是任意一点离圆心的距离小于等于半径的话,该点就在圆里面。

distance() 函数

float distance (genType p0, genType p1)

计算向量p0,p1之间的距离

glsl 内置了 distance() 函数来帮助我们解决这类问题 所以就有了下面的代码

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    // Normalized pixel coordinates (from 0 to 1)
    vec2 uv =  fragCoord/iResolution.xy;
    uv.x /= iResolution.y/iResolution.x;
    uv.y = 1. -uv.y;
    
      
 
     
     // 圆心(0.3,0.3) r = 0.2 
     float result = step(distance(uv,vec2(.3,.3)),.2);
          fragColor = vec4(vec3(result ),1.0);

    
    // Output to screen
   
}

组合封装

我们把上面矩形跟圆形的代码封装成一个函数的话,我们就可以组合一些图形显示了

float Circle(vec2 uv,vec2 center,float r){
  float result = step(distance(uv,center),r);
  return result;     
}
float Rect(vec2 uv,vec2 startPoint, float w,float h){
    float left = step(startPoint.x,uv.x);
    float right = step(uv.x,startPoint.x + w);
    float top = step(startPoint.y ,uv.y);
    float bottom = step(uv.y,startPoint.y + h);
    float pixelValue = top * right * left * bottom;
    return pixelValue;
}

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    // Normalized pixel coordinates (from 0 to 1)
    vec2 uv =  fragCoord/iResolution.xy;
    uv.x /= iResolution.y/iResolution.x;
    uv.y = 1. -uv.y;
    
    float circleA = Circle(uv,vec2(.2,.2),.1);
    float circleB = Circle(uv,vec2(.7,.5),.2);
    float circleC = Circle(uv,vec2(.2,.5),.05);
    
    float rectA = Rect(uv,vec2(.2,.2),.2,0.2);
    float rectB = Rect(uv,vec2(.31,.5),.1,0.2);
    float rectC = Rect(uv,vec2(.2,.5),.1,0.2);
    
    float result = circleA + circleB + circleC;
    
    result += rectA + rectB + rectC;
    
    fragColor = vec4(vec3(result),1.0);
    
    
    // Output to screen
   
}

image.png

颜色遮罩

我们最后直接根据uv坐标作为颜色值给他加个颜色遮罩

    fragColor = vec4(vec3(result)* vec3(uv,1.0),1.0);

image.png

再把shadertoy内置时间变量加上

 fragColor = vec4(vec3(result)*cos(iTime+uv.xyx+vec3(0,2,4)),1.0);

xy-20210731-003834.gif

完美~~

To Be Continued

今天的分享就到这里,下一期我们介绍更好玩的一些造型函数跟造型方法