WebGL打开 3D 世界的大门(三):着色器和GLSL语言

这一篇文章将会比较硬核,主要讲解着色器和GLSL语言,内容比较枯燥,我将尽量用通俗易懂的话讲解。在上一节我们提到webgl每次绘制都需要两个着色器,顶点着色器和片元着色器,一个顶点着色器和一个片元着色器放到一个着色器程序中,WebGL应用可以有多个着色器程序。

顶点着色器

通常是这样的,将点转换到裁剪空间(-1到1)。

csharp 复制代码
   void main() {
       gl_Position = transformPointsToClipspaceCoordinates
   }
   

顶点着色器可以通过以下方式获取数据:

  1. Attributes 属性 (从缓冲中获取的数据)

    这个代码例子就是,看过前两节的同学应该比较熟悉了。

    ini 复制代码
    var buf = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, buf);
    gl.bufferData(gl.ARRAY_BUFFER, someData, gl.STATIC_DRAW);
    var positionLoc = gl.getAttribLocation(someShaderProgram, "a_position");
    // 开启从缓冲中获取数据
    gl.enableVertexAttribArray(positionLoc);   
    var numComponents = 3; // (x, y, z)
    var type = gl.FLOAT; // 32位浮点数据
    var normalize = false; // 不标准化
    var offset = 0; // 从缓冲起始位置开始获取
    var stride = 0; // 到下一个数据跳多少位内存
    // 0 = 使用当前的单位个数和单位长度 ( 3 * Float32Array.BYTES_PER_ELEMENT )    
    gl.vertexAttribPointer(positionLoc, numComponents, type, false, stride, offset);

    这里不再重复讲解。这也是一种比较主要的方式。

  2. Uniforms 全局变量(在一次绘制中对所有顶点保持一致值)

    ini 复制代码
    attribute vec4 a_position;
    uniform vec4 u_offset; 
    void main() {
        gl_Position = a_position + u_offset;
    }

    让所有的点移动一个固定值,我们可以这样使用它:

    ini 复制代码
    var offsetLoc = gl.getUniformLocation(someProgram, "u_offset");
    gl.uniform4fv(offsetLoc, [1, 0, 0, 0]); // 向右偏移一半屏幕宽度

    要注意的是全局变量属于单个着色程序,如果多个着色程序有同名全局变量,需要找到每个全局变量并设置自己的值。全局变量有很多类型,对应的类型有对应的设置方法。

    scss 复制代码
     -   gl.uniform1f (floatUniformLoc, v); // float
     -   gl.uniform1fv(floatUniformLoc, [v]); // float 或 float array
     -   gl.uniform2f (vec2UniformLoc, v0, v1); // vec2
     -   gl.uniform2fv(vec2UniformLoc, [v0, v1]); // vec2 或 vec2 array
     -   gl.uniform3f (vec3UniformLoc, v0, v1, v2); // vec3
     -   gl.uniform3fv(vec3UniformLoc, [v0, v1, v2]); // vec3 或 vec3 array
     -   gl.uniform4f (vec4UniformLoc, v0, v1, v2, v4); // vec4
     -   gl.uniform4fv(vec4UniformLoc, [v0, v1, v2, v4]); // vec4 或 vec4 array
     -    
     -   gl.uniformMatrix2fv(mat2UniformLoc, false, [ 4x element array ]) // mat2 或 mat2 array
     -   gl.uniformMatrix3fv(mat3UniformLoc, false, [ 9x element array ]) // mat3 或 mat3 array
     -   gl.uniformMatrix4fv(mat4UniformLoc, false, [ 16x element array ]) // mat4 或 mat4 array
     -    
     -   gl.uniform1i (intUniformLoc, v); // int
     -   gl.uniform1iv(intUniformLoc, [v]); // int 或 int array
     -   gl.uniform2i (ivec2UniformLoc, v0, v1); // ivec2
     -   gl.uniform2iv(ivec2UniformLoc, [v0, v1]); // ivec2 或 ivec2 array
     -   gl.uniform3i (ivec3UniformLoc, v0, v1, v2); // ivec3
     -   gl.uniform3iv(ivec3UniformLoc, [v0, v1, v2]); // ivec3 or ivec3 array
     -   gl.uniform4i (ivec4UniformLoc, v0, v1, v2, v4); // ivec4
     -   gl.uniform4iv(ivec4UniformLoc, [v0, v1, v2, v4]); // ivec4 或 ivec4 array
     -    
     -   gl.uniform1i (sampler2DUniformLoc, v); // sampler2D (textures)
     -   gl.uniform1iv(sampler2DUniformLoc, [v]); // sampler2D 或 sampler2D array
     -    
     -   gl.uniform1i (samplerCubeUniformLoc, v); // samplerCube (textures)
     -   gl.uniform1iv(samplerCubeUniformLoc, [v]); // samplerCube 或 samplerCube array

    全局变量还是比较灵活的的可以是数组也可以是结构体。还可以对数组里面的值单独赋值。例如下面的结构体

    ini 复制代码
       struct SomeStruct {
        bool active;
       vec2 someVec2;
        };
        uniform SomeStruct u_someThing;
        
        var someThingActiveLoc = gl.getUniformLocation(someProgram, "u_someThing.active");

