WebGL及其图像处理入门
次访问
前言
WebGL仅仅是一个光栅化引擎,它可以根据你的代码绘制出点,线和三角形。
想要利用WebGL完成更复杂任务,取决于你能否提供合适的代码,组合使用点,线和三角形代替实现。
本文将带大家了解WebGL基础的绘制流程,并结合之前图片滤镜(基础滤镜和lut滤镜)和图像卷积(模糊、锐化等)的应用,用WebGL来实现。
获取WebGL
WebGL基于HTML5 Canvas,所以我们需要使用Canvas作为载体。
1 |
|
再通过getContext方法来获取WebGL上下文。在上面的script标签内加入下面的代码:
1 | const canvas = document.getElementById('canvas'); |
清空
1 | gl.clearColor(1.0, 1.0, 0.0, 1.0); // 设置清空颜色缓冲区时的颜色 |
直接运行上面的2行代码清空,我们可以看到canvas被填充满了黄色。
因为gl.clearColor中接受RGBA四个值的范围是0~1,所以如果gl.clearColor(0.0, 0.0, 0.0, 1.0)就会填充黑色。
画点
1 | function drawPoint() { |
运行drawPoint函数后我们会看到300x300的canvas被填充成黑色,中间有一个10x10的白色点。
现在来解读下上面的代码。
分为6步来看,前两步获取webgl并清空屏幕。
第3步获取OpenGL Shading Language(GLSL)编写的着色程序。
该语言运行于GPU,是类似于C或C++的强类型语言,它总是成对出现,每对方法中一个叫顶点着色器,另一个叫片断着色器,组合起来称作一个 program(着色程序)
顶点着色器的作用是计算顶点的位置,根据位置对点, 线和三角形在内的一些图元进行光栅化处理。片断着色器的作用是计算出当前绘制图元中每个像素的颜色值。
可以利用JavaScript中创建字符串的方式创建GLSL字符串:
1 | // 顶点着色器 |
或者跟本文的例子一样,将它们放在非JavaScript类型的标签中:
1 | <!-- 顶点着色器 --> |
这里a_position属性的数据类型是vec4,vec4是一个有四个浮点数据的数据类型。
GLSL中命名约定:
- a_ 代表属性,值从缓冲中提供;
- u_ 代表全局变量,直接对着色器设置;
- v_ 代表可变量,是从顶点着色器的顶点中插值来出来的。
获取到着色器资源后,接着创建着色器,绑定资源并编译,然后创建着色程序,把编译好的2个着色器加进来,再连接和使用该着色程序。
到这一步我们的着色程序就初始化完毕。
最后就是绘制drawArrays。
由于drawArrays之前的步骤应用频繁,下面我们把它们封装起来。
创建着色器
着色器都是成对出现的,比如本例中的vertexShader和fragmentShader。
获取着色器资源source后,根据type创建不同的着色器,vertexShader的type是gl.VERTEX_SHADER,fragmentShader的type是gl.FRAGMENT_SHADER。
然后绑定并编译。
1 | function createShader(gl, type, source) { |
创建着色程序
创建着色程序,把着色器加进来,链接程序。
1 | function createProgram(gl, vertexShader, fragmentShader) { |
然后通过着色器script标签的id,创建连接好的着色程序:
1 | function createProgramFromScripts (gl, vertexShaderScriptId, fragmentShaderScriptId) { |
封装好之后,上面的drawPoint函数就可以优化成下面这样:
1 | // 1、获取webgl |
画多个点
上面我们画了一个点,现在画多个点。
比如下面的3个点:
1 | const points = [ |
需要把这3个点的数据传给webgl:
1 | // 创建一个buffer,用来放3个点在裁剪空间的坐标 |
接着获取shader中a_position的地址并做一些配置:
1 | const positionAttributeLocation = gl.getAttribLocation(program, "a_position"); // 获取shader中a_position的地址 |
最后绘制3个点:
1 | gl.drawArrays(gl.POINTS, 0, 3); // 绘制3个点 |
把上面的绘制3个点改一下,可以绘制成三角形:
1 | gl.drawArrays(gl.TRIANGLES, 0, 3); // 绘制三角形 |
因为图元类型primitiveType为三角形gl.TRIANGLES, 顶点着色器每运行三次WebGL将会根据三个gl_Position值绘制一个三角形。
关于buffer和attribute
上面我们用到了buffer和attribute,那它们是干什么的呢?
其实,缓冲操作是GPU获取顶点数据的一种方式。
gl.createBuffer创建一个缓冲;gl.bindBuffer是设置缓冲为当前使用缓冲; gl.bufferData将数据拷贝到缓冲,这个操作一般在初始化完成。一旦数据存到缓冲中,还需要告诉WebGL怎么从缓冲中提取数据传给顶点着色器的属性。
首先,我们要获取WebGL给属性分配的地址,这一步一般在初始化时完成。
1 | // 询问顶点数据应该放在哪里 |
绘制前还需要发出3个命令。
1、告诉WebGL我们想从缓冲中提供数据。
1 | gl.enableVertexAttribArray(location); |
2、将缓冲绑定到 ARRAY_BUFFER 绑定点,它是WebGL内部的一个全局变量。
1 | gl.bindBuffer(gl.ARRAY_BUFFER, someBuffer); |
3、告诉WebGL从 ARRAY_BUFFER 当前绑定点的缓冲获取数据。
1 | gl.vertexAttribPointer( |
numComponents: 每个顶点有几个单位的数据(1 - 4)
typeOfData: 单位数据类型是什么(BYTE, FLOAT, INT, UNSIGNED_SHORT, 等等…)
normalizeFlag: 标准化
strideToNextPieceOfData: 从一个数据到下一个数据要跳过多少位
offsetIntoBuffer: 数据在缓冲的什么位置
如果每个类型的数据都用一个缓冲存储,stride 和 offset 都是 0 。
stride 为 0 表示 “用符合单位类型和单位个数的大小”。 offset 为 0 表示从缓冲起始位置开始读取。
它们用 0 以外的值会复杂得多,虽然这样会取得一些性能能上的优势, 但是一般情况下并不值得。
标准化标记(normalizeFlag)适用于所有非浮点型数据。如果传递false就解读原数据类型。
坐标转换
上面的例子中,我们的顶点坐标都是裁剪空间坐标,比如:
1 | const points = [ |
裁剪空间的x范围是[-1, 1],正方向向右,y的范围也是[-1, 1],正方向向上。
对于描述二维空间中的物体,比起裁剪空间坐标你可能更希望使用屏幕像素坐标。
所以我们来改造一下顶点着色器:
1 | <script type='x-shader/x-vertex' id='vertex-shader-2d'> |
然后我们用像素坐标表示新的3个点:
1 | const points = [ |
使用program后,我们需要获取vertex-shader-2d中添加的全局变量u_resolution的位置,并设置分辨率:
1 | const resolutionUniformLocation = gl.getUniformLocation(program, "u_resolution"); |
然后绘制三角形:
1 | gl.drawArrays(gl.TRIANGLES, 0, 3); |
这时,我们的坐标系原点在左下角,如果要像传统二维API那样原点在左上角,我们只需翻转y轴:
1 | // gl_Position = vec4(clipSpace, 0, 1); |
画矩形
我们将通过绘制两个三角形来绘制一个矩形,每个三角形有三个点,所以一共有6个点:
1 | const points = [ |
然后绘制时把次数改成6次:
1 | // 绘制 |
画图
改造着色器
首先我们接着改造上面坐标转换后的顶点着色器,增加a_texCoord和v_texCoord。
1 | attribute vec2 a_texCoord; |
然后用片断着色器寻找纹理上对应的颜色:
1 | <script id="fragment-shader-2d" type="x-shader/x-fragment"> |
加载图片
点击选择图片按钮后,加载图片,图片加载完成后开始绘制图片。
1 | const inputElement = document.getElementById('fileInput'); |
绘制图片
绘制图片我们在drawPic函数中进行,首先获取gl并创建着色程序:
1 | function drawPic(image) { |
接着找2个顶点坐标位置(分别是矩形和纹理的坐标):
1 | let positionLocation = gl.getAttribLocation(program, "a_position"); |
再画一个和图片一样大小的矩形,首先我们获取画矩形需要的6个点:
1 | const x1 = 0; |
再和上文一样,把点的数据传给webgl,并设置读取方式:
1 | let positionBuffer = gl.createBuffer(); |
然后创建纹理:
1 | // 创建纹理 |
并对图片渲染做设置,保证可以渲染任何尺寸的图片:
1 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); |
然后把图片加载到上面创建的纹理中:
1 | gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image); |
然后告诉webgl如何从裁剪空间转换到像素空间:
1 | gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); |
然后对a_texCoord的地址并做一些配置。
渲染纹理时需要纹理坐标,而不是像素坐标,无论纹理是什么尺寸,纹理坐标范围始终是 0.0 到 1.0:
1 | gl.enableVertexAttribArray(texcoordLocation); |
接着清空屏幕并使用着色程序:
1 | gl.clearColor(0, 0, 0, 0); |
再设置全局变量分辨率:
1 | let resolutionLocation = gl.getUniformLocation(program, "u_resolution"); |
到现在就可以画矩形了(6个点画2个三角形组成矩形):
1 | gl.drawArrays(gl.TRIANGLES, 0, 6); |
到这里图片就出现了:
简单说就是,我们画了一个和图片一样大的矩形,创建了一个纹理并把图片传到纹理中,再把纹理贴到矩形上,这样图片就显示出来了。
操作像素
现在我们对图片做一些简单的像素操作。
换位
比如红蓝换位,我们只需要改片段着色器:
1 | gl_FragColor = texture2D(u_image, v_texCoord).bgra; |
灰度
解析出颜色通道后,做加权算法,再重新设置颜色:
1 | vec4 color = texture2D(u_image, v_texCoord); |
更多改变像素颜色的风格算法,可以看看之前写的《前端基础滤镜》。
颜色查找表
颜色查找表又叫LUT(look up table),可以实现自定义且多样的风格化滤镜,不清楚的可以看之前写的《前端如何通过LUT实现图片滤镜》。
首先需要创建一个新的片断着色器,实现LUT算法。(顶点着色器和上面画图的一样)
1 | <script type='x-shader/x-fragment' id='fragment-shader-2d'> |
然后改造下我们的html,除了上传图片,我们还需要上传lut图片,再增加一个应用按钮:
1 | <input type="file" id="fileInput" /> |
再给它们绑定事件,上传图片后设置图片地址,点击应用按钮时应用lut滤镜效果:
1 | const fileInput = document.getElementById('fileInput'); |
接下来就是应用lut滤镜函数applyLUT,首先获取gl并创建连接好的着色程序:
1 | function applyLUT() { |
接着找地址:
1 | const positionLocation = gl.getAttribLocation(program, 'a_position'); |
然后设置canvas宽高和图片一样:
1 | canvas.width = image.width; |
再传图片和lut图片数据:
1 | const image_texture = gl.createTexture(); |
然后设置positionLocation和texcoordLocation:
1 | const positionBuffer = gl.createBuffer(); |
最后就是清空、使用着色程序、设置窗口等配置及画矩形了:
1 | gl.clearColor(0, 0, 0, 0); |
到这里我们就能看到应用滤镜后的图片,比如我们应用下面这个lut文件:
点击应用后效果:
卷积
卷积在图片处理上应用广泛,可以实现比如边缘检测、锐化、模糊等等
我们将在片断着色器中计算卷积,所以创建一个新的片断着色器。
1 | <script id="fragment-shader-2d" type="x-shader/x-fragment"> |
在JavaScript中,我们继续改造上面的drawPic函数,首先找到下面3个地址:
1 | let textureSizeLocation = gl.getUniformLocation(program, "u_textureSize"); |
然后在drawArrays前设置图片大小,提供卷积核,并设置卷积核及其权重:
1 | // 设置图片大小 |
1 | function computeKernelWeight(kernel) { |
上传图片后就能看到应用卷积核后的效果:
下面是一些常见的卷积核:
1 | let kernels = { |
也可以看看之前写的《卷积在前端图像处理上的应用》。