[WebGL 项目练习 | 青训营笔记16]

140 阅读13分钟

前言

这是我参与[第五届青训营]伴学笔记创作活动的第 16 天,相信的大家对图形构建或者说canvas比较的感兴趣,接下来我们就来讲讲WebGLweb图形库

初识 WebGL

Why WebGL / Why GPU?

  • WebGL 是什么?
    • GPU !== WebGL !== 3D
  • WebGL 为什么不像其他前端技术那么简单

WebGL是运用了 GPU3D 的技术

Modern Graphics System

微信截图_20230214221034.png

  • 光栅 (Raster): 几乎所有的现代图形系统都是基于光来绘制图形的,光栅就是指构成图像的像素阵列。
  • 像素 (Pixel) :一个像素对应图像上的一个点,它通常保存图像上的某个具体位置的颜色等信息。
  • 帧缓存(Frame Buffer) : 在绘图过程中,像素信息被存放于缓存中,帧缓存是一块内存地址。
  • CPU (Central Processing Unit) : 中央处理单元,负责逻辑计算。
  • GPU (`Graphics Processing Unit) : 图形处理单元,负责图形计算

微信截图_20230214221008.png

  1. 轮廓提取 / meshing
  2. 光栅化
  3. 帧缓存
  4. 渲染

The Pipeline

Data ==> Processor ==> Frame buffer ==> Pixels

通常情况下是将数据通过管道传输的形式传输数据,生成完数据,然后放到缓存区,然后批量的绘制出来,类似于活字印刷

CPU VS GPU

微信截图_20230214221245.png 微信截图_20230214221251.png 微信截图_20230214221339.png

  1. GPU由大量的小运算单元构成
  2. 每个运算单元只负责处理很简单的计算
  3. 每个运算单元彼此独立
  4. 因此所有计算可以并行处理

对图形的计算不需要非常复杂的逻辑计算,比如像素点的信息,不需要很强大的运算单元和运算能力,但是我这给像素点是非常多的,即使是一张图片都是有48万个像素点,不可能开个48万个核去计算,那样非常的浪费,所以还是交给处理这方面比较强的GPU去处理

WebGL & OpenGL

web.eecs.umich.edu/~sugih/cour… 微信截图_20230214221348.png

  1. 理论上来去说WebGL 是利用GPU技术完成绘图的API,它实际上是OpenGL家族 的一个成员,这个成员有Javascript的接口,把这些APIjavascript的runtime 里面去实现了。
  2. OpenGL的Javascript的接口底层和C++OpenGL的借口底层是差不多的,因为他们是一个系列的

WebGL Startup

  1. 创建 WebGL 上下文
  2. 创建 WebGL Program
  3. 将数据存入缓冲区
  4. 将缓冲区数据读取到GPU
  5. GPU 执行 WebGL 程序,输出结果 微信截图_20230214221355.png
  1. Javascript创建WebGL上下文是通过canvas对象,我们在canvas对象上获得WebGL的上下文;
  2. 接下来就创建WebGL的程序,这里面有处理图形的代码,在GPU处理的部分,他不是运用javascript语言去编写的,而是GLSL编程语言去写的,把javascript代码给编译好,给构建出来;
  3. 最后变成WebGL的程序;然后我们去应用这个WebGL程序,然后我们创建数据,存入缓存区,最终我们将缓存区的数据读到GPU,在GPU里面去执行WebGL的程序,最后输出结果

Create WebGL Context

const canvas = document.querSelector('canvas')
const gl = canvas.getContext('webgl')
function create3DContext(canvas,options) {
    const names = ['webgl','experimental-webgl','webkit-3d','maz-webgl'];
    if (options.webgl2) names.unshift('webgl2');
    let context = null;
    for (let ii = 0; ii < names.length; ++ii ) {
        try() {
            context = canvas.getContext(names[ii],options);
        } catch (context) {
            break;
        }
    }
    return context
}

The Shaders

  1. Vertex Shader
attribute vec2 position;

void main() {
    gl_PointSize = 1.0;
    gl_Position = vec4(position,1.0,1.0);
}
  1. Fragment Shader
precision mediump float;

void main() {
    gl_FragColor = vec4(1.0,0.0,0.0,1.0);
}

  • 着色器有两种:
  1. 顶点着色器,用来定义图形顶点的位置,从而可以绘制出不同的形状
  2. 片段着色器/片源着色器,顶点和图源定义好的区域类,用简单的概念来去理解的话,就是顶点图源确定了那个区域内的像素点;
  • 比如说三角形,三个顶点的绘制确定就是交予顶点着色器去绘制,内容就交给片段着色器

Create Program

const vertexShader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vertexShader, vertex); 
gl.compileShader(vertexShader);

const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER); 
gl.shaderSource(fragmentShader, fragment); 
gl.compileShader(fragmentShader);

const program = gl.createProgram(); 
gl.attachShader(program, vertexShader); 
gl.attachShader(program, fragmentShader); 
gl.linkProgram(program);

gl.useProgram(program);

Data to Frame Buffer

Axes 微信截图_20230214225401.png Typed Array

const points = new Float32Array([
    -1, -1,
    0, 1,
    1, -1,
])
const bufferId = gl.createBuffer()
gl.bindBuffer(gl.ARRAY_BUFFER,bufferId);
gl.bindBuffer(gl.ARRAY_BUFFER,points,gl.STATIC_DRAW)

canvas 是左手坐标系,而webGL 的话是右手坐标系,就是浏览器和canvas 左上角的顶点坐标是(0,0),坐标x是向上的,坐标y是;webGL 中心点是坐标圆点,左上角是(-1,1),右下角是(1,-1)

Frame Buffer to GPU

const vPosition = gl.getAttribLocation(program, 'position')//获取顶点着色器中的position变量的地址
gl.vertexAttribPointer(vPosition, 2, gl.FLOAT, false, 0, 0);//给变量设置长度和类型
gl.enableVertexAttribArray(vPosition); //激活这个变量
attribute vec2 position;

void main() {
    gl_PointSize = 1.0;
    gl_Position = vec4(position, 1.0, 1.0);
}

Output

Output

l.clear(gl.COLOR_BUFFER_BTT);
gl.drawArray(gl.TRIANGLES, 0 ,points.length / 2)

WebGL

为什么 WebGL 那么难呢?

2D vs WebGL

微信截图_20230214230358.png 2D

const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('2d');

ctx.beginPath();
ctx.moveTo(250, 0);
ctx.lineTo(500, 500);
ctx.lineTo(0, 500);
ctx.fillStyle = 'red';
ctx.fill();

Mesh.js

import {Renderer, Figure2D, Mesh2D} from '@mesh.js/core';

const canvas = document.querySelector('canvas');
const renderer = new Renderer(canvas);

const figure = new Figure2D();
figure.beginPath();
figure.moveTo(250, 0);
figure.lineTo(500, 500);
figure.lineTo(0, 500);

const mesh = new Mesh2D(figure, canvas);
mesh.setFill({
  color: [1, 0, 0, 1],
});

renderer.drawMeshes([mesh]);

mesh.js 是月影大佬封装的WebGL的底层,可以像2Dcanvas命令式去绘图;注意:在这个mesh.js或者说是WebGL底层是得分两个步骤1. 图形轮廓的处理,2. 把轮廓给网格化之后,然后才去完成渲染,这样就写出来的代码就和2Dcanvas代码就有点相似了

Polygons

Polvgons

微信截图_20230214231438.png

Triangulations 微信截图_20230214231443.png

多边形就是有一个简单的三角形去组装成一个复杂的多边形,刚刚我们已经搞定了一个有颜色的三角形,去掉颜色也只是一个API的事情;我们需要对一个复杂的多边形进行三角剖分

Draw Polygon with 2D Triangulations

使用 Earcut 进行三角剖分

微信截图_20230214231530.png

const vertices = [
    [-0.7,0.51,
    [-0.4,0.3],
    [-0.25, 0.711,
    [-0.1,0.56],
    [-0.1, 0.13],
    [0.4, 0.21],
    [0,-0.6],
    [-0.3,-0.3]
    [-0.6,-0.3],
    [-0.45,0.0],
]

const points = verctices.flat();
const triangles = earcut(points)

earcut 的三角剖分API,这个API返回的是顶点的下标数组

3D Meshing

微信截图_20230214231849.png 3D Meshing

微信截图_20230214232049.png

const {Scene} = spritejs;

const {Mesh3d, shaders} = spritejs.ext3d;
const container = document.getElementById('container');
const scene = new Scene({
    container,
    displayRatio: 2,
});
const layer = scene.layer3d('fglayer', {
    camera: {
    fov: 35,
    },
    directionalLight: [0.5, 1.0, -0.3],
    directionalLightColor: [1, 1, 1, 0.15],
});

layer.camera.attributes.pos = [5, 3, 6];
layer.camera.lookAt([0, 0, 0]);

(async function () {
    const texture = layer.createTexture('https://p2.ssl.qhimg.com/t01598a49e49aba1046.jpg');
    const program = layer.createProgram({
      ...shaders.NORMAL_TEXTURE,
      uniforms: {
        tMap: {value: texture},
    },
});

 const model = await layer.loadModel('https://s3.ssl.qhres2.com/static/8613b585d1542274.json');

 // For an accurate wireframe, triangle vertices need to be duplicated to make line pairs.
 // Here we do so by generating indices. If your geometry is already indexed, this needs to be adjusted.
const index = new Uint16Array((model.position.length / 3 / 3) * 6);
 for(let i = 0; i < model.position.length / 3; i += 3) {
 // For every triangle, make three line pairs (start, end)
 index.set([i, i + 1, i + 1, i + 2, i + 2, i], i * 2);
}
model.index = index;

// console.log(geometry);

const wireframeMesh = new Mesh3d(program, {
    mode: 'LINES',
});

 wireframeMesh.setGeometry(model);
 layer.append(wireframeMesh);
 wireframeMesh.animate([
    {rotateY: 0},
    {rotateY: 360},
], {
    duration: 5000,
    iterations: Infinity,
  });
}());

3D 的更加的复杂,他也是分成非常多的三角网格,我们用实时的计算是非常的慢,所以在一般的情况下,WebGL渲染3D的时候,不会去实时的分割这个三角网格,而是通过设计软件,把这个模型的三角网格去先分割好,然后把这些数据顶点传入到WebGL里面去渲染;所以一个复杂的图形是如此渲染的。

Transforms

  • 平移 微信截图_20230214232557.png

  • 旋转 微信截图_20230214232602.png

  • 缩放 微信截图_20230214232608.png

  • 旋转+缩放是线性变换 微信截图_20230214232701.png 微信截图_20230214232704.png

  • 从线性变换到齐次矩阵 微信截图_20230214232709.png

2D3Dtransforms是差不多的;其中CSS3中的transform 是23的矩阵,实际上是33 的矩阵,因为底下的(0,0,1),所以把这行给省略掉了;你可以理解为是一个是二维和三维矩阵的线性变换的transform

Apply Transforms

Apply Transforms

attribute vec2 position;
uniform mat3 modelMatrix;
void main() {
  gl_PointSize = 1.0;
  vec3 pos = modelMatrix * vec3(position, 1.0);
  gl_Position = vec4(pos, 1.0);
}
let transform = gl.getUniformLocation(program, 'modelMatrix');
gl.uniformMatrix3fv(transform, false, 
  [0.5, 0, 0, 
   0, 0.5, 0,
   0.1, -0.1, 1]); 

固定的变量是可以通过uniform 传进来,得知道顶点是通过cossintan计算,3D 就是3 * 3的矩阵线性变换,4D 就是4 * 4的矩阵线性变换

3D Matrix

3D 标准模型的四个齐次矩阵 (mat4) 挑动高

    1. 投影矩阵 Projection Matrix
    1. 模型矩阵 Model Matrix
    1. 视图矩阵 View Matrix
    1. 法向量矩阵 Normal Matrix

3D的标准模型是非常的复杂,一般的情况像threee.js或者像其他的一些渲染库,会有不同的齐次矩阵;

  1. 投影矩阵,投影矩阵是定义坐标系的,标准的坐标系是中间是坐标原点,两边是(-1,1)这样的坐标系,我们是可以缩放旋转这个坐标系的,缩放旋转坐标系就是用这个投影矩阵来做。
  2. 模型矩阵,对元素或者图形本身去进行这个,transform 的变换
  3. 视图矩阵,因为在3D模型里面有个摄像机,摄像机在某个空间位置,最终我们呈现出来的画面是摄像机在的那个位置拍摄出来的图像,所以我们可以对视图矩阵进行变换,相当于是移动这个摄像机
  4. 法向量矩阵,定义了这个3D模型每个面的法线的这个坐标,法线就是当前垂直这个面的所在的网格垂线;为什么要定义这个法线呢?是因为比如我们要计算光照,光线射到平面上的时候,它和平面上的这个夹角会影响到这个平面上的这个明暗。这时候我们就需要计算光射到平面上的夹角,这个角度和法向量的夹角;法向量矩阵就是来计算这个的。

推荐书籍文章

  1. The book of shaders
  2. Mesh.js
  3. glsl-doodle
  4. SpriteJS
  5. ThreeJS
  6. ShaderToy
  7. 稀土掘金
  1. The book of shaders】是适合图形学的入门,有中英版
  2. Mesh.js】基于WebGL渲染的图形库,它提供的是相对底层的,与这些three.js相比更底层的这个绘图的API,它绘图的模式和canvas 2D是非常的相像
  3. glsl-doodle】这个库在码上掘金平台有非常好的支持,开箱即用,它可以屏蔽掉那些attribute创建Polygons细节,不用管这些,你直接写shader着色器就可以了
  4. SpriteJS】是可视化渲染框架,库的功能还是挺强大的,它的底层就是mesh.js实现的
  5. thress.js】是一个现在比较流行3D渲染库
  6. ShaderToy】是写一些比较有意思有趣的shader的平台,ShaderToy国内访问是比较慢的,想写的话,也可以在码上掘金上去写,码上掘金除了可以写glsl-doodle,也是兼容ShaderToy的 ,80%以上的例子都可在码上掘金上实现

WebGL项目实战

  1. 平台
  • 码上掘金
  1. 运用的语法是shaderToy

码上掘金的环境搭建

  1. 点击链接,进入码上掘金
  2. 点击新建代码片段按钮
  3. 选择实验模板下的GLSL
  4. 因为码上掘金支持GLSL的语法,并且实现了一个小demo,我们把右边javascript代码风格改成Custom(已经是Custom就省略此步骤)
  5. 删除以下信息的代码,以下代码采用的是CDN引入库的模式,并设置了version版本,定义了类型
#!/jcode/lang/glsl https://xitu.github.io/jcode-languages/dist/lang-glsl.json

#version 300 es
precision highp float;

微信截图_20230215115455.png

画个正方形

颜色的定义WebGL和CSS的区别

#!/jcode/lang/glsl https://xitu.github.io/jcode-languages/dist/lang-glsl.json

#version 300 es
precision highp float;

out vec4 fragColor;

void main() {
  fragColor = vec4(1.0,0.0,0.0,1.0);
}
  • 默认shader着色器用的rgba的色彩,css 类似,只不过css 是运用的是整数,而且是0-255,来表示这颜色的阈值;webgl 用的是浮点数,它是从 0-1 来表示,1是最大的,0是最小的;绿色的话就是一个(0,1,0,1)

微信截图_20230215123917.png

画个黑白对半的正方形

#!/jcode/lang/glsl https://xitu.github.io/jcode-languages/dist/lang-glsl.json

#version 300 es
precision highp float;

out vec4 fragColor;

uniform vec2 dd_resolution;
void main() {
  vec2 st = gl_FragCoord.xy / dd_resolution;
    if (st.x > 0.5) {
        fragColor = vec4(1.0);
    } else {
        fragColor.a = 1.0;
    }
}
  • 通过ifelse去判断来实现黑白对半 微信截图_20230215121935.png

画个圆形

#!/jcode/lang/glsl https://xitu.github.io/jcode-languages/dist/lang-glsl.json

#version 300 es
precision highp float;

out vec4 fragColor;

uniform vec2 dd_resolution; // 传入画布分辨率

void main() {
  vec2 st = gl_FragCoord.xy / dd_resolution; 
  fragColor.rgb = step(0.5,st.x) * vec3(1.0); // webgl 自带的阶梯函数
  fragColor.a = 1.0;
}
  • 通过webgl 自带的阶梯函数去实现黑白对半
void main() {
  vec2 st = gl_FragCoord.xy / dd_resolution; 
  vec2 center = vec2(0.5);
  float r = 0.2;
  fragColor.rgb = step(length(st - center),r) * vec3(1.0); 
  fragColor.a = 1.0;
}
  • 这个r 半径大于两个向量stcenter之间的距离,就是黑色,反之小于就是白色
  • 但是你会看到圆的边缘有锯齿,这是因为我们处理的是像素点,所以在默认的情况下,如果是曲线的话,是有锯齿的;如果你的显示设备比较好的话,看起来是比较的明显
  • 对锯齿进行一个消除,在阶梯函数上做一个平滑,运用smotthstep()函数,在0.01 - d 之间是平滑的,因为当靠近边缘的时候会有一个渐变的效果,就会显示比较的平滑
void main() {
  vec2 st = gl_FragCoord.xy / dd_resolution; 
  vec2 center = vec2(0.5);
  float r = 0.2;
  float d = length(st - center);
  fragColor.rgb = smoothstep(d - 0.01, d, r) * vec3(1.0); 
  
  fragColor.a = 1.0;
}

微信截图_20230215122902.png 微信截图_20230215123647.png

画个圆环

#!/jcode/lang/glsl https://xitu.github.io/jcode-languages/dist/lang-glsl.json

#version 300 es
precision highp float;

out vec4 fragColor;

uniform vec2 dd_resolution; // 传入画布分辨率

void main() {
  vec2 st = gl_FragCoord.xy / dd_resolution; 
  vec2 center = vec2(0.5);
  float r = 0.2;
  float d = length(st - center);
  vec3 c1 = smoothstep(d - 0.01,d,r) * vec3(1.0);
  vec3 c2 = smoothstep(d,d + 0.01,r - 0.03) * vec3(1.0);

  fragColor.rgb = c1-c2; 
  
  fragColor.a = 1.0;
}
  • 画两个圆,一个大一点,一个小一点,大圆 - 小圆 就可实现圆环,其中里面的小圆锯齿过渡就得另外一边的距离+0.01

微信截图_20230215124922.png

封装圆环绘画函数
  • 对这个圆环进行封装,我们可以用这个封装方法去划线,
#!/jcode/lang/glsl https://xitu.github.io/jcode-languages/dist/lang-glsl.json

#version 300 es
precision highp float;

out vec4 fragColor;

uniform vec2 dd_resolution; // 传入画布分辨率

float stroke(float d, float d0, float w, float smth) {
  float th = 0.5 * w;
  smth = smth * w;
  float start = d0 - th;
  float end = d0 + th;
  return smoothstep(start, start + smth, d) - smoothstep(end - smth, end, d);
};

void main() {
  vec2 st = gl_FragCoord.xy / dd_resolution; 
  
  float d = stroke(st.x, 0.5, 0.02, 0.1); 
  fragColor.rgb = d * vec3(1.0);
  fragColor.a = 1.0;
}

微信截图_20230215130738.png

  • 通过这个封装方法去画个圆环
out vec4 fragColor;
uniform vec2 dd_resolution; // 传入画布分辨率

float stroke(float d, float d0, float w, float smth) {
  float th = 0.5 * w;
  smth = smth * w;
  float start = d0 - th;
  float end = d0 + th;
  return smoothstep(start, start + smth, d) - smoothstep(end - smth, end, d);
};

void main() {
  vec2 st = gl_FragCoord.xy / dd_resolution; 
  vec2 center = vec2(0.5);
  float r = 0.2;
  float d = length(st - center);

  float d2 = stroke(d, r, 0.02, 0.1); 
  fragColor.rgb = d2 * vec3(1.0);
  fragColor.a = 1.0;
}

微信截图_20230215131355.png

通过计算点到某个位置的距离来构图的思路,被称之为距离场构图法

画一条斜线和曲线和正弦曲线
out vec4 fragColor;
uniform vec2 dd_resolution; // 传入画布分辨率

float stroke(float d, float d0, float w, float smth) {
  float th = 0.5 * w;
  smth = smth * w;
  float start = d0 - th;
  float end = d0 + th;
  return smoothstep(start, start + smth, d) - smoothstep(end - smth, end, d);
}

void main() {
  vec2 st = gl_FragCoord.xy / dd_resolution; 
  vec2 center = vec2(0.5);
  float r = 0.2;
  float d = length(st - center);
  // vec3 c1 = smoothstep(d - 0.01,d,r) * vec3(1.0);
  // vec3 c2 = smoothstep(d,d + 0.01,r - 0.03) * vec3(1.0);

  float d = stroke(d, r, 0.02, 0.1); 
  fragColor.rgb = d * vec3(1.0);
  fragColor.a = 1.0;
}
  • 绘制斜线和曲线和正弦曲线
void main() {
  vec2 st = gl_FragCoord.xy / dd_resolution; 
  float d1 = stroke(st.y , st.x, 0.02, 0.1 );
  float d2 = stroke(st.y, 4.0 * (st.x - 0.5) * (st.x - 0.5), 0.02, 0.1);
  float d3 = stroke(st.y * 2.0, 1.0 - sin(30.0 * st.x),0.02,0.1);

  fragColor.rgb = d1 * vec3(1.0,1.0,0.0) 
  + d2 * vec3(0.0, 1.0, 1.0)
  + d3 * vec3(0.5, 1.0, 0.5);
  fragColor.a = 1.0;
}

微信截图_20230215135508.png

绘制任意的直线

float sdf_line(vec2 a, vec2 b, vec2 st) {
  vec2 ap = st - a;
  vec2 ab = b - a;
  return ((ap.x * ab.y) - (ab.x * ap.y)) / length(ab);
}

那如何绘制线段呢?

  • 线段和直线的区别: 直线是两个端点是无限长的,而线段是有限长度的,所以我们需要设置线段的两端的坐标
绘制一条线段

微信截图_20230215140247.png

P的线段落到A上,d 的距离就是PA
P的线段落到B上的话,就是PB

uniform vec2 dd_resolution;

out vec4 fragColor;

float stroke(float d, float d0, float w, float smth) {
  float th = 0.5 * w;
  smth = smth * w;
  float start = d0 - th;
  float end = d0 + th;
  return smoothstep(start, start + smth, d) - smoothstep(end - smth, end, d);
}

float sdf_line(vec2 a, vec2 b, vec2 st) {
  vec2 ap = st - a;
  vec2 ab = b - a;
  return ((ap.x * ab.y) - (ab.x * ap.y)) / length(ab);
}

float sdf_seg(vec2 a, vec2 b, vec2 st) {
  vec2 ap = st - a;
  vec2 ab = b - a;
  vec2 bp = st - b;
  float l = length(ab);
  float proj = dot(ap, ab) / l;
  if(proj >= 0.0 && proj <= l) {
    return sdf_line(a, b, st);
  }
  return min(length(ap), length(bp));
}

float sdf_plot(vec2 a, vec2 b, vec2 c, vec2 st) {
  float d1 = sdf_seg(a, b, st);
  float d2 = sdf_seg(b, c, st);

  return min(d1, d2);
}

void main() {
  vec2 st = gl_FragCoord.xy / dd_resolution; 
  float d1 = sdf_seg(vec2(0.3), vec2(0.7, 0.5), st);
  float d2 = sdf_line(vec2(1.0, 0.0), vec2(0.0, 1.0), st);
  
  fragColor.rgb = stroke(d1, 0.0, 0.03, 0.2) * vec3(1.0, 1.0, 0.0)
  + stroke(d2, 0.0, 0.03, 0.2) * vec3(0.0, 1.0, 1.0);

  fragColor.a = 1.0;
}

那我们如何计算P 落到AB的投影呢?

    1. 叉乘是平行四边形的面积,在物理学上来说就是力臂和力臂产生的力矩。
    1. 如果是点乘的话就是这两个向量,做的功,物理的话,就是力沿着这个位移的方向做的功,所以它等于的是ap * ab * cosδ(Delta),余弦值就是ap在ab 上的投影那个向量 * ab除以这个,就得到了ap投影的长度
    1. 这投影长度在0 和 l 之间

微信截图_20230215142810.png

绘制任意曲线

如何绘制椭圆的线段?

  • GSL的宏定义
#ifndef PLOT
#define PLOT(f, st, step) sdf_plot(vec2(st.x - step, f(st.x - step)), vec2(st.x, f(st.x)), vec2(st.x + step, f(st.x + step)), st)
#endif
  • 完整代码
#!/jcode/lang/glsl https://xitu.github.io/jcode-languages/dist/lang-glsl.json

#version 300 es
precision highp float;

uniform vec2 dd_resolution;

out vec4 fragColor;

float stroke(float d, float d0, float w, float smth) {
  float th = 0.5 * w;
  smth = smth * w;
  float start = d0 - th;
  float end = d0 + th;
  return smoothstep(start, start + smth, d) - smoothstep(end - smth, end, d);
}

float sdf_line(vec2 a, vec2 b, vec2 st) {
  vec2 ap = st - a;
  vec2 ab = b - a;
  return ((ap.x * ab.y) - (ab.x * ap.y)) / length(ab);
}

float sdf_seg(vec2 a, vec2 b, vec2 st) {
  vec2 ap = st - a;
  vec2 ab = b - a;
  vec2 bp = st - b;
  float l = length(ab);
  float proj = dot(ap, ab) / l;
  if(proj >= 0.0 && proj <= l) {
    return sdf_line(a, b, st);
  }
  return min(length(ap), length(bp));
}

float sdf_plot(vec2 a, vec2 b, vec2 c, vec2 st) {
  float d1 = sdf_seg(a, b, st);
  float d2 = sdf_seg(b, c, st);

  return min(d1, d2);
}

#ifndef PLOT
#define PLOT(f, st, step) sdf_plot(vec2(st.x - step, f(st.x - step)), vec2(st.x, f(st.x)), vec2(st.x + step, f(st.x + step)), st)
#endif

float fx(in float x) {
  return 0.0;
}

float fy(in float x) {
  return 9999999.99 * x;
}

float f1(in float x) {
  return floor(x);
}

float f2(in float x) {
  return sin(2.0 * x) / x;
}

void main() {
  vec2 st = gl_FragCoord.xy / dd_resolution;
  st = mix(vec2(-10, -10), vec2(10, 10), st);

  float stp = 0.1;
  
  float px = PLOT(fx, st, stp);
  float py = PLOT(fy, st, stp);

  float c1 = stroke(px, 0.0, 0.2, 0.2);
  float c2 = stroke(py, 0.0, 0.2, 0.2);

  float p1 = PLOT(f1, st, stp);
  float c3 = stroke(p1, 0.0, 0.4, 0.2);

  float p2 = PLOT(f2, st, stp);
  float c4 = stroke(p2, 0.0, 0.4, 0.2);

  // float d1 = sdf_seg(vec2(0.3), vec2(0.7, 0.5), st);
  // float d2 = sdf_line(vec2(1.0, 0.0), vec2(0.0, 1.0), st);

  fragColor.rgb = c1 * vec3(1.0, 1.0, 1.0) +
    c2 * vec3(1.0, 1.0, 1.0) +
    c3 * vec3(1.0, 1.0, 0.0) +
    c4 * vec3(0.0, 1.0, 1.0);

  fragColor.a = 1.0;
}

如果是要封装椭圆的线段方法,会非常的复杂,我需要对曲线方程比较了解;在GSL里面支持宏定义,我们可以定义一个宏,来对这个点进行一个采样,有了这个宏就可以定义任意的曲线了

微信截图_20230215145418.png

绘制三角形

微信截图_20230215162346.png

/**
   三角形的 SDF
*/
float sdf_triangle(vec2 st, vec2 a, vec2 b, vec2 c) {
vec2 va = a - b;
vec2 va = b - c;
vec2 va = c - a;

float d1 = sdf_line(a, b, st);
float d2 = sdf_line(b, c, st);
float d3 = sdf_line(c, a, st);

// 三角形内切圆半径
float l = abs(va.x * vb.y  -  va.y * vb.x) / (length(va) + length(vb) + length(vc));

// 点在三角形内部,定义距离为正
if(d1 >= 0.0 && d2 >= 0.0 && d3 >= 0.0 || d1 <= 0.0 && d2<= 0.0 && d3 <= 0.0) {
    return min(abs(d1), min(abs(d2), min(abs(d3))) / l;
}

// 点在三角形内部,定义距离为正
d1 = sdf_seg(a, b, st);
d2 = sdf_seg(b, c, st);
d3 = sdf_seg(c, a, st);
    return -min(abs(d1), min(abs(d2),abs(d3))) / l;
}

void main() {
    vec2 st = gl_FragCoord.xy / dd_resolution;
    // st = mix(vec(-10, -10), vec2(10, 10), st);
    
    float stp = 0.1;
    
    float px = PLOT(fx, st, stp);
    float py = PLOT(fy, st, stp);
    
    float c1 = stroke(px, 0.0, 0.2, 0.2);
    float c2 = stroke(py, 0.0, 0.2, 0.2);
    
    float p1 = PLOT(f1, st, stp);
    float c3 = stroke(p1, 0.0, 0.4, 0.2);
    
    float p2 = PLOT(f2, st, stp);
    float c4 = stroke(p2, 0.0, 0.4, 0.2);
    
    float d = sdf_triangle(st, vec2(0.25), vec2(0.75), vec2(0.25, 0.75));
    float c5 = stoke(d, 0.0, 0.2, 0.3);
    
  // float d1 = sdf_seg(vec2(0.3), vec2(0.7, 0.5), st);
  // float d2 = sdf_line(vec2(1.0, 0.0), vec2(0.0, 1.0), st);

  fragColor.rgb = c5 * vec3(0.5, 0.5, 1.0);

  fragColor.a = 1.0;
}

微信截图_20230215165241.png

思想都是运用距离场构图法来去绘制的

void main() {
    vec2 st = gl_FragCoord.xy / dd_resolution;
    // st = mix(vec(-10, -10), vec2(10, 10), st);
    
    float stp = 0.1;
    
    float px = PLOT(fx, st, stp);
    float py = PLOT(fy, st, stp);
    
    float c1 = stroke(px, 0.0, 0.2, 0.2);
    float c2 = stroke(py, 0.0, 0.2, 0.2);
    
    float p1 = PLOT(f1, st, stp);
    float c3 = stroke(p1, 0.0, 0.4, 0.2);
    
    float p2 = PLOT(f2, st, stp);
    float c4 = stroke(p2, 0.0, 0.4, 0.2);
    
    float d = sdf_triangle(st, vec2(0.25), vec2(0.75), vec2(0.25, 0.75));
    float c5 = stoke(d, 0.0, 0.2, 0.3);
    
  // float d1 = sdf_seg(vec2(0.3), vec2(0.7, 0.5), st);
  // float d2 = sdf_line(vec2(1.0, 0.0), vec2(0.0, 1.0), st);

  fragColor.rgb = d * vec3(0.5, 0.5, 1.0);

  fragColor.a = 1.0;
}

微信截图_20230215165717.png

绘制复杂图形

地砖代码链接

微信截图_20230215170225.png 中国古代钱币 钱币代码链接 微信截图_20230215170046.png

花代码链接 微信截图_20230215170654.png 直角坐标系转换成极坐标,还能通过随机和噪声,可以绘制比较多好玩的

Read more

  1. The book of shaders
  2. Mesh.js
  3. glsl-doodle
  4. SpriteJS
  5. ThreeJS
  6. ShaderToy
  7. 稀土掘金