片元着色器

片元着色器的主要任务栅格化像素的颜色值,片元着色器一般通过以下方式获取数据:

1,Uniforms 全局变量 ,这个和顶点着色器一样,不再重复

2,Varings可变量 ,在我们第二节例子里面有,可以自行查看,就是颜色值取决于顶点,其他颜色进行插值计算。

3,Textures 纹理 一般来说纹理是图片信息,下面给出一个用纹理加载图片的例子:

代码地址: gitee.com/feng-lianxi... 其实代码里面已经写的比较清楚了,现在讲解一下:片元着色器

csharp 复制代码
  // fragment shaders don't have a default precision so we need
  // highp:高精度,适合需要高精度的计算。mediump:中等精度,适合一般的计算。lowp:低精度,适合颜色计算。
  precision mediump float;
  varying vec2 v_texCoord;
  uniform sampler2D u_image;
  void main() {
    // gl_FragColor is a special variable a fragment shader
    // is responsible for setting
    gl_FragColor = texture2D(u_image, v_texCoord);; // return redish-purple
  }

texture2D,是片元着色器的内置方法,用来加载图片,这里面有两个参数:有两个参数image和顶点坐标。也就是我们需要把图片加载进来,然后确定图片需要放到什么位置。

加载图片代码:

markdown 复制代码
 //****************************加载图片***********************
 var image = new Image();
image.src = "./css/imgs/flower.png";  // 必须在同一域名下
image.onload = function () {
    render(image);
}

//****************************将图片放到着色器里面***********************
var texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);

// Set the parameters so we can render any size image.
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);

// Upload the image into the texture.
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);

然后设置图片存放位置。

ini 复制代码
var texcoordLocation = gl.getAttribLocation(program, "a_texCoord");
var texcoordBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, texcoordBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
    1.0, 1.0,
    1.0, 0.0,
    0.0, 1.0,
    0.0, 0.0,
]), gl.STATIC_DRAW);

// bind the texcoord buffer.
gl.bindBuffer(gl.ARRAY_BUFFER, texcoordBuffer);
gl.enableVertexAttribArray(texcoordLocation);
var size = 2;          // 2 components per iteration
var type = gl.FLOAT;   // the data is 32bit floats
var normalize = false; // don't normalize the data
var stride = 0;        // 0 = move forward size * sizeof(type) each iteration to get the next position
var offset = 0;        // start at the beginning of the buffer
gl.vertexAttribPointer(
    texcoordLocation, size, type, normalize, stride, offset);
   

这样就图片就加载成功了。

扩展一下我们可以直接对图片进行处理,例如锐化,描边,还有各种其他计算。这个回来有空再讲解。

内置函数和方法

下面是一些常见的内置函数:

1. 角度和三角函数
  • radians(degrees) - 角度转弧度
  • degrees(radians) - 弧度转角度
  • sin(angle) - 正弦
  • cos(angle) - 余弦
  • tan(angle) - 正切
  • asin(x) - 反正弦
  • acos(x) - 反余弦
  • atan(y, x) - 反正切
  • sinh(x) - 双曲正弦
  • cosh(x) - 双曲余弦
  • tanh(x) - 双曲正切
  • asinh(x) - 反双曲正弦
  • acosh(x) - 反双曲余弦
  • atanh(x) - 反双曲正切
2. 指数函数
  • pow(x, y) - x的y次方
  • exp(x) - e的x次方
  • log(x) - 自然对数
  • exp2(x) - 2的x次方
  • log2(x) - 以2为底的对数
  • sqrt(x) - 平方根
  • inversesqrt(x) - 平方根的倒数
