Three.js后处理 提取亮色

338 阅读1分钟

Three.js后处理 提取亮色

image.png

在某些需求下,需要将一个场景中比较“亮”的颜色提取出来。那么,我们需要知道怎么怎么计算亮度。

自定义一个提取亮色的Shader

1. 计算颜色的亮度

在前端开发中,通常使用RGB来表示颜色,每个通道的值越大,表示该颜色所占的比重越多,同时,亮度也就越高。所以,很容易想到的就是使用三个通道的平均数来表示亮度,即luma=(R+G+B)3luma = \frac{(R + G + B)}{3}

但是对于人眼的感知来说,三种颜色“贡献”的亮度并不相同,所以,基于人眼感知的计算方式就是一个加权平均数luma=0.299×R+0.587×G+0.114×B=CRGB(0.299,0.587,0.114)luma = 0.299 \times R + 0.587 \times G + 0.114 \times B = \overrightarrow{C_{RGB}} \cdot \overrightarrow{(0.299, 0.587, 0.114)}

在这里,我们采用第二种加权平均数的算法,亮度的范围为[0,1][0, 1]

// texel为采样后的颜色信息
float luma = dot(texel.xyz, vec3(0.299, 0.587, 0.114));

2. 提取高亮度的颜色

有了第一步计算好的亮度之后,我们就可以在片段着色器中对颜色进行过滤。代码也相当简单

float luma = dot(texel.xyz, vec3(0.299, 0.587, 0.114));
if(luma >= threshold) {
  // 亮度大于等于阈值
  gl_FragColor = texel;
} else {
  gl_FragColor = vec4(0, 0, 0, 0) // 隐藏低于阈值的颜色
}

3. 完整Shader

const shader = {
    uniforms: {
      'tDiffuse': { value: null }, // 被采样的texture
      'luminosityThreshold': { value: 0.1 } // 亮度阈值
    },
    vertexShader: /* glsl */`

      varying vec2 vUv;

      void main() {

        vUv = uv;

        gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );

      }`,

    fragmentShader: /* glsl */`

      uniform sampler2D tDiffuse; // 被采样的texture
      uniform float luminosityThreshold; // 高于阈值的亮度才会提取

      varying vec2 vUv;

      void main() {

        // 采样
        vec4 texel = texture2D( tDiffuse, vUv );

        // 计算当前采样点的亮度
        vec3 lumaVec = vec3( 0.299, 0.587, 0.114 );
        float luma = dot( texel.xyz, lumaVec ); // R * 0.299 + G * 0.587 + B * 0.114

        if(luma >= luminosityThreshold) {
          // 亮度大于等于阈值
          gl_FragColor = texel;
        } else {
          gl_FragColor = vec4(0, 0, 0, 0);
        }
      }`
  }

Three.js中的LuminosityHighPassShader

在Three.js库中,提供了一个亮色提取的shader,即LuminosityHighPassShader(examples/jsm/shaders/LuminosityHighPassShader.js)。主要的代码就是片段着色器:

uniform sampler2D tDiffuse; // 要采样的texture

// 亮度低于阈值的颜色 使用vec4(defaultColor, defaultOpacity)替换
uniform vec3 defaultColor;
uniform float defaultOpacity;


uniform float luminosityThreshold; // 亮度阈值
uniform float smoothWidth; // 平滑宽度

varying vec2 vUv;

void main() {

  vec4 texel = texture2D( tDiffuse, vUv ); // 采样

  vec3 luma = vec3( 0.299, 0.587, 0.114 );

  float v = dot( texel.xyz, luma ); // 计算亮度

  vec4 outputColor = vec4( defaultColor.rgb, defaultOpacity ); // 低于阈值时输出的颜色


  // 以下两句为核心代码
  float alpha = smoothstep( luminosityThreshold, luminosityThreshold + smoothWidth, v );

  gl_FragColor = mix( outputColor, texel, alpha );
}

smoothstep(lowerEdge, upperEdge, x)

这个函数是一个平滑的插值函数:

smoothstep(lowerEdge,upperEdge,x)={0x<lowerEdge[0,1]之间平滑插值x>lowerEdgex<upperEdge1x>upperEdgesmoothstep(lowerEdge, upperEdge, x)=\begin{cases} 0 & x < lowerEdge \\ [0, 1]之间平滑插值 & x > lowerEdge 且 x < upperEdge \\ 1 & x > upperEdge \end{cases}

先来理一下思路,这个shader和我们之前编写的有一些不同:我们采用的方式是“一刀切”,我们的阈值是一个点;但是LuminosityHighPassShader并不是,它的阈值是一个范围。

  float alpha = smoothstep( luminosityThreshold, luminosityThreshold + smoothWidth, v );
  1. 当明亮度v小于阈值时,alpha的值为0
  2. 当明亮度v大于luminosityThreshold + smoothWidth时,alpha的值为1
  3. 当明亮度v在范围[luminosityThreshold,luminosityThreshold+smoothWidth][luminosityThreshold, luminosityThreshold + smoothWidth]内,alpha的值为[0,1][0, 1]之间的插值

mix(x, y, a)

这个函数是一个线性插值函数:mix(x,y,a)=(1a)x+aymix(x, y, a) = (1-a)x + ay

表示从xx变化到yyaa表示程度

  1. a=0a = 0时,表示起点xx
  2. a=1a = 1时,表示终点yy
  3. aa介于(0,1)(0, 1)之间时,使用线性插值

对于代码gl_FragColor = mix( outputColor, texel, alpha );,使用线性插值的方式不太好解释。

我们回头看一下公式(1a)x+ay(1-a)x + ay,这个公式和three.js中的premultipliedAlphafalse时,NormalBlending的混合方式很像。

关于混合blend的内容,如果不了解,可以看我的另一篇文章Three.js混合 - 掘金 (juejin.cn)

aa替换为AlphasrcAlpha_{src}yy替换为CsrcC_{src}xx替换为CdstC_{dst},公式就变成: mix(Cdst,Csrc,Alphasrc)=AlphasrcCsrc+(1Alphasrc)Cdstmix(C_{dst}, C_{src}, Alpha_{src}) = Alpha_{src}C_{src} + (1- Alpha_{src})C_{dst}。可以看到,这里就是手动的在shader中实现blend

所以,这句代码可以这样解释:对于一个采样后的颜色texel来说,我们指定一个alpha当作它的透明度,与“背景”outputColor进行混合。

总之,LuminosityHighPassShader通过给采样后的颜色指定一个alpha,来确定这个颜色是否显示;并且还会与给定地“背景颜色”进行混合。