webgl-fundamentals 教程解读:在 WebGL 中实现图像处理(纹理采样、像素偏移与 3x3 卷积核)
图形学【免费下载链接】webgl-fundamentalsWebGL lessons that start with the basics项目地址https://gitcode.com/gh_mirrors/we/webgl-fundamentals点击查看免费下载WebGL 中的图像处理远比想象中简单只要掌握纹理texture与纹理坐标texture coordinate这两个概念把图片交给 GPU 的片元着色器就能用几行 GLSL 实现通道交换、模糊、锐化、边缘检测等常见滤镜。本篇基于 webgl-fundamentals 仓库的 图像处理教程日文版 展开结合仓库中 webgl-2d-image.html、webgl-2d-image-3x3-convolution.html 等可运行示例从零讲解把一张图片画到 canvas 上到用卷积核做任意图像处理的完整链路读完即可自己动手扩展滤镜效果。一、为什么图像处理在 WebGL 中很简单图像处理之所以简单是因为 WebGL 本身就是一个极其擅长并行计算的引擎每个像素都由片元着色器独立计算天然适合做像素级操作。与传统的用 JavaScript 逐像素读写 ImageData不同WebGL 把计算下放到 GPU你只需要把图片上传为纹理用片元着色器决定每个像素输出什么颜色通过修改着色器中的取样逻辑得到不同的处理效果。仓库中的示例就是这条思路的最小实现只画一个矩形WebGL 中实际是两个三角形把图片贴在矩形上然后在片元着色器中逐像素处理。二、纹理与纹理坐标0.0 到 1.0 的世界绘制图像必须使用纹理。与渲染时 WebGL 使用裁剪空间坐标clip space而非像素坐标一样读取纹理时 WebGL 使用的是纹理坐标纹理坐标与纹理的实际尺寸无关无论图片是 64×64 还是 4096×4096坐标范围都是0.0 到 1.0纹理数据的起点对应坐标(0.0, 0.0)终点对应(1.0, 1.0)坐标从左到右、从第一行像素到最后一行像素递增上/下在纹理空间中并无意义只有当你把它贴到几何体上、并确定朝向之后才有上下之分。仓库在 3D 纹理教程 中给出了从像素坐标换算到纹理坐标的公式可帮助理解这一映射关系texcoordX pixelCoordX / (width - 1) texcoordY pixelCoordY / (height - 1)下图展示了同一张 F 纹理上不同像素坐标与纹理坐标的对应关系既然只画一个矩形两个三角形就需要告诉 WebGL纹理的哪一部分对应矩形的哪个点。这个信息从顶点着色器传递到片元着色器使用的是名为varying的特殊变量——之所以叫 varying是因为它的值会在顶点之间变化GPU 在绘制每个像素时会对顶点着色器提供的值进行插值interpolate。三、顶点着色器新增纹理坐标属性并传递以上一篇 WebGL 基础教程 的顶点着色器为基础需要新增一个接收纹理坐标的 attribute并把它通过 varying 传给片元着色器attribute vec2 a_texCoord; ... varying vec2 v_texCoord; void main() { ... // 将 texCoord纹理坐标传给片元着色器。 // GPU 在对各顶点之间做插值时会同步插值这个 texCoord 的值。 v_texCoord a_texCoord; }完整实现见仓库 webgl-2d-image.html 中的vertex-shader-2d它把像素坐标先换算到 0~1再换算到裁剪空间并沿 Y 轴翻转同时原样把a_texCoord交给v_texCoord。四、片元着色器用 texture2D 从纹理取样片元着色器的任务是把应该画什么颜色的决定权交给纹理——通过取样器sampler从纹理中查色script idfragment-shader-2d typex-shader/x-fragment precision mediump float; // 这就是本次使用的纹理 uniform sampler2D u_image; // 顶点着色器传来的纹理坐标 varying vec2 v_texCoord; void main() { // 从纹理中查询颜色。 gl_FragColor texture2D(u_image, v_texCoord); } /scripttexture2D接收两个参数纹理取样器u_image与纹理坐标v_texCoord返回该坐标处的纹素颜色。这段代码本身就是一个原样输出的滤镜——把纹理像素一比一画到屏幕上。五、JavaScript 侧加载图片、创建纹理、上传数据着色器就绪后还需要完成三件事加载图片 → 创建纹理 → 把图片复制进纹理。浏览器中图片加载是异步的因此代码需要重组等onload触发后再开始渲染。function main() { var image new Image(); image.src http://someimage/on/our/server; // 必须与页面同源 image.onload function() { render(image); } } function render(image) { ... // 此前写过的全部代码…… ... // 获取纹理坐标数据的去向attribute位置。 var texCoordLocation gl.getAttribLocation(program, a_texCoord); // 定义矩形各顶点与纹理坐标的对应关系 var texCoordBuffer gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([ 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0]), gl.STATIC_DRAW); gl.enableVertexAttribArray(texCoordLocation); gl.vertexAttribPointer(texCoordLocation, 2, gl.FLOAT, false, 0, 0); // 创建纹理 var texture gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, texture); // 设置参数以便渲染任意尺寸的图片 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); // 把图片数据上传到纹理 gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image); ... }逐段说明各 API 的作用API作用备注gl.getAttribLocation(program, a_texCoord)查询顶点着色器中 attribute 的位置返回的 location 是后续绑定的依据gl.createBuffer()/gl.bindBuffer(gl.ARRAY_BUFFER, ...)创建并绑定顶点缓冲纹理坐标数据存放在 ARRAY_BUFFER 中gl.bufferData(..., gl.STATIC_DRAW)上传 6 个顶点2 个三角形的纹理坐标左上(0,0)、右上(1,0)、左下(0,1)……对应两个三角形gl.vertexAttribPointer(texCoordLocation, 2, gl.FLOAT, false, 0, 0)告诉 WebGL 如何从缓冲中读取数据每顶点 2 个分量x、yFLOAT 类型gl.createTexture()/gl.bindTexture(gl.TEXTURE_2D, ...)创建纹理并绑定到纹理目标后续参数与数据上传都作用于当前绑定纹理gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S/T, gl.CLAMP_TO_EDGE)设置 S/T 方向越界采样时夹取边缘保证任意尺寸含非 2 的幂图片都能渲染gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN/MAG_FILTER, gl.NEAREST)缩/放采样使用最近邻滤波便于逐像素精确处理避免插值带来的串色gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image)将 HTMLImageElement 上传为纹素数据内部格式与外部格式均为 RGBA注意WebGL 加载图片存在安全限制CORS本地直接双击 HTML 文件无法运行这类示例。需要启动一个简单的本地 Web 服务器几分钟即可搞定具体步骤见 WebGL 开发环境教程仓库还提供了npm -g install servez后运行servez 解压目录的方式。到这里图片已经能在 WebGL 中渲染出来对应示例 webgl-2d-image.html。六、第一个滤镜交换红蓝通道渲染图片只是起点真正的乐趣在于处理。最简单的一个效果是交换红R与蓝B通道——只需把片元着色器里的一行取样结果从.rgba改为.bgra... gl_FragColor texture2D(u_image, v_texCoord).bgra; ...GLSL 向量支持分量重排swizzle.bgra会按 B、G、R、A 的顺序重组颜色视觉上红色与蓝色互换。对应示例 webgl-2d-image-red2blue.html。七、参考其他像素onePixel 计算与均值模糊通道交换只使用当前像素。若想实现需要查看邻居像素的滤镜如模糊该怎么办由于 WebGL 使用 0.0~1.0 的纹理坐标引用纹理在原始图像中移动 1 个像素所需的纹理坐标偏移量可用简单除法求得onePixel 1.0 / textureSize下面这个片元着色器实现水平方向三像素取平均的模糊效果script idfragment-shader-2d typex-shader/x-fragment precision mediump float; // 本次使用的纹理 uniform sampler2D u_image; uniform vec2 u_textureSize; // 顶点着色器传来的纹理坐标 varying vec2 v_texCoord; void main() { // 求出1 个像素在纹理坐标中的大小 vec2 onePixel vec2(1.0, 1.0) / u_textureSize; // 取左、中、右三个像素的颜色平均值 gl_FragColor ( texture2D(u_image, v_texCoord) texture2D(u_image, v_texCoord vec2(onePixel.x, 0.0)) texture2D(u_image, v_texCoord vec2(-onePixel.x, 0.0))) / 3.0; } /scriptJavaScript 侧只需把图片尺寸作为 uniform 传入var textureSizeLocation gl.getUniformLocation(program, u_textureSize); ... // 设置图片尺寸 gl.uniform2f(textureSizeLocation, image.width, image.height);对比未模糊的 原图示例即可看到水平方向的柔化效果对应示例 webgl-2d-image-blend.html。八、卷积核Convolution Kernel通用化图像处理既然知道如何引用其他像素就可以用**卷积核convolution kernel**把图像处理泛化。以 3×3 卷积核为例绘制某个像素时同时参考上下左右及对角方向共 8 个相邻像素用 3×3 矩阵中的每个元素指定每个邻居像素的贡献权重把 9 个采样结果加权求和后除以核的**权重weight即核内所有元素之和**与 1.0 中的较大者得到最终颜色可以理解为均值模糊机制的推广——不同的核系数组合产生模糊、锐化、边缘检测等不同效果。8.1 片元着色器实现script idfragment-shader-2d typex-shader/x-fragment precision mediump float; // 本次使用的纹理 uniform sampler2D u_image; uniform vec2 u_textureSize; uniform float u_kernel[9]; uniform float u_kernelWeight; // 顶点着色器传来的纹理坐标 varying vec2 v_texCoord; void main() { vec2 onePixel vec2(1.0, 1.0) / u_textureSize; vec4 colorSum texture2D(u_image, v_texCoord onePixel * vec2(-1, -1)) * u_kernel[0] texture2D(u_image, v_texCoord onePixel * vec2( 0, -1)) * u_kernel[1] texture2D(u_image, v_texCoord onePixel * vec2( 1, -1)) * u_kernel[2] texture2D(u_image, v_texCoord onePixel * vec2(-1, 0)) * u_kernel[3] texture2D(u_image, v_texCoord onePixel * vec2( 0, 0)) * u_kernel[4] texture2D(u_image, v_texCoord onePixel * vec2( 1, 0)) * u_kernel[5] texture2D(u_image, v_texCoord onePixel * vec2(-1, 1)) * u_kernel[6] texture2D(u_image, v_texCoord onePixel * vec2( 0, 1)) * u_kernel[7] texture2D(u_image, v_texCoord onePixel * vec2( 1, 1)) * u_kernel[8] ; // 对求和结果的 RGB 部分除以权重Alpha 固定为 1.0 gl_FragColor vec4((colorSum / u_kernelWeight).rgb, 1.0); } /script注意采样顺序与核数组索引的对应关系核按从上到下、从左到右排列u_kernel[0]对应左上角(-1,-1)u_kernel[4]对应中心(0,0)u_kernel[8]对应右下角(1,1)。8.2 JavaScript 侧传入核与权重从 JavaScript 发送卷积核及其权重核心是computeKernelWeight——它把所有元素求和若权重小于等于 0常见于边缘检测类核其系数和为 0则回退为 1防止除零function computeKernelWeight(kernel) { var weight kernel.reduce(function(prev, curr) { return prev curr; }); return weight 0 ? 1 : weight; } ... var kernelLocation gl.getUniformLocation(program, u_kernel[0]); var kernelWeightLocation gl.getUniformLocation(program, u_kernelWeight); ... var edgeDetectKernel [ -1, -1, -1, -1, 8, -1, -1, -1, -1 ]; gl.uniform1fv(kernelLocation, edgeDetectKernel); gl.uniform1f(kernelWeightLocation, computeKernelWeight(edgeDetectKernel));u_kernel是 GLSL 数组 uniformgl.getUniformLocation用u_kernel[0]取得数组起点随后gl.uniform1fv一次性传入 9 个浮点数。8.3 仓库内置的 20 个卷积核仓库示例 webgl-2d-image-3x3-convolution.html 预置了 20 种卷积核可通过下拉框实时切换对比效果名称核3×3用途normal中心为 1、其余为 0原样输出gaussianBlur0.045 / 0.122 / 0.045、0.122 / 0.332 / 0.122、0.045 / 0.122 / 0.045高斯模糊归一化权重gaussianBlur21/2/1、2/4/2、1/2/1高斯模糊按 1/16 归一gaussianBlur30/1/0、1/1/1、0/1/0轻度模糊十字形unsharpen-1/-1/-1、-1/9/-1、-1/-1/-1反锐化掩膜提锐sharpness0/-1/0、-1/5/-1、0/-1/0锐化sharpen-1/-1/-1、-1/16/-1、-1/-1/-1强锐化edgeDetect-0.125 均布、中心 1边缘检测保持中心亮度edgeDetect2-1/-1/-1、-1/8/-1、-1/-1/-1经典拉普拉斯边缘检测edgeDetect3-5/0/0、0/0/0、0/0/5对角线边缘检测edgeDetect4上排 -1、中排 0、下排 1水平边缘检测edgeDetect5-1/-1/-1、2/2/2、-1/-1/-1水平边缘检测变体edgeDetect6-5/-5/-5、-5/39/-5、-5/-5/-5强边缘/高反差sobelHorizontal1/2/1、0/0/0、-1/-2/-1Sobel 水平方向sobelVertical1/0/-1、2/0/-2、1/0/-1Sobel 垂直方向previtHorizontal1/1/1、0/0/0、-1/-1/-1Prewitt 水平方向previtVertical1/0/-1、1/0/-1、1/0/-1Prewitt 垂直方向boxBlur0.111 均布 9 元素盒式模糊均值triangleBlur0.0625/0.125/0.0625、0.125/0.25/0.125、0.0625/0.125/0.0625三角二项式模糊emboss-2/-1/0、-1/1/1、0/1/2浮雕效果每个核的权重若元素和为 0 则按 1 处理由computeKernelWeight自动计算无需手工指定。九、为什么示例中没有给 u_image 赋值也能工作细心的读者会发现所有示例代码都只调用了gl.bindTexture从未显式给u_image设置值。原因有两个 WebGL 默认规则uniform 默认值为 0u_image默认引用纹理单元 0texture unit 0默认激活的纹理单元就是单元 0gl.bindTexture会把纹理自动绑定到单元 0。因此u_image无需显式赋值即可正常工作。WebGL 内部维护着一组纹理单元每个 sampler uniform 引用哪个单元需要通过查询 sampler uniform 的位置 → 用gl.uniform1i设置单元索引两步完成var textureUnitIndex 6; // 使用纹理单元 6 var u_imageLoc gl.getUniformLocation(program, u_image); gl.uniform1i(u_imageLoc, textureUnitIndex);若要把纹理放到其他单元先调用gl.activeTexture激活该单元再绑定// 把纹理 someTexture 绑定到纹理单元 6 gl.activeTexture(gl.TEXTURE6); gl.bindTexture(gl.TEXTURE_2D, someTexture);或利用gl.TEXTURE0的枚举连续性var textureUnitIndex 6; gl.activeTexture(gl.TEXTURE0 textureUnitIndex); gl.bindTexture(gl.TEXTURE_2D, someTexture);关于纹理单元数量需要留意规格限制所有 WebGL 实现必须在片元着色器中支持至少 8 个纹理单元在顶点着色器中则可能为 0需要超过 8 个单元时先用gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS)查询可用数量若要在顶点着色器中使用纹理用gl.getParameter(gl.MAX_VERTEX_TEXTURE_IMAGE_UNITS)查询从实际实现看超过 99% 的机器在顶点着色器中支持至少 4 个纹理单元。十、GLSL 命名约定a_、u_、v_ 前缀的含义教程中的着色器变量带有a_、u_、v_前缀这并非 GLSL 语法要求而是教程采用的命名约定好处是一眼即可看出变量来源前缀含义数据来源a_attribute由顶点缓冲buffer提供的顶点数据u_uniform从 JavaScript 传入着色器的全局常量v_varying从顶点着色器传到片元着色器、在绘制每个像素时被插值的值各变量在 WebGL 管线中的位置与插值细节可参考 WebGL 的工作原理。十一、进阶方向给图片叠加多个效果单张图片只套一个卷积核还不过瘾教程的续篇 图像处理续 介绍了帧缓冲framebuffer方案创建两张纹理与两个帧缓冲按原图 → 模糊 → Texture 1 → 锐化 → Texture 2 → 边缘检测 → Texture 1 → 模糊 → Texture 2 → 原样输出 → Canvas的顺序交替读写实现效果链。核心思路是// 创建两张纹理并分别附加到两个帧缓冲 for (var ii 0; ii 2; ii) { var texture createAndSetupTexture(gl); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, image.width, image.height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); var fbo gl.createFramebuffer(); gl.bindFramebuffer(gl.FRAMEBUFFER, fbo); gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0); } // 交替应用各效果 for (var ii 0; ii effectsToApply.length; ii) { setFramebuffer(framebuffers[ii % 2], image.width, image.height); drawWithKernel(effectsToApply[ii]); gl.bindTexture(gl.TEXTURE_2D, textures[ii % 2]); } // 最终写回 canvas setFramebuffer(null, canvas.width, canvas.height); drawWithKernel(normal);其中gl.bindFramebuffer(gl.FRAMEBUFFER, null)表示写回 canvas 而非帧缓冲且每次切换渲染目标都必须通过gl.viewport同步设置正确的视口尺寸。该方案的交互式实现见 webgl-2d-image-processing.html。十二、小结与下一步回顾全篇WebGL 图像处理的简单体现在清晰的四步流水线上上传gl.texImage2D把图片变成纹理映射纹理坐标0.0~1.0决定每个片元采样纹理的哪个位置取样texture2D在片元着色器中取色计算修改取样逻辑通道重排、邻域采样、卷积核加权即可得到任意滤镜。本文所涉全部示例均可在仓库webgl/目录下直接运行基础渲染见 webgl-2d-image.html红蓝交换见 webgl-2d-image-red2blue.html均值模糊见 webgl-2d-image-blend.html20 种卷积核的可交互演示见 webgl-2d-image-3x3-convolution.html。着色器编译、程序链接等样板代码由 webgl-utils.js 的createProgramFromScripts提供封装示例本身只保留与主题直接相关的绘制逻辑。下一步若想深入可继续学习 给图片叠加多个效果、2D 平移变换 或 WebGL 内部工作原理逐步进入三维世界。赞分享图形学【免费下载链接】webgl-fundamentalsWebGL lessons that start with the basics项目地址https://gitcode.com/gh_mirrors/we/webgl-fundamentals点击查看免费下载相关推荐WebGL 纹理贴图实战指南从着色器采样到纹理图集webgl-fundamentals 系列WebGL 纹理贴图实战指南从着色器采样到纹理图集webgl fundamentals 系列 本指南以 webgl fundamentals https:图形学webgl-fundamentals 实战在 WebGL 中使用 2 个以上纹理纹理单元 Texture Unit 详解webgl fundamentals 实战在 WebGL 中使用 2 个以上纹理纹理单元 Texture Unit 详解 在 webgl fundamen图形学WebGL 纹理完全指南纹理坐标、mipmap、NPOT 限制与纹理图集webgl-fundamentals 实战WebGL 纹理完全指南纹理坐标、mipmap、NPOT 限制与纹理图集webgl fundamentals 实战 本文是 webgl fundament图形学创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考