已经过去的2020是一个不怎么顺遂的一年,出入公共场所都需要体温监测,而人流量密集的商场,一般会采用热成像技术来快速测量体温。那么今天我们就来说说如何让一张普通图片变成具有热成像的效果。
如果你对shader相关的技术感兴趣也可以阅读以下文章:
本期代码使用javascript编写,涉及一些webgl,glsl相关知识。从本文中你可以了解到:
- 如何colorRamp实现gameboy效果
- 如何对图片进行模糊处理
- 如何实现简单的高亮效果
- 如何结合以上技术实现热成像效果
colorRamp
这一小节我们介绍colorRamp的原理,并仅仅使用colorRamp实现将图片的GameBoy风格。
最终的成品:
什么ColorRamp
简单来说colorRamp就一个颜色条,他可以是渐变的,也可以就是固定的几个颜色。类似ps填充颜色时使用的那个。
我们可以使用代码生成这种图片,但通常也会使用固定的图片素材。这里我们以图片素材为例生成一个colorRamp:
这样就可以在片段着色器中使用这张图片了:
color = texture2D(u_img, uv);
但需要注意的时如何按照这样的代码渲染colorRamp会得到下面的结果
固定颜色的图片,变成渐变的
这是因为我们在调用gl.texParameteri函数是第三个参数传入gl.LINEAR,如果传入gl.NEAREST,则不会进行插值。
var val ;
if(condition){
val = gl.LINEAR;
}else{
val = gl.NEAREST
}
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, val);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, val);
colorRamp只是我们的配色方案,不会直接将它绘制在canvas上。有了配色方案后我们还需要告诉shader什么位置使用何种配色。例如在场景一个类Minecraft的游戏中我们需要一些"土块".
这时我们的shader可以是这样的
blender中图形化的shader
它可以根据z轴的方向来决定是涂上绿色还是涂上土色。更进一步如果我们将colorRamp作为参数,在不改变shader的逻辑的情况下,就可以做出多种材质,实现shader的复用。
对于更复杂的几何体我们还可以结合法线,mix函数制作。这里就不深入展开。
Gameboy风格
要如何实现上述的gameBoy风格shader?代码如下
uniform float u_colorRampLuminosity;
uniform sampler2D u_img0;
uniform sampler2D u_img1;
float saturate(float x){
return clamp(x,0.,1.);
}
void main() {
vec4 color = texture2D(u_img0, uv);
float luminance = 0.;
luminance = dot(vec3(.3,.6,.1) , color.rgb);
luminance = saturate(luminance + u_colorRampLuminosity);
color.rgb = texture2D(u_img1, vec2(luminance, .0)).rgb;
gl_FragColor = color;
}
这里我们我们向shader中传入三个变量分别是我们的原图纹理u_img0,colorRamp纹理u_img1,以及一个控制参数u_colorRampLuminosity。
每一个像素都有其对应的color,每一个color根据一定的规则得到一个介于[0,1)之间的值luminance。最后我们在告诉shader,这个像素点需要用colorRamp中的哪一个颜色上色。由于colorRamp是一个一维分布的色条所以y分量为0。
dot(vec3(.3,.6,.1), color.rgb)这一步骤,其实就是计算rgb三个通道的贡献,对应的分量大该通道的贡献就大。
RGB取值对结果影响
而控制参数u_colorRampLuminosity,则可以理解为偏移量,就是在目前的基础上向colorRamp的左侧/右侧做偏移:
不同u_colorRampLuminosity对结果的影响
Blur
在这篇文章中我们曾经说到过一种实现模糊的卷积核这是上帝的杰作:《前端图形学从入门到放弃》2.5 画皮:纹理贴图。其原理就是对每一个像素取该像素和其周围像素点颜色的平均值来实现。
vec4 Blur(vec2 uv, sampler2D source, float Intensity)
{
float step = 0.00390625 * Intensity;
vec4 result = vec4 (0, 0, 0, 0);
vec2 texCoord = vec2(0, 0);
texCoord = uv + vec2(-step, -step);
result += texture2D(source, texCoord);
texCoord = uv + vec2(-step, 0);
result += 2.0 * texture2D(source, texCoord);
texCoord = uv + vec2(-step, step);
result += texture2D(source, texCoord);
texCoord = uv + vec2(0, -step);
result += 2.0 * texture2D(source, texCoord);
texCoord = uv;
result += 4.0 * texture2D(source, texCoord);
texCoord = uv + vec2(0, step);
result += 2.0 * texture2D(source, texCoord);
texCoord = uv + vec2(step, -step);
result += texture2D(source, texCoord);
texCoord = uv + vec2(step, 0);
result += 2.0* texture2D(source, texCoord);
texCoord = uv + vec2(step, -step);
result += texture2D(source, texCoord);
result = result * 0.0625;
return result;
}
上面的代码实现了一个简易的模糊函数,对图片(sampler2D source)模糊处理。用来平均的是上下左右以及斜向45度的8个点。其中原始点的权重最大为4,正上下左右的权重为2,剩余点为1。再通过第三个参数Intensity控制取点的距离。Intensity越大选取的点离原始坐标越远。
最终效果
但这种方法有一个缺点,就是当Intensity取值过大时,不可避免的会产生重影。这是因为虽然我们为每个点加上了权重,但变化依旧是线性,而我们需要的模糊效果因该是随着距离的增大影响递减的。
Intensity等于14时,重影相当严重
为此我们需要改良代码
float BlurHD_G(float bhqp, float x)
{
return exp(-(x * x) / (2.0 * bhqp * bhqp));
}
vec4 BlurHD(vec2 uv, sampler2D source, float Intensity){
const int iterations = 16;
int halfIterations = iterations / 2;
float sigmaX = 0.1 + Intensity * 0.5;
float sigmaY = sigmaX;
float total = 0.0;
vec4 ret = vec4(0., 0., 0., 0.);
for (int iy = 0; iy < iterations; ++iy)
{
float fy = BlurHD_G(sigmaY, float(iy - halfIterations));
float offsety = float(iy - halfIterations) * 0.00390625;
for (int ix = 0; ix < iterations; ++ix)
{
float fx = BlurHD_G(sigmaX, float(ix - halfIterations));
float offsetx = float(ix - halfIterations) * 0.00390625;
total += fx * fy;
vec4 a = texture2D(source, uv + vec2(offsetx, offsety));
a.rgb *=a.a;
ret += a * fx * fy;
}
}
代码改良方向:
- 将取值点由33增加到了1616;
- 采用递减的指数函数:exp(-(x x)/(2.0 bhqp * bhqp));
- 控制了偏移的范围(float(iy - halfIterations)*0.00390625);
Intensity等于14时,模糊很严重但未重影
e^(-xx/a) [红色a=4,杨红a=2,蓝色a=1]*
Glow
最后我们来说下发光,这个shader实现很简单:
vec4 emission = color;
float low = .5;
vec3 glowColor = vec3(1.,1.,1.);
emission.rgb *= glow * glowColor;
color.rgb += emission.rgb;
这里最终的颜色等于基础色(color)加上发光(emission),emission是强度glow与颜色glowColor的乘积。
左边原始图片,右边发光强度0.5,发光色白色
当然也可以改变发光色:
热成像
看到这里读者肯定都着急了,说好的热成像?别急下面就是见证奇迹的时刻:
void main() {
vec4 color = vec4(1.,1.,1.,1.);
// 模糊
color = BlurHD(fragColor, u_image0, u_blurIntensity);
// 负片
color.rgb = 1.0 - color.rgb;
// colorRamp
float luminance = 0.;
luminance = dot(vec3(u_r,u_g,u_b) , color.rgb);
luminance = saturate(luminance + u_colorRampLuminosity);
color.rgb = texture2D(u_image1, vec2(luminance, .0)).rgb;
// 发光
vec4 emission = color;
float glow = 0.5;
vec3 glowColor = vec3(1.,1.,.1);
emission.rgb *= glow * glowColor;
color.rgb += emission.rgb;
color.rgb *= color.a;
gl_FragColor = color;
}
这里有一个效果color.rgb =1.0- color.rgb;因为太过简单,上面没有单独拿出来说,相当于做了一个负片的效果。shader连续使用了模糊 => 负片 => colorRamp => 发光
这里u_r,u_g,u_b是外界传入的值,例如r=1,g,b = 0.0;就是仅由红通道来决定colorRamp,有点类似blender 中的seperateRGB的意思。
float saturate(float x){
return clamp(x,0.,1.);
}
最终效果
当然这个shader也可用在真人身上,比如封面图。其中Intensity越大风格越卡通,大家可以把他们抽离出来当初参数从外界传入。自己体会一下。
最近很常用的一张表情包**
下期视频我们来聊聊一些赛博朋克风格的shader吧!