WebGL着色器和Chromakey绿幕抠图

2024年3月1日 ... ☕️ 4 min read

所用的一个基础库实现了一个绿幕抠图的功能,使用的是threejsShaderMaterial。看这个之前需要先了解什么是是shader。

shader中文名叫着色器,是WebGL的重要组件。它使用 GLSL 语言编写,运行在GPU上。所以,相比较运行在CPU上的js主线程,WebGL可以说是一片新天地。

着色器顾名思义,就是给素材着色的。它会给几何体的每个像素着色,改变渲染效果。

Threejs内置了非常多的材质,但是也可以自己创建着色器来满足特定需要。shaderMaterial就是干这个用的。

shaderMaterial对象包含两个属性:vertexShaderfragmentShader。他们对应两个用GLSL语言写好的着色器程序。

顶点着色器Vertex Shader Vertex Shader 用于定位几何体的顶点,它的工作原理是发送顶点位置、网格变换(position、旋rotation和 scale 等)、摄像机信息(position、rotation、fov 等)。GPU 将按照 Vertex Shader 中的指令处理这些信息,然后将顶点投影到 2D 空间中渲染成 Canvas。 当使用 Vertex Shader 时,它的代码将作用于几何体的每个顶点。在每个顶点之间,有些数据会发生变化,这类数据称为 attribute;有些数据在顶点之间永远不会变化,称这种数据为 uniform。Vertex Shader 会首先触发,当顶点被放置,GPU 知道几何体的哪些像素可见,然后执行 Fragment Shader。

attribute:使用顶点数组封装每个顶点的数据,一般用于每个顶点都各不相同的变量,如顶点的位置。 uniform:顶点着色器使用的常量数据,不能被修改,一般用于对同一组顶点组成的单个 3D 物体中所有顶点都相同的变量,如当前光源的位置。

片元着色器Fragment Shader Fragment Shader 在 Vertex Shader 之后执行,它的作用是为几何体的每个可见像素进行着色。我们可以通过uniforms 将数据发送给它,也可以将 Vertex Shader 中的数据发送给它,我们将这种从 Vertex Shader 发送到 Fragment Shader 的数据称为 varying。 Fragment Shader 中最直接的指令就是可以使用相同的颜色为所有像素进行着色。如果只设置了颜色属性,就相当于得到了与 MeshBasicMaterial 等价的材质。如果我们将光照的位置发送给 Fragment Shader,然后根据像素收到光照影响的多少来给像素上色,此时就能得到与 MeshPhongMaterial 效果等价的材质。

具体使用可以参考threejs官方介绍:ShaderMaterial

绿幕抠图的功能,就是编辑FragmentShader处理像素来实现的。

绿幕抠图原理

原文:WebGL Chromakey 实时绿幕抠图

  1. 传入四个参数
  2. 目标颜色,期望抠除背景色,可以不是绿色
  3. 相似度阈值
  4. 平滑度敏感系数
  5. 颜色饱和度敏感系数
  6. 使用 WebGL(片元着色器 (opens new window)) 逐个比对原像素与目标颜色
  7. 计算过程
  8. 将颜色转换到 UV 空间,计算出当前像素的与目标颜色的距离
  9. 将距离 - 相似度阈值,小于 0 则判定为绿幕,将像素点设置为全透明(alpha=0)
  10. 与平滑度参数计算,将相似度转换成 alpha 通道值,越大越不透明
  11. 计算出愿像素点的灰度值
  12. 将相似度与饱和度参数计算,然后与原像素点的灰度值混合,越大越靠近原像素点,越小越就接近灰度 (后两步为了移除前景边缘与绿幕反光,导致的前景像素点混合了绿幕背景颜色)

Shader 代码

#version 300 es
precision mediump float;
out vec4 FragColor;
in vec2 v_texCoord;

uniform sampler2D frameTexture;
uniform vec3 keyColor;

// 色度的相似度阈值
uniform float similarity;
// 透明度的平滑度计算
uniform float smoothness;
// 降低绿幕饱和度,提高抠图准确度
uniform float spill;

vec2 RGBtoUV(vec3 rgb) {
  return vec2(
    rgb.r * -0.169 + rgb.g * -0.331 + rgb.b *  0.5    + 0.5,
    rgb.r *  0.5   + rgb.g * -0.419 + rgb.b * -0.081  + 0.5
  );
}

void main() {
  // 获取当前像素的rgba值
  vec4 rgba = texture(frameTexture, v_texCoord);
  // 计算当前像素与绿幕像素的色度差值
  vec2 chromaVec = RGBtoUV(rgba.rgb) - RGBtoUV(keyColor);
  // 计算当前像素与绿幕像素的色度距离(向量长度), 越相似则色度距离越小
  float chromaDist = sqrt(dot(chromaVec, chromaVec));
  // 设置了一个相似度阈值,baseMask < 0,则像素是绿幕,> 0 则像素点可能属于前景(比如人物)
  float baseMask = chromaDist - similarity;
  // 与平滑度参数计算,将 baseMask 转换成 alpha 通道值,越大越不透明
  float fullMask = pow(clamp(baseMask / smoothness, 0., 1.), 1.5);
  rgba.a = fullMask;
  // 如果 baseMask < 0,spillVal 等于 0;baseMask 越小,像素点饱和度越低
  float spillVal = pow(clamp(baseMask / spill, 0., 1.), 1.5);
  // 计算当前像素的灰度值
  float desat = clamp(rgba.r * 0.2126 + rgba.g * 0.7152 + rgba.b * 0.0722, 0., 1.);
  rgba.rgb = mix(vec3(desat, desat, desat), rgba.rgb, spillVal);
  FragColor = rgba;
}

关于GLSL的语法介绍:Learn WebGL

GLSL几个注意事项:

  • 不支持动态传入的数组,即数组长度必须是常量;
  • 支持for/while/do-while循环;
  • 类型需要定义明确,且和js类型不同:int/float/vec2/vec3/vec4。

#webgl

SideEffect is a blog for front-end web development.
Code by Axiu / rss