说明
【跟月影学可视化】学习笔记。
边缘模糊
在遍历像素点的时候,同时计算当前像素点到图片中心点的距离,然后根据距离设置透明度,就可以实现边缘模糊的效果。
<!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>
合成效果如下