张春成
2023/01/09阅读:44主题:默认主题
WebGL的实时渲染
WebGL的实时渲染
本文提供一个前端样例,用于实时捕捉流数据并进行计算和渲染。
开源代码可见我的前端笔记本
Image Cookbook[1]
图像滤波器
图像滤波器可以理解成依次应用于图像中每一个像素及其临域的算数计算,目前非常火热的卷积计算就是其中的典型案例
其中,卷积运算可以理解成临域内的线性加权求和,用于提取特定的有意义特征。
另外,还有更复杂的非线性运算可以用于提取更加复杂的特征,比如边缘滤波器SNN等。它的非线性体现在对称像素之间的比较和选择。
The Symmetric Nearest Neighbor Filter (SNN) is a non-linear edge preserving image processing filter. It is a very effective noise reduction technique that removes noise while maintaining sharp image edges. This graphic filter produce results very close to these of Median[2] and Kuwahara filters.
Symmetric Nearest Neighbor Filter - FIVEKO[3]
与卷积运算相比,这些非线性滤波器往往需要多次计算和比较,虽然效果显著,但计算开销巨大。

rawImage-plant

SNN-plant

rawImage-city

SNN-city

rawImage-flower

Outline-flower
实时流获取与渲染
因此,本文借助 WebGL 的 shader 渲染器,通过 GPU 或 CPU 中的图形处理单元进行计算,极大地提升了滤波器的计算效率。其中,渲染器的滤波计算采用如下代码。
/**
* Shader of SNN or LNN
**/
precision mediump float;
// our texture
uniform sampler2D u_image;
uniform float u_size;
// DIRECT picks > or < for selecting the Largest or the Smallest neighbor
#define KERNEL_SIZE ${KERNEL_SIZE}
#define HALF_SIZE (KERNEL_SIZE / 2)
#define DIRECT ${SelectSNNDirect}
void main() {
float u_pixelsCountRev = 1.0 / float(HALF_SIZE * KERNEL_SIZE);
vec2 textCoord = vec2(gl_FragCoord.x / u_size, 1.0 - gl_FragCoord.y / u_size);
vec2 onePixel = vec2(1.0, 1.0) / u_size;
vec4 meanColor = vec4(0);
vec4 v = texture2D(u_image, textCoord);
int count = 0;
for (int y = 0; y <= HALF_SIZE; y++) {
for (int x = -HALF_SIZE; x <= HALF_SIZE; x++) {
vec4 v1 = texture2D(u_image, textCoord + vec2(x, y) * onePixel);
vec4 v2 = texture2D(u_image, textCoord + vec2(-x, -y) * onePixel);
vec4 d1 = abs(v - v1);
vec4 d2 = abs(v - v2);
float dd1 = dot(d1, d1);
float dd2 = dot(d2, d2);
vec4 rv = (dd1 DIRECT dd2) ? v1 : v2;
meanColor += rv;
}
}
gl_FragColor = meanColor * u_pixelsCountRev;
}
图像流获取和渲染
为了进行前端适配,使用字节流的方式将图像接入到渲染器中,核心代码如下。
/**
* Require the rawImage from internet
* https://source.unsplash.com/random/600x600/?city
* Bind rawImage into WebGL
**/
const rawImage = {
return await new Promise((resolve, reject) => {
const image = new Image();
image.crossOrigin = "anonymous";
image.onerror = reject;
image.onload = () => {
resolve(image);
};
image.src = url;
});
}
{
const u_size = gl.getUniformLocation(program, "u_size");
const a_vertex = gl.getAttribLocation(program, "a_vertex");
gl.useProgram(program);
gl.enableVertexAttribArray(a_vertex);
gl.vertexAttribPointer(a_vertex, 2, gl.FLOAT, false, 0, 0);
gl.uniform1f(u_size, Math.max(viewof gl.width, viewof gl.height));
// while (true)
function render() {
gl.texImage2D(
gl.TEXTURE_2D,
0,
gl.RGBA,
gl.RGBA,
gl.UNSIGNED_BYTE,
rawImage
);
gl.drawArrays(gl.TRIANGLE_FAN, 0, 4);
// yield;
}
render();
}
视频流获取和渲染
另外,视频流同样可以读入渲染器中,通过循环获取和计算实现视频的实时渲染
/**
* Read video stream from your device
* and bind it with the WebGL shader
**/
// Read vidso stream
const video = {
const video = document
.createElement("div")
.appendChild(html`<video autoplay playsinline>`);
const stream = await navigator.mediaDevices.getUserMedia({
video: {
width: { ideal: width },
height: { ideal: height },
frame: { ideal: 5 }
},
audio: false
});
yield video;
video.srcObject = stream;
video.play();
invalidation.then(() => stream.getTracks().forEach((t) => t.stop()));
}
// Bind the stream with the WebGL shader
{
const u_size = gl.getUniformLocation(program, "u_size");
const a_vertex = gl.getAttribLocation(program, "a_vertex");
gl.useProgram(program);
gl.enableVertexAttribArray(a_vertex);
gl.vertexAttribPointer(a_vertex, 2, gl.FLOAT, false, 0, 0);
gl.uniform1f(u_size, Math.max(viewof gl.width, viewof gl.height));
while (true) {
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, video);
gl.drawArrays(gl.TRIANGLE_FAN, 0, 4);
yield;
}
}
参考资料
Image Cookbook: https://observablehq.com/@listenzcc/image-cookbook
[2]Median: https://fiveko.com/median-filter/
[3]Symmetric Nearest Neighbor Filter - FIVEKO: https://fiveko.com/symmetric-nearest-neighbor-filter/
作者介绍