图形学【免费下载链接】webgl-fundamentalsWebGL lessons that start with the basics项目地址https://gitcode.com/gh_mirrors/we/webgl-fundamentals点击查看免费下载导读本文是 webgl-fundamentals 开源课程系列中讲解「着色器Shader与 GLSL」的核心篇章对应仓库中的 日文版讲义 与 英文版原文。文中将系统梳理 WebGL 渲染管线中两大着色器——顶点着色器Vertex Shader与片段着色器Fragment Shader的职责与固定形态逐一拆解它们获取数据的三种途径attribute、uniform、texture/varying并深入介绍 GLSL 语言特有的向量/矩阵类型、分量选择器swizzle、强类型约束与内置函数体系。读完本文你将具备独立阅读和编写 GLSL 着色器、正确配置缓冲区与 uniform、理解 varying 插值原理的完整实战能力为后续 2D 变换、图像处理与 3D 图形打下坚实基础。本系列课程建议按顺序阅读WebGL 基础 → WebGL 的工作原理 → 本文。若尚未了解 WebGL 内部的运行机制建议先阅读「WebGL 的工作原理」一章。着色器与着色器程序Shader Program概述正如「WebGL 的工作原理」中所阐述的WebGL 每次绘制任何图形时都需要两个着色器顶点着色器Vertex Shader片段着色器Fragment Shader每一个着色器本质上都是一个「函数」。顶点着色器与片段着色器会被链接到一起构成一个「着色器程序Shader Program」通常简称为「程序Program」。一个典型的 WebGL 应用会拥有多个着色器程序分别用于不同的绘制效果。在仓库的 webgl-fundamentals.html 中可以看到两者的创建与链接过程// create GLSL shaders, upload the GLSL source, compile the shaders var vertexShader createShader(gl, gl.VERTEX_SHADER, vertexShaderSource); var fragmentShader createShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource); // Link the two shaders into a program var program createProgram(gl, vertexShader, fragmentShader);其中createShader与createProgram是仓库中提供的基础封装函数见 webgl-fundamentals.html分别完成gl.createShader→gl.shaderSource→gl.compileShader的编译流程以及gl.createProgram→gl.attachShader→gl.linkProgram的链接流程并会通过gl.getShaderParameter/gl.getProgramParameter检查编译与链接状态。顶点着色器Vertex Shader职责与固定形态顶点着色器的职责是生成裁剪空间坐标clip space coordinates。它必须采用如下固定形态void main() { // 进行某些计算生成「裁剪空间坐标」的值。 // 将计算结果赋给 gl_Position。 gl_Position doMathToMakeClipspaceCoordinates; }顶点着色器对每一个顶点调用一次。每次被调用时都必须将某个裁剪空间坐标值写入特殊的全局变量gl_Position。顶点着色器需要输入数据它获取数据的方式共有三种attribute属性——从「缓冲区buffer」中取出的数据uniform——在一次绘制调用draw call期间对所有顶点都保持相同的值texture纹理——从「像素/纹素texel」中读取的数据attribute属性从缓冲区读取数据最常用的方式是通过**缓冲区buffer**与attribute属性来传递数据这一点在「WebGL 的工作原理」中已有说明。首先创建缓冲区var buf gl.createBuffer();向缓冲区中写入数据gl.bindBuffer(gl.ARRAY_BUFFER, buf); gl.bufferData(gl.ARRAY_BUFFER, someData, gl.STATIC_DRAW);然后针对已创建的着色器程序在初始化阶段查询其 attribute 的位置locationvar positionLoc gl.getAttribLocation(someShaderProgram, a_position);在渲染阶段则需要告知 WebGL 如何从缓冲区中取出数据并送入该 attribute// 针对指定 attribute启用从缓冲区读取数据 gl.enableVertexAttribArray(positionLoc); var numComponents 3; // (x, y, z) 共 3 个数据 var type gl.FLOAT; // 32 位浮点数 var normalize false; // 不进行自动归一化处理 var offset 0; // 从缓冲区开头读取 var stride 0; // 每个顶点数据的字节数 // 0 按「数据类型与数据个数」采用相应的 stride gl.vertexAttribPointer(positionLoc, numComponents, type, false, stride, offset);其中stride用于描述「从当前顶点数据到下一个顶点数据需要跳过多少字节」在数据紧密排列时设为0即可WebGL 会自动按照type与numComponents计算正确的步长。回顾「WebGL 基础」中的示例顶点着色器可以直接把从缓冲区读取的数据原样输出不做任何计算attribute vec4 a_position; void main() { gl_Position a_position; }这个例子之所以可行是因为在把数据放入缓冲区时数据本身已经是裁剪空间坐标因此着色器内无需再做任何数学变换。仓库中的 webgl-fundamentals.html 完整演示了这套流程顶点数据positions三个 2D 裁剪空间点写入缓冲区后通过gl.vertexAttribPointer配置读取规则最终由gl.drawArrays(gl.TRIANGLES, 0, 3)触发绘制。attribute 可用的数据类型包括float、vec2、vec3、vec4、mat2、mat3、mat4。uniform一次绘制调用内保持不变的公共值uniform 是指着色器中在一次绘制调用即一次gl.drawArrays/gl.drawElements调用期间对所有顶点都保持相同值的变量。最简单的例子是在顶点着色器中为坐标增加一个偏移量attribute vec4 a_position; uniform vec4 u_offset; void main() { gl_Position a_position u_offset; }这样就能为每个顶点都加上相同的偏移量从而使整个图形产生平移。使用 uniform 时首先在初始化阶段查询着色器程序中定义的 uniform 位置var offsetLoc gl.getUniformLocation(someProgram, u_offset);然后在绘制前设置 uniform 的值gl.uniform4fv(offsetLoc, [1, 0, 0, 0]); // 向右X 正方向移动半屏uniform 的重要特性使用 uniform 时需要注意uniform 隶属于各自的着色器程序。如果拥有多个着色器程序且它们定义了同名的 uniform这些 uniform 彼此独立各自拥有不同的位置location也各自保存不同的值。调用gl.uniform???时只会设置「当前程序current program」的 uniform而当前程序由最后一次调用gl.useProgram所指定的程序决定。uniform 的完整类型与对应设置函数uniform 可以使用的类型很多设置数据时必须调用与着色器程序中 uniform 定义相匹配的函数gl.uniform1f (floatUniformLoc, v); // float gl.uniform1fv(floatUniformLoc, [v]); // float或 float 数组 gl.uniform2f (vec2UniformLoc, v0, v1); // vec2 gl.uniform2fv(vec2UniformLoc, [v0, v1]); // vec2或 vec2 数组 gl.uniform3f (vec3UniformLoc, v0, v1, v2); // vec3 gl.uniform3fv(vec3UniformLoc, [v0, v1, v2]); // vec3或 vec3 数组 gl.uniform4f (vec4UniformLoc, v0, v1, v2, v4); // vec4 gl.uniform4fv(vec4UniformLoc, [v0, v1, v2, v4]); // vec4或 vec4 数组 gl.uniformMatrix2fv(mat2UniformLoc, false, [ 元素个数为 4 的倍数的数组 ]) // mat2或 mat2 数组 gl.uniformMatrix3fv(mat3UniformLoc, false, [ 元素个数为 9 的倍数的数组 ]) // mat3或 mat3 数组 gl.uniformMatrix4fv(mat4UniformLoc, false, [ 元素个数为 16 的倍数的数组 ]) // mat4或 mat4 数组 gl.uniform1i (intUniformLoc, v); // int gl.uniform1iv(intUniformLoc, [v]); // int或 int 数组 gl.uniform2i (ivec2UniformLoc, v0, v1); // ivec2 gl.uniform2iv(ivec2UniformLoc, [v0, v1]); // ivec2或 ivec2 数组 gl.uniform3i (ivec3UniformLoc, v0, v1, v2); // ivec3 gl.uniform3iv(ivec3UniformLoc, [v0, v1, v2]); // ivec3或 ivec3 数组 gl.uniform4i (ivec4UniformLoc, v0, v1, v2, v4); // ivec4 gl.uniform4iv(ivec4UniformLoc, [v0, v1, v2, v4]); // ivec4或 ivec4 数组 gl.uniform1i (sampler2DUniformLoc, v); // sampler2D纹理 gl.uniform1iv(sampler2DUniformLoc, [v]); // sampler2D或 sampler2D 数组 gl.uniform1i (samplerCubeUniformLoc, v); // samplerCube纹理 gl.uniform1iv(samplerCubeUniformLoc, [v]); // samplerCube或 samplerCube 数组此外还有处理布尔值的bool、bvec2、bvec3、bvec4类型设置它们可以使用gl.uniform?f?或gl.uniform?i?系列函数。uniform 数组的批量设置与逐元素设置若 uniform 被定义为数组可以一次性设置整个数组的值// 着色器侧代码 uniform vec2 u_someVec2[3];// JavaScript 侧代码初始化阶段 var someVec2Loc gl.getUniformLocation(someProgram, u_someVec2); // JavaScript 侧代码渲染阶段 gl.uniform2fv(someVec2Loc, [1, 2, 3, 4, 5, 6]); // 一次性设置 u_someVec2 整个数组也可以逐个元素设置但此时必须分别获取数组每个元素的位置// JavaScript 侧代码初始化阶段 var someVec2Element0Loc gl.getUniformLocation(someProgram, u_someVec2[0]); var someVec2Element1Loc gl.getUniformLocation(someProgram, u_someVec2[1]); var someVec2Element2Loc gl.getUniformLocation(someProgram, u_someVec2[2]); // JavaScript 侧代码渲染阶段 gl.uniform2fv(someVec2Element0Loc, [1, 2]); // 设置 u_someVec2[0] 的值 gl.uniform2fv(someVec2Element1Loc, [3, 4]); // 设置 u_someVec2[1] 的值 gl.uniform2fv(someVec2Element2Loc, [5, 6]); // 设置 u_someVec2[2] 的值结构体structuniform 的字段级位置查询GLSL 还支持结构体语法struct SomeStruct { bool active; vec2 someVec2; }; uniform SomeStruct u_someThing;使用结构体时需要针对结构体的每一个字段分别查询位置var someThingActiveLoc gl.getUniformLocation(someProgram, u_someThing.active); var someThingSomeVec2Loc gl.getUniformLocation(someProgram, u_someThing.someVec2);顶点着色器中使用纹理顶点着色器中读取纹理的方法与片段着色器中使用纹理相同详见后文。片段着色器Fragment Shader职责与固定形态片段着色器的职责是为被光栅化的当前像素决定颜色。它必须采用如下固定形态precision mediump float; void main() { // 进行某些计算决定「像素的颜色」。 // 将计算结果赋给 gl_FragColor。 gl_FragColor doMathToMakeAColor; }片段着色器对每一个像素调用一次。每次被调用时都必须将某种颜色信息写入特殊的全局变量gl_FragColor。注意片段着色器没有默认的精度声明因此开头必须显式写出precision mediump float;mediump是一个不错的默认选择见仓库 webgl-fundamentals.html 中的实际用法。片段着色器同样需要数据获取方式有三种uniform——一次绘制调用期间对所有像素都保持相同的值texture纹理——从「像素/纹素」读取的数据varying——从顶点着色器传来的数据会按需进行插值片段着色器中的 uniform其机制与顶点着色器中的 uniform 完全相同请参照前述章节。片段着色器中使用纹理在着色器中从纹理取值需要声明sampler2D类型的 uniform并使用 GLSL 函数texture2D从中取出值precision mediump float; uniform sampler2D u_texture; void main() { vec2 texcoord vec2(0.5, 0.5); // 获取纹理中央的值 gl_FragColor texture2D(u_texture, texcoord); }从纹理中取出的数据究竟是什么样的取决于众多设置项其细节在「3D 纹理」一章中有深入讲解这里先用一个满足最低需求的示例来说明纹理的完整使用流程。首先创建纹理并设置数据var tex gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, tex); var level 0; var width 2; var height 1; var data new Uint8Array([ 255, 0, 0, 255, // 红色像素 0, 255, 0, 255, // 绿色像素 ]); gl.texImage2D(gl.TEXTURE_2D, level, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, data); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);在初始化阶段查询着色器程序中定义的 uniform 位置var someSamplerLoc gl.getUniformLocation(someProgram, u_texture);在渲染阶段首先将纹理绑定到某一个「纹理单元texture unit」var unit 5; // 指定某一个纹理单元 gl.activeTexture(gl.TEXTURE0 unit); gl.bindTexture(gl.TEXTURE_2D, tex);然后告诉着色器「你绑定的是哪个纹理单元」gl.uniform1i(someSamplerLoc, unit);关于纹理单元的补充说明所有纹理相关的 WebGL API 都作用于「活动纹理单元active texture unit」它相当于一个全局变量指明当前操作作用于哪一个纹理单元。仓库 webgl-2-textures.md 对此有详细解释并用「用 JavaScript 模拟 WebGL 内部实现」的方式展示了gl.activeTexture/gl.bindTexture/gl.texImage2D背后所维护的textureUnits数组与activeTextureUnit索引。注意 uniform 侧使用从 0 开始的整数指定纹理单元如gl.uniform1i(u_image0Location, 0)而gl.activeTexture侧使用gl.TEXTURE0、gl.TEXTURE1等预定义常量好在这些常量被定义为连续的整数值因此可以写成gl.activeTexture(gl.TEXTURE0 ii)这样的循环形式见 webgl-2-textures.md。仓库中的 webgl-3d-textures.html 展示了sampler2Dtexture2D在实际 3D 示例中的标准用法配合从顶点着色器传来的 varying 纹理坐标使用。varying从顶点着色器到片段着色器的数据通道varying 是从顶点着色器向片段着色器传递数据的手段这在「WebGL 的工作原理」中已有介绍。使用 varying 时需要在顶点着色器与片段着色器中成对声明同名的 varying。顶点着色器针对每个顶点调用一次并在其中设置「该顶点的 varying 值」。当 WebGL 绘制像素时会对顶点之间的区域进行插值interpolation——即填补顶点与顶点之间的空白——同时对 varying 的值也进行插值。片段着色器被调用时拿到的就是插值后的 varying 值。顶点着色器attribute vec4 a_position; uniform vec4 u_offset; varying vec4 v_positionWithOffset; void main() { gl_Position a_position u_offset; v_positionWithOffset a_position u_offset; }片段着色器precision mediump float; varying vec4 v_positionWithOffset; void main() { // 将「裁剪空间坐标值」-1 1转换为「颜色空间坐标值」0 1。 vec4 color v_positionWithOffset * 0.5 0.5; gl_FragColor color; }上述示例本身在实际应用中没有太大意义——一般很少会直接把裁剪空间坐标原样传给片段着色器并当作颜色使用。但通过观察该示例产生的「中间色」可以直观地理解 varying 的插值究竟是怎么回事。在「WebGL 的工作原理」一章中varying 的插值机制有更直观的演示三个顶点的坐标0,-100、150,125、-175,100经矩阵变换后写入gl_Position同时被换算成v_color传给片段着色器见 webgl-how-it-works.mdWebGL 获取三个顶点的值之后通过插值将三角形光栅化片段着色器则对构成三角形的每一个像素各调用一次见 webgl-how-it-works.md。从源码结构看仓库中 webgl-2d-triangle-with-position-for-color.html 正是该讲解对应的可运行示例。GLSL 语言深入GLSL 是「Graphics Library Shader Language图形库着色器语言」的缩写正是编写着色器所用的语言。它具备一些在 JavaScript 中不常见的独特特性其设计目标就是针对图形光栅化过程中常见的数学计算。以下逐一介绍 GLSL 的核心语言特性。矩阵相关的数据类型GLSL 内置了vec2、vec3、vec4数据类型分别表示 2 个、3 个、4 个数据的组合同样地mat2、mat3、mat4用于表示 2x2、3x3、4x4 的矩阵。有了这些类型可以方便地写出诸如「矩阵乘以标量非矩阵的普通数值」的计算vec4 a vec4(1, 2, 3, 4); vec4 b a * 2.0; // b 的值为 vec4(2, 4, 6, 8)。矩阵与矩阵相乘、向量与矩阵相乘也都可以直接书写mat4 a ???; mat4 b ???; mat4 c a * b; vec4 v ???; vec4 y c * v;分量选择器Selectors与 swizzle各种 vec 类型变量拥有丰富的「选择器selector」。以vec4为例vec4 v;v.x与v.s、v.r、v[0]意义相同v.y与v.t、v.g、v[1]意义相同v.z与v.p、v.b、v[2]意义相同v.w与v.q、v.a、v[3]意义相同利用这一点可以轻松实现 vec 分量的重排称为swizzle不仅可以交换顺序还可以重复使用同一元素。例如v.yyyy等价于vec4(v.y, v.y, v.y, v.y)同理v.bgra等价于vec4(v.b, v.g, v.r, v.a)在构造 vec 或 mat 值时还可以一次性写入多个部分vec4(v.rgb, 1)等价于vec4(v.r, v.g, v.b, 1)另外vec4(1)会被解释为vec4(1, 1, 1, 1)严格的数据类型约束GLSL 是一种强类型语言对数据类型要求非常严格这一点常常让人踩坑float f 1; // 错误。1 是 int 类型不能把 int 值赋给 float 变量。正确写法是以下之一float f 1.0; // 使用 float 值即可 float f float(1) // 将整数值强制转换为 float 类型前面例子中的vec4(v.rgb, 1)之所以不会对1报错是因为vec4在内部对参数执行了类似float(1)的强制类型转换。丰富的内置函数GLSL 内置了大量函数其中许多可以同时处理多个分量。例如计算角度angle正弦sine的函数T sin(T angle)其中 T 可以是float、vec2、vec3、vec4中的任意类型。传入vec4类型的值时返回的也是vec4即对每个分量分别求正弦。具体而言若v是vec4类型vec4 s sin(v);等价于vec4 s vec4(sin(v.x), sin(v.y), sin(v.z), sin(v.w));有时某个参数是 float其余是 T此时该 float 会作用于所有分量。例如v1与v2为vec4类型、f为 float 类型时vec4 m mix(v1, v2, f);等价于vec4 m vec4( mix(v1.x, v2.x, f), mix(v1.y, v2.y, f), mix(v1.z, v2.z, f), mix(v1.w, v2.w, f));精度声明GLSL 还要求着色器尤其是片段着色器声明数值精度。片段着色器没有默认精度所以必须以precision mediump float;之类的语句开头顶点着色器通常可以使用默认精度但显式声明依然是良好的习惯。这一约定在仓库所有示例的片段着色器中都可见例如 webgl-fundamentals.html。关于精度选择highp/mediump/lowp的更多细节可参考仓库中的 webgl-qna-when-to-choose-highp--mediump--lowp-in-shaders.md 问答文章。总结将一切串起来这正是本系列 WebGL 讲义的核心要点WebGL 应用本质上就是「创建各种着色器 → 向着色器提供数据 → 调用gl.drawArrays或gl.drawElements」。调用之后WebGL 会自动完成为每一个顶点调用一次顶点着色器处理全部顶点为每一个像素调用一次片段着色器处理全部像素最终将像素绘制出来。以上就是 WebGL 的全部。仓库中的 webgl-fundamentals.html 用约 150 行代码完整演示了这一过程编译两个 GLSL 着色器、链接成程序、配置 attribute 缓冲区、调用一次gl.drawArrays即完成一次最简单的三角形绘制。通往下一课方向选择「创建着色器」需要编写若干行代码不过这段代码在绝大多数 WebGL 程序中都是通用且可复用的——写好一次之后基本无需再关心。关于「编译 GLSL 着色器并链接成着色器程序」的具体封装已在「WebGL 的模板代码Boilerplate」一章中专门讲解仓库中对应实现见 webgl-fundamentals.html 中的createShader与createProgram函数。接下来有两条路线可供选择对图像处理感兴趣下一步建议学习「WebGL 中的图像处理」对平移、旋转、缩放乃至最终的三维处理感兴趣请从「二维平移」开始该主题会逐步过渡到三维处理。在这两条路线中本文介绍的 attribute、uniform、texture、varying 四种数据通道与 GLSL 基础语法都是最核心的底层能力值得反复研读、动手验证。赞分享图形学【免费下载链接】webgl-fundamentalsWebGL lessons that start with the basics项目地址https://gitcode.com/gh_mirrors/we/webgl-fundamentals点击查看免费下载相关推荐WebGL 着色器与 GLSL 完全指南顶点着色器、片段着色器与着色器语言详解WebGL 着色器与 GLSL 完全指南顶点着色器、片段着色器与着色器语言详解 本文是 WebGL 基础 https://link.gitcode.com/i图形学Bruno Simon Folio 2019Three.js与GLSL着色器编程终极指南Bruno Simon Folio 2019Three.js与GLSL着色器编程终极指南 Bruno Simon Folio 2019是一个基于Three.j前端3D渲染Sigma.js的WebGL渲染原理顶点着色器与片元着色器深度剖析Sigma.js的WebGL渲染原理顶点着色器与片元着色器深度剖析 Sigma.js 是一个专注 图可视化 的 JavaScript 库能够借助 WebGL数据可视化前端图形学创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考