说明
【跟月影学可视化】学习笔记。
边缘模糊
在遍历像素点的时候,同时计算当前像素点到图片中心点的距离,然后根据距离设置透明度,就可以实现边缘模糊的效果。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>图片边缘模糊的效果</title> </head> <body> <img src="https://images.unsplash.com/photo-1666552982368-dd0e2bb96993?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2070&q=80" alt="" /> <canvas id="paper" width="0" height="0"></canvas> <script type="module"> import { loadImage, getImageData, traverse, } from "./common/lib/util.js"; const canvas = document.getElementById("paper"); const context = canvas.getContext("2d"); (async function () { // 异步加载图片 const img = await loadImage( "https://images.unsplash.com/photo-1666552982368-dd0e2bb96993?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2070&q=80" ); // 获取图片的 imageData 数据对象 const imageData = getImageData(img); console.log("imageData---->", imageData); // 遍历 imageData 数据对象:traverse 函数会自动遍历图片的每个像素点,把获得的像素信息传给参数中的回调函数处理 traverse(imageData, ({r, g, b, a, x, y}) => { const d = Math.hypot((x - 0.5), (y - 0.5)); a *= 1.0 - 2 * d; return [r, g, b, a]; }); // 更新canvas内容 canvas.width = imageData.width; canvas.height = imageData.height; // 将数据从已有的 ImageData 对象绘制到位图 context.putImageData(imageData, 0, 0); })(); </script> </body> </html>
图片融合
给一张照片加上阳光照耀的效果。具体操作就是,把下面的透明的图片叠加到一张照片上。这种能叠加到其他照片上的图片,通常被称为纹理(Texture
),叠加后的效果也叫做纹理效果。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>纹理与图片叠加</title> </head> <body> <img src="./assets/img/flower.jpg" alt="" /> <canvas id="paper" width="0" height="0"></canvas> <script type="module"> import { loadImage, getImageData, traverse, getPixel, } from "./common/lib/util.js"; import { transformColor, brightness, saturate, } from "./common/lib/color-matrix.js"; const canvas = document.getElementById("paper"); const context = canvas.getContext("2d"); (async function () { // 异步加载图片 const img = await loadImage( "./assets/img/flower.jpg" ); // 阳光效果图 const sunlight = await loadImage( "./assets/img/sunlight-texture.png" ); // 获取图片的 imageData 数据对象 const imageData = getImageData(img); console.log("imageData---->", imageData); const texture = getImageData(sunlight); console.log("texture---->", texture); // 遍历 imageData 数据对象:traverse 函数会自动遍历图片的每个像素点,把获得的像素信息传给参数中的回调函数处理 traverse(imageData, ({ r, g, b, a, index }) => { const texColor = getPixel(texture, index); return transformColor( [r, g, b, a], brightness(1 + 0.7 * texColor[3]), saturate(2 - texColor[3]) ); }); // 更新canvas内容 canvas.width = imageData.width; canvas.height = imageData.height; // 将数据从已有的 ImageData 对象绘制到位图 context.putImageData(imageData, 0, 0); })(); </script> </body> </html>
弊端:必须循环遍历图片上的每个像素点,图片一大计算量很大。
片元着色器是怎么处理像素的?
在 WebGL 中,先通过图片或者 Canvas 对象来创建纹理对象,纹理对象包括了整张图片的所有像素点的颜色信息,然后通过 uniform 传递给着色器,再通过纹理坐标 vUv 来读取对应的具体坐标处像素的颜色信息。
纹理坐标是一个变量,类型是二维向量,x、y 的值从 0 到 1。
webgl 实现滤镜
创建纹理对象
function createTexture(gl, img) { // 创建纹理对象 const texture = gl.createTexture(); // 设置预处理函数,由于图片坐标系和WebGL坐标的Y轴是反的,这个设置可以将图片Y坐标翻转一下 gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true); // 激活指定纹理单元,WebGL有多个纹理单元,因此在Shader中可以使用多个纹理 gl.activeTexture(gl.TEXTURE0); // 将纹理绑定到当前上下文 gl.bindTexture(gl.TEXTURE_2D, texture); // 指定纹理图像 gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, img); // 设置纹理的一些参数 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.LINEAR); // 解除纹理绑定 gl.bindTexture(gl.TEXTURE_2D, null); return texture; }
设置纹理
function setTexture(gl, idx) { // 激活纹理单元 gl.activeTexture(gl.TEXTURE0 + idx); // 绑定纹理 gl.bindTexture(gl.TEXTURE_2D, texture); // 获取shader中纹理变量 const loc = gl.getUniformLocation(program, 'tMap'); // 将对应的纹理单元写入shader变量 gl.uniform1i(loc, idx); // 解除纹理绑定 gl.bindTexture(gl.TEXTURE_2D, null); }
在 Shader 中使用纹理对象
uniform sampler2D tMap; ... // 从纹理中提取颜色,vUv是纹理坐标 vec3 color = texture2D(tMap, vUv);
这里直接使用 gl-renderer
库
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>webgl 实现滤镜</title> </head> <body> <canvas width="1920" height="1080"></canvas> <script src="./common/lib/gl-renderer.js"></script> <script> const vertex = ` attribute vec2 a_vertexPosition; attribute vec2 uv; varying vec2 vUv; void main() { gl_PointSize = 1.0; vUv = uv; gl_Position = vec4(a_vertexPosition, 1, 1); } `; const fragment = ` #ifdef GL_ES precision highp float; #endif uniform sampler2D tMap; uniform mat4 colorMatrix; varying vec2 vUv; void main() { vec4 color = texture2D(tMap, vUv); gl_FragColor = colorMatrix * vec4(color.rgb, 1.0); gl_FragColor.a = color.a; } `; const canvas = document.querySelector('canvas'); const renderer = new GlRenderer(canvas); // load fragment shader and createProgram const program = renderer.compileSync(fragment, vertex); renderer.useProgram(program); (async function () { const texture = await renderer.loadTexture('./assets/img/flower.jpg'); renderer.uniforms.tMap = texture; const r = 0.2126, g = 0.7152, b = 0.0722; renderer.uniforms.colorMatrix = [ r, r, r, 0, g, g, g, 0, b, b, b, 0, 0, 0, 0, 1, ]; renderer.setMeshData([{ positions: [ [-1, -1], [-1, 1], [1, 1], [1, -1], ], attributes: { uv: [ [0, 0], [0, 1], [1, 1], [1, 0], ], }, cells: [[0, 1, 2], [2, 0, 3]], }]); renderer.render(); }()); </script> </script> </body> </html>
webgl 实现图片的粒子化
将图形网格化,因为原始图像的图片像素宽高是 1920px 和 1080px
,所以我们用 vec2 st = vUv * vec2(192, 108)
就可以得到 10px X 10px
大小的网格。
为了取出来的颜色是一个乱序的色值。可以使用伪随机函数 random 根据网格随机一个偏移量,因为这个偏移量是 0~1
之间的值,我们将它乘以 2 再用 1 减去它,就能得到一个范围在 -1~1
之间的随机偏移。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>webgl 实现图片的粒子化</title> </head> <body> <canvas width="1920" height="1080"></canvas> <script src="./common/lib/gl-renderer.js"></script> <script> const vertex = ` attribute vec2 a_vertexPosition; attribute vec2 uv; varying vec2 vUv; void main() { gl_PointSize = 1.0; vUv = uv; gl_Position = vec4(a_vertexPosition, 1, 1); } `; const fragment = ` #ifdef GL_ES precision highp float; #endif uniform sampler2D tMap; uniform float uTime; varying vec2 vUv; float random (vec2 st) { return fract(sin(dot(st.xy, vec2(12.9898,78.233)))*43758.5453123); } void main() { vec2 st = vUv * vec2(192, 108); vec2 uv = vUv + 1.0 - 2.0 * random(floor(st)); vec4 color = texture2D(tMap, mix(uv, vUv, min(uTime, 1.0))); gl_FragColor.rgb = color.rgb; gl_FragColor.a = color.a * uTime; } `; const canvas = document.querySelector('canvas'); const renderer = new GlRenderer(canvas); // load fragment shader and createProgram const program = renderer.compileSync(fragment, vertex); renderer.useProgram(program); (async function () { const texture = await renderer.loadTexture('./assets/img/flower.jpg'); renderer.uniforms.tMap = texture; renderer.uniforms.uTime = 0; renderer.setMeshData([{ positions: [ [-1, -1], [-1, 1], [1, 1], [1, -1], ], attributes: { uv: [ [0, 0], [0, 1], [1, 1], [1, 0], ], }, cells: [[0, 1, 2], [2, 0, 3]], }]); renderer.render(); function update(t) { renderer.uniforms.uTime = t / 5000; requestAnimationFrame(update); } update(0); }()); </script> </script> </body> </html>
实现效果如下:
webgl 实现图像合成
使用 shader 技术可以把绿幕图片合成到其他图片上
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>webgl 实现图像合成</title> </head> <body> <canvas width="1920" height="1080"></canvas> <script src="./common/lib/gl-renderer.js"></script> <script> const vertex = ` attribute vec2 a_vertexPosition; attribute vec2 uv; varying vec2 vUv; void main() { gl_PointSize = 1.0; vUv = uv; gl_Position = vec4(a_vertexPosition, 1, 1); } `; const fragment = ` #ifdef GL_ES precision highp float; #endif uniform sampler2D tMap; uniform sampler2D tCat; varying vec2 vUv; void main() { vec4 color = texture2D(tMap, vUv); vec2 st = vUv * 3.0 - vec2(1.2, 0.5); vec4 cat = texture2D(tCat, st); gl_FragColor.rgb = cat.rgb; if(cat.r < 0.5 && cat.g > 0.6) { gl_FragColor.rgb = color.rgb; } gl_FragColor.a = color.a; } `; const canvas = document.querySelector('canvas'); const renderer = new GlRenderer(canvas); // load fragment shader and createProgram const program = renderer.compileSync(fragment, vertex); renderer.useProgram(program); (async function () { const [picture, cat] = await Promise.all([ renderer.loadTexture('./assets/img/flower.jpg'), renderer.loadTexture('./assets/img/cat.png'), ]); renderer.uniforms.tMap = picture; renderer.uniforms.tCat = cat; renderer.setMeshData([{ positions: [ [-1, -1], [-1, 1], [1, 1], [1, -1], ], attributes: { uv: [ [0, 0], [0, 1], [1, 1], [1, 0], ], }, cells: [[0, 1, 2], [2, 0, 3]], }]); renderer.render(); }()); </script> </script> </body> </html>
合成效果如下