3. 常用数学函数
  • abs(x) - 绝对值
  • sign(x) - 符号函数
  • floor(x) - 向下取整
  • ceil(x) - 向上取整
  • fract(x) - 小数部分
  • mod(x, y) - 取模
  • min(x, y) - 最小值
  • max(x, y) - 最大值
  • clamp(x, minVal, maxVal) - 限制在范围内
  • mix(x, y, a) - 线性插值 (x*(1-a) + y*a)
  • step(edge, x) - 阶跃函数
  • smoothstep(edge0, edge1, x) - 平滑阶跃

4. 几何函数

  • length(x) - 向量长度
  • distance(p0, p1) - 两点距离
  • dot(x, y) - 点积
  • cross(x, y) - 叉积 (仅vec3)
  • normalize(x) - 标准化向量
  • faceforward(N, I, Nref) - 翻转法线
  • reflect(I, N) - 反射向量
  • refract(I, N, eta) - 折射向量
5. 矩阵函数
  • matrixCompMult(x, y) - 矩阵分量乘法
  • outerProduct(c, r) - 外积
  • transpose(m) - 转置矩阵
  • determinant(m) - 行列式
  • inverse(m) - 逆矩阵
6. 向量关系函数
  • lessThan(x, y) - 分量比较 <
  • lessThanEqual(x, y) - 分量比较 <=
  • greaterThan(x, y) - 分量比较 >
  • greaterThanEqual(x, y) - 分量比较 >=
  • equal(x, y) - 分量比较 ==
  • notEqual(x, y) - 分量比较 !=
  • any(x) - 任意分量为true
  • all(x) - 所有分量为true
  • not(x) - 逻辑非
7. 纹理查询函数
  • texture2D(sampler, coord) - 2D纹理采样 (WebGL1)
  • texture(sampler, coord) - 通用纹理采样 (WebGL2)
  • textureCube(sampler, coord) - 立方体贴图采样
  • texture2DProj(sampler, coord) - 投影纹理采样
  • texture2DLod(sampler, coord, lod) - 带LOD的采样
  • textureSize(sampler, lod) - 获取纹理尺寸
  • texelFetch(sampler, coord, lod) - 直接获取纹素
8. 片段处理函数
  • dFdx(p) - x方向导数
  • dFdy(p) - y方向导数
  • fwidth(p) - 导数总和 (abs(dFdx(p)) + abs(dFdy(p)))
9. 噪声函数 (部分实现可能不支持)
  • noise1(x) - 一维噪声
  • noise2(x) - 二维噪声
  • noise3(x) - 三维噪声
  • noise4(x) - 四维噪声
10. 原子操作 (WebGL2/GLSL ES 3.00)
  • atomicAdd()
  • atomicMin()
  • atomicMax()
  • atomicAnd()
  • atomicOr()
  • atomicXor()
  • atomicExchange()
  • atomicCompSwap()
11. 图像操作 (WebGL2/GLSL ES 3.00)
  • imageLoad()
  • imageStore()
  • imageAtomicAdd()
  • imageAtomicMin()

总结

到这里我们讲解了GLSL语法,以及怎么写着色器,怎么传值。3D世界的大门已经被我们推开一点。接下来我们介绍图像的旋转,平移,矩阵变换,为3维度变换做准备。感谢大家观看到这里。

最后

基于webgl,webgpu做图像处理,将来配合参数少的人工智能模型还是比较有机会的。这个抽个时间再专门研究一下。有好的想法的同学欢迎留言。谢谢。

相关推荐
幼儿园技术家8 分钟前
什么是RESTful 或 GraphQL?
前端
echola_mendes1 小时前
LangChain 结构化输出:用 Pydantic + PydanticOutputParser 驯服 LLM 的“自由发挥”
服务器·前端·数据库·ai·langchain
拉不动的猪1 小时前
刷刷题46(常见的三种js继承类型及其优缺点)
前端·javascript·面试
关注我:程序猿之塞伯坦1 小时前
JavaScript 性能优化实战:突破瓶颈,打造极致 Web 体验
开发语言·前端·javascript
兰德里的折磨5501 小时前
对于后端已经实现逻辑了,而前端还没有设置显示的改造
前端·vue.js·elementui
hikktn1 小时前
【开源宝藏】30天学会CSS - DAY9 第九课 牛顿摆动量守恒动画
前端·css·开源
申朝先生3 小时前
面试的时候问到了HTML5的新特性有哪些
前端·信息可视化·html5
在下千玦3 小时前
#前端js发异步请求的几种方式
开发语言·前端·javascript
知否技术3 小时前
面试官最爱问的Vue3响应式原理:我给你讲明白了!
前端·vue.js
小周同学:4 小时前
vue将页面导出成word
前端·vue.js·word