# 图像处理

GLSL

  • sampler2D

在WebGL中绘制图片需要使用纹理。和WebGL渲染时需要裁剪空间坐标相似, 渲染纹理时需要纹理坐标,而不是像素坐标。

无论纹理是什么尺寸,纹理坐标范围始终是 0.0 到 1.0 。

因为我们只用画一个矩形(其实是两个三角形),所以需要告诉WebGL矩形中每个顶点对应的纹理坐标。

我们将使用一种特殊的叫做'varying'的变量将纹理坐标从顶点着色器传到片断着色器,它叫做“可变量” 是因为它的值有很多个,WebGL会用顶点着色器中值的进行插值,然后传给对应像素执行的片断着色器。

# 编写着色器

attribute vec2 a_position;
attribute vec2 a_texCoord;

uniform vec2 u_resolution;

varying vec2 v_texCoord;

void main() {
   // convert the rectangle from pixels to 0.0 to 1.0
   vec2 zeroToOne = a_position / u_resolution;

   // convert from 0->1 to 0->2
   vec2 zeroToTwo = zeroToOne * 2.0;

   // convert from 0->2 to -1->+1 (clipspace)
   vec2 clipSpace = zeroToTwo - 1.0;

   gl_Position = vec4(clipSpace * vec2(1, -1), 0, 1);

   // pass the texCoord to the fragment shader
   // The GPU will interpolate this value between points.
   v_texCoord = a_texCoord;
}

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

precision mediump float;

// our texture
uniform sampler2D u_image;

// the texCoords passed in from the vertex shader.
varying vec2 v_texCoord;

void main() {
   gl_FragColor = texture2D(u_image, v_texCoord);
}

# 加载图像

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

function main() {
  const image = new Image();
  image.src = "http://someimage/on/our/server";  // 注意跨域问题
  image.onload = function() {
    render(image);
  }
}

# 加载纹理

function setRectangle(gl, x, y, width, height) {
  var x1 = x;
  var x2 = x + width;
  var y1 = y;
  var y2 = y + height;
  gl.bufferData(
    gl.ARRAY_BUFFER,
    new Float32Array([x1, y1, x2, y1, x1, y2, x1, y2, x2, y1, x2, y2]),
    gl.STATIC_DRAW
  );
}
// ...
// 顶点
const positionLocation = gl.getAttribLocation(program, "a_position");
// 纹理
const texcoordLocation = gl.getAttribLocation(program, "a_texCoord");
// 分辨率
const resolutionLocation = gl.getUniformLocation(program, "u_resolution");

// 顶点缓冲区
const positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); // 1
setRectangle(gl, 0, 0, image.width, image.height);
// 纹理坐标缓冲区
const 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
);

// 创建纹理
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
);
// ...
// ...
// enable texcoord
gl.enableVertexAttribArray(texcoordLocation);
// bind the texcoord buffer.
gl.bindBuffer(gl.ARRAY_BUFFER, texcoordBuffer);
// set texcoord
gl.vertexAttribPointer(texcoordLocation, 2, gl.FLOAT, false, 0, 0);
// set the resolution
gl.uniform2f(resolutionLocation, gl.canvas.width, gl.canvas.height);
// ...

# 渲染结果1

如图

# 渲染结果2

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

gl_FragColor = texture2D(u_image, v_texCoord).bgra;

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

# 渲染结果3

如果我们的图像处理需要其他像素的颜色值怎么办? 由于WebGL的纹理坐标范围是 0.0 到 1.0 , 那我们可以简单计算出移动一个像素对应的距离, onePixel = 1.0 / textureSize。

这个片断着色器将每个像素的值设置为与左右像素的均值。

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;
}
const textureSizeLocation = gl.getUniformLocation(program, "u_textureSize");
// ...
// use program 
// ...
// 设置图像的大小
gl.uniform2f(textureSizeLocation, image.width, image.height);

现在横向变模糊了

# 渲染结果4

知道了怎么获取像素值,现在我们来做一些图片处理常用的卷积内核。

我们将在片断着色器中计算卷积,所以使用一个新的片断着色器代码。

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值求和除以权重
   // 将阿尔法值设为 1.0
   gl_FragColor = vec4((colorSum / u_kernelWeight).rgb, 1.0);
}
function computeKernelWeight(kernel) {
  const weight = kernel.reduce(function (prev, curr) {
    return prev + curr;
  });
  return weight <= 0 ? 1 : weight;
}
// ...
const kernelLocation = gl.getUniformLocation(program, "u_kernel[0]");
const kernelWeightLocation = gl.getUniformLocation(program, "u_kernelWeight");
// ...
// use program
// ...
const edgeDetectKernel = [
    -1, -1, -1,
    -1,  8, -1,
    -1, -1, -1
];
gl.uniform1fv(kernelLocation, edgeDetectKernel);
gl.uniform1f(kernelWeightLocation, computeKernelWeight(edgeDetectKernel));
// ...
// draw

现在边缘变锐利了