女朋友想学webGL修图,安排!
前言
看完小白可以用webgl实现修图功能!我们平常生活中都使用过adobe photoshop修图,各种各样的滤镜以及特效眼花缭乱,实现高斯模糊,雕刻,曝光等这些特效看起来似乎很难,那么今天我们来手敲一个简单实现。
之前讲了简单的webgl的原理与点的绘制、以及webgl在vscode需要注意的点,本文将接着介绍如何做个简单的修图功能,由于篇幅有限,只讲基本的语法、多边形绘制、缓冲区、帧缓存、纹理uv等。
预览
canvas也可以更简单的实现,getImageData可以得到点的集合,然后putImageData绘制就行了。但是一些复杂的算法,例如高斯模糊、雕刻效果,貌似就没有webgl灵活了。
createBuffer 缓冲区
缓冲区你可以理解canvas的save()保存状态,但是这里我们一般是点的集合,这里我不会讲具体的api细节,但是知道具体的代码流程就行,就是创建buffer及数据 -> 绑定数据 -> 如何加载
let bufferOrigin = gl.createBuffer()
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW)
gl.bindBuffer(gl.ARRAY_BUFFER, bufferOrigin);
gl.enableVertexAttribArray(positionAttributeLocation); // 告诉缓冲区怎么加载
gl.vertexAttribPointer(positionAttributeLocation, size, type, normalize, stride, offset);
Program 对象
const canvas = document.querySelector("#canvas");
image.width = 540
image.height = 720
canvas.style.width = 540 + 'px'
canvas.style.height = 720 + 'px'
const gl = canvas.getContext("webgl");
if (!gl) {
return;
}
const program = webglUtils.createProgramFromScripts(gl, ["vertex-shader-2d", "fragment-shader-2d"]);
image加载的dom对象,设置宽高,webglUtils是封装的方法,其实就是之前的初始化的着色器,返回program程序对象。
shader 着色器
先看看着色器源码
<script id="vertex-shader-2d" type="x-shader/x-vertex">
attribute vec2 a_position; // attribute在顶点着色器处理
attribute vec2 a_texCoord; // 纹理参数
uniform vec2 u_resolution; // 页面的坐标
attribute vec4 a_composeColor; // 纹理增强的向量
varying vec4 v_composeColor;
void main() {
// 屏幕坐标 -> 裁剪坐标
vec2 zeroToOne = a_position / u_resolution;
vec2 zeroToTwo = zeroToOne * 2.0;
vec2 clipSpace = zeroToTwo - 1.0;
gl_Position = vec4(clipSpace * vec2(1, -1), 0, 1);
v_composeColor = a_composeColor;
v_texCoord = a_texCoord;
}
</script>
attribute类型用于顶点着色器的属性,一般可以在后期动态添加一些控制,但是uniform是只能静态编译的时候就决定了,所以一般用于控制材质、光照等确定的值。为了能控制到片元着色器,那么一定要使用varying这个类型,一般通过变量传递给片元着色器做动态的渲染,所以一般会配合attriubute + varying
<script id="fragment-shader-2d" type="x-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;
varying vec4 v_composeColor;
void main() {
vec2 onePixel = vec2(1.0, 1.0) / u_textureSize;
// 卷积内核的前置处理,u_kernel我们传递的核心数据
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] +
//.....
// 计算最终的颜色结果
gl_FragColor = vec4((colorSum / u_kernelWeight).rgb, 1) * v_composeColor;
}
</script>
这里返回的结果gl_FragColor就是最终绘制的颜色。注意颜色范围是0到1需要做个转换,这里的最核心的也就是卷积的算法
卷积
卷积就是一个 3×3 的矩阵, 矩阵中的每一项代表当前处理的像素和周围8个像素的乘法因子, 相乘后将结果加起来除以内核权重(内核中所有值的和或 1.0 ,取二者中较大者)
像素矩阵 * 修改矩阵 = 赋值于内核也就是中心位置
这也就是我们能处理模糊、锐化等特效的原理, 下面是简单的计算
// 将周围八个点相加用于平均数相除
function computeKernelWeight(kernel) {
const weight = kernel.reduce(function(prev, curr) {
return prev + curr;
});
return weight <= 0 ? 1 : weight;
}
gl_FragColor = vec4((colorSum / u_kernelWeight).rgb, 1) * v_composeColor;
滤镜
const kernelsFilter = {
sharpness: {
name: '锐度',
data: [
0, -1, 0,
-1, 5, -1,
0, -1, 0
],
},
gaussianBlur: {
name: '高斯模糊',
data: [
0, 1, 0,
1, 1, 1,
0, 1, 0
],
},
edgeDetect2: {
name: '反相',
data: [
-1, -1, -1,
-1, 8, -1,
-1, -1, -1
],
},
emboss: {
name: '浮雕效果',
data: [
-2, -1, 0,
-1, 1, 1,
0, 1, 2
],
},
};
// 向量乘积的滤镜
const composeFilter = {
light: {
name: '曝光',
data: new Float32Array([1.2, 1.2, 1.2, 1])
},
langmanmeigui: {
name: '浪漫玫瑰',
data: new Float32Array([1.1, 1, 1, 1])
},
// ....
}
将上面的参数传入对上面的着色器,然后通过卷积赋值于gl_FragColor,这样简单的修图工具就大功告成了。
texcoord 纹理
const texcoordLocation = gl.getAttribLocation(program, "a_texCoord");
// ...
gl.vertexAttribPointer(texcoordLocation, size2, type2, normalize2, stride2, offsetVal2);
这里用缓冲区处理,本质图片也就是4个点的矩形,因为每个点其实对应像素和位置, 下面是创建纹理的标准代码,将image传入到textImage2D,然后将缓冲区绑定到这样我们就可以绘制纹理了
// webgl创建纹理,并设置基本纹理参数,载入image图片
const 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);
帧缓冲
如何给图片施加多种状态的叠加效果,也就是图片 -> 纹理一 -> 纹理一 + 纹理二 -> 画布,那么我们需要用到帧缓冲,其实就是通过不断的bindTexture来覆盖之前的状态。
// 绘制帧缓冲
function drawFrames () {
const originTexture = createAndSetupTexture(gl)
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
let textures = []
let frameBuffers = []
const kernelsFilterList = ['gaussianBlur', 'emboss', 'boxBlur',
'gaussianBlur', 'boxBlur', 'gaussianBlur', 'boxBlur', 'gaussianBlur'] //叠加效果的数组
for (let i = 0; i < kernelsFilterList.length; i++) {
let texture = createAndSetupTexture(gl)
textures.push(texture)
gl.texImage2D(
gl.TEXTURE_2D, 0, gl.RGBA, image.width, image.height, 0,
gl.RGBA, gl.UNSIGNED_BYTE, null);
var fBuffer = gl.createFramebuffer()
frameBuffers.push(fBuffer)
gl.bindFramebuffer(gl.FRAMEBUFFER, fBuffer);
// 绑定纹理到帧缓冲
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
}
gl.bindTexture(gl.TEXTURE_2D, originTexture);
for (var i = 0; i < kernelsFilterList.length; i++) {
setFramebuffer(frameBuffers[i], image.width, image.height);
drawWithKernel(kernelsFilterList[i]);
// 叠加
gl.bindTexture(gl.TEXTURE_2D, textures[i]);
}
// 绘制
setFramebuffer(null, canvas.width, canvas.height);
drawWithKernel("normal");
function setFramebuffer (fbo, width, height) {
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo); // 绑定帧缓存
gl.uniform2f(resolutionLocation, width, height); // 设置到裁剪坐标
gl.viewport(0, 0, width, height); // 将裁剪坐标自适应到屏幕坐标
}
}
总结
通过基本的语法、纹理使用、帧缓存等,我们对webgl的基本的2d图形处理有了一定的认知,正常在绘制三角形,四边形,圆形,我们都可以使用缓存区,最后drawArrays绘制,在一些图形的渲染需要保存之前的状态的时候,我们可以使用帧缓存处理。关于当前页面的优化,当前的修图页面应该将各种调色分到不同的glsl文件,同样我们也可以做裁剪,上传图片编辑并下载。
如果觉得文章对你有帮助,不要忘了一键三连 👍。
附录
来源:juejin.cn/post/7215977393696522299