WebGL 理论基础 - 图像处理 上 - 图1

    此文上接 WebGL 基础概念,如果没有读过我建议你先看这里。

    在 WebGL 中绘制图片需要使用纹理。和 WebGL 渲染时需要裁剪空间坐标相似,渲染纹理时需要纹理坐标,而不是像素坐标。无论纹理是什么尺寸,纹理坐标范围始终是 0.0 到 1.0 。

    因为我们只用画一个矩形(其实是两个三角形),所以需要告诉 WebGL 矩形中每个顶点对应的纹理坐标。 我们将使用一种特殊的叫做 ‘varying’ 的变量将纹理坐标从顶点着色器传到片段着色器,WebGL会用顶点着色器中值的进行插值,然后传给对应像素执行的片段着色器。

    接着用上篇文章中最后一个顶点着色器,我们需要添加一个属性,用它接收纹理坐标然后传给片段着色器。

    1. attribute vec2 a_texCoord;
    2. ...
    3. varying vec2 v_texCoord;
    4. void main() {
    5. ...
    6. // 将纹理坐标传给片段着色器
    7. // GPU会在点之间进行插值
    8. v_texCoord = a_texCoord;
    9. }

    然后用片段着色器寻找纹理上对应的颜色

    1. <script id="2d-fragment-shader" type="x-shader/x-fragment">
    2. precision mediump float;
    3. // 纹理
    4. uniform sampler2D u_image;
    5. // 从顶点着色器传入的纹理坐标
    6. varying vec2 v_texCoord;
    7. void main() {
    8. // 在纹理上寻找对应颜色值
    9. gl_FragColor = texture2D(u_image, v_texCoord);
    10. }
    11. </script>

    最后我们需要加载一个图像,创建一个纹理然后将图像复制到纹理中。 由于浏览器中的图片是异步加载的,所以我们需要重新组织一下代码, 等待纹理加载,一旦加载完成就开始绘制。

    1. function main() {
    2. var image = new Image();
    3. image.src = "http://someimage/on/our/server"; // 必须在同一域名下
    4. image.onload = function() {
    5. render(image);
    6. }
    7. }
    8. function render(image) {
    9. ...
    10. // 之前的代码
    11. ...
    12. // 找到纹理的地址
    13. var texCoordLocation = gl.getAttribLocation(program, "a_texCoord");
    14. // 给矩形提供纹理坐标
    15. var texCoordBuffer = gl.createBuffer();
    16. gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer);
    17. gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
    18. 0.0, 0.0,
    19. 1.0, 0.0,
    20. 0.0, 1.0,
    21. 0.0, 1.0,
    22. 1.0, 0.0,
    23. 1.0, 1.0]), gl.STATIC_DRAW);
    24. gl.enableVertexAttribArray(texCoordLocation);
    25. gl.vertexAttribPointer(texCoordLocation, 2, gl.FLOAT, false, 0, 0);
    26. // 创建纹理
    27. var texture = gl.createTexture();
    28. gl.bindTexture(gl.TEXTURE_2D, texture);
    29. // 设置参数,让我们可以绘制任何尺寸的图像
    30. gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
    31. gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
    32. gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
    33. gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
    34. // 将图像上传到纹理
    35. gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
    36. ...
    37. }

    这是 WenGL 绘制的图像。注意:如果你想在本地运行,需要使用一个简单的 web 服务, 使得 WebGL 可以加载本地图片,例如 http-server

    WebGL 理论基础 - 图像处理 上 - 图2

    CodePen 地址

    这个图片没什么特别的,让我们来对它进行一些操作。把红和蓝的值互换如何?

    1. gl_FragColor = texture2D(u_image, v_texCoord).bgra;

    现在红色和蓝色调换位置了。

    WebGL 理论基础 - 图像处理 上 - 图3

    CodePen 地址

    如果我们的图像处理需要其他像素的颜色值怎么办? 由于WebGL的纹理坐标范围是 0.0 到 1.0 , 那我们可以简单计算出移动一个像素对应的距离,onePixel = 1.0 / textureSize ,这个片段着色器将每个像素的值设置为与左右像素的均值。

    1. <script id="2d-fragment-shader" type="x-shader/x-fragment">
    2. precision mediump float;
    3. // 纹理
    4. uniform sampler2D u_image;
    5. uniform vec2 u_textureSize;
    6. // 从顶点着色器传入的像素坐标
    7. varying vec2 v_texCoord;
    8. void main() {
    9. // 计算1像素对应的纹理坐标
    10. vec2 onePixel = vec2(1.0, 1.0) / u_textureSize;
    11. // 对左中右像素求均值
    12. gl_FragColor = (
    13. texture2D(u_image, v_texCoord) +
    14. texture2D(u_image, v_texCoord + vec2(onePixel.x, 0.0)) +
    15. texture2D(u_image, v_texCoord + vec2(-onePixel.x, 0.0))) / 3.0;
    16. }
    17. </script>

    我们需要在 JavaScript 中传入纹理的大小。

    1. ...
    2. var textureSizeLocation = gl.getUniformLocation(program, "u_textureSize");
    3. ...
    4. // 设置图像的大小
    5. gl.uniform2f(textureSizeLocation, image.width, image.height);
    6. ...

    可以和上方没有模糊处理的图片对比一下。

    WebGL 理论基础 - 图像处理 上 - 图4

    CodePen 地址

    知道了怎么获取像素值,现在我们来做一些图片处理常用的卷积内核。在这个例子中我们将使用 3×3 的内核,卷积内核就是一个 3×3 的矩阵,矩阵中的每一项代表当前处理的像素和周围8个像素的乘法因子,相乘后将结果加起来除以内核权重(内核中所有值的和或 1.0 ,取二者中较大者),这有一个不错的相关文章这里是C++实现的一些具体代码

    我们将在片段着色器中计算卷积,所以创建一个新的片段着色器。

    1. <script id="2d-fragment-shader" type="x-shader/x-fragment">
    2. precision mediump float;
    3. // 纹理
    4. uniform sampler2D u_image;
    5. uniform vec2 u_textureSize;
    6. uniform float u_kernel[9];
    7. uniform float u_kernelWeight;
    8. // 从顶点着色器传入的纹理坐标
    9. varying vec2 v_texCoord;
    10. void main() {
    11. vec2 onePixel = vec2(1.0, 1.0) / u_textureSize;
    12. vec4 colorSum =
    13. texture2D(u_image, v_texCoord + onePixel * vec2(-1, -1)) * u_kernel[0] +
    14. texture2D(u_image, v_texCoord + onePixel * vec2( 0, -1)) * u_kernel[1] +
    15. texture2D(u_image, v_texCoord + onePixel * vec2( 1, -1)) * u_kernel[2] +
    16. texture2D(u_image, v_texCoord + onePixel * vec2(-1, 0)) * u_kernel[3] +
    17. texture2D(u_image, v_texCoord + onePixel * vec2( 0, 0)) * u_kernel[4] +
    18. texture2D(u_image, v_texCoord + onePixel * vec2( 1, 0)) * u_kernel[5] +
    19. texture2D(u_image, v_texCoord + onePixel * vec2(-1, 1)) * u_kernel[6] +
    20. texture2D(u_image, v_texCoord + onePixel * vec2( 0, 1)) * u_kernel[7] +
    21. texture2D(u_image, v_texCoord + onePixel * vec2( 1, 1)) * u_kernel[8] ;
    22. // 只把rgb值求和除以权重
    23. // 将阿尔法值设为 1.0
    24. gl_FragColor = vec4((colorSum / u_kernelWeight).rgb, 1.0);
    25. }
    26. </script>

    在 JavaScrip t中我们需要提供卷积内核和它的权重

    1. function computeKernelWeight(kernel) {
    2. var weight = kernel.reduce(function(prev, curr) {
    3. return prev + curr;
    4. });
    5. return weight <= 0 ? 1 : weight;
    6. }
    7. ...
    8. var kernelLocation = gl.getUniformLocation(program, "u_kernel[0]");
    9. var kernelWeightLocation = gl.getUniformLocation(program, "u_kernelWeight");
    10. ...
    11. var edgeDetectKernel = [
    12. -1, -1, -1,
    13. -1, 8, -1,
    14. -1, -1, -1
    15. ];
    16. gl.uniform1fv(kernelLocation, edgeDetectKernel);
    17. gl.uniform1f(kernelWeightLocation, computeKernelWeight(edgeDetectKernel));
    18. ...

    看!可以用下拉菜单选择不同的卷积内核。

    WebGL 理论基础 - 图像处理 上 - 图5