Three.js后处理 提取亮色
在某些需求下,需要将一个场景中比较“亮”的颜色提取出来。那么,我们需要知道怎么怎么计算亮度。
自定义一个提取亮色的Shader
1. 计算颜色的亮度
在前端开发中,通常使用RGB来表示颜色,每个通道的值越大,表示该颜色所占的比重越多,同时,亮度也就越高。所以,很容易想到的就是使用三个通道的平均数来表示亮度,即。
但是对于人眼的感知来说,三种颜色“贡献”的亮度并不相同,所以,基于人眼感知的计算方式就是一个加权平均数。
在这里,我们采用第二种加权平均数的算法,亮度的范围为。
// 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)
这个函数是一个平滑的插值函数:
先来理一下思路,这个shader和我们之前编写的有一些不同:我们采用的方式是“一刀切”,我们的阈值是一个点;但是LuminosityHighPassShader并不是,它的阈值是一个范围。
float alpha = smoothstep( luminosityThreshold, luminosityThreshold + smoothWidth, v );
- 当明亮度
v小于阈值时,alpha的值为0 - 当明亮度
v大于luminosityThreshold + smoothWidth时,alpha的值为1 - 当明亮度
v在范围内,alpha的值为之间的插值
mix(x, y, a)
这个函数是一个线性插值函数:
表示从变化到,表示程度。
- 当时,表示起点
- 当时,表示终点
- 当介于之间时,使用线性插值
对于代码gl_FragColor = mix( outputColor, texel, alpha );,使用线性插值的方式不太好解释。
我们回头看一下公式,这个公式和three.js中的premultipliedAlpha为false时,NormalBlending的混合方式很像。
关于混合blend的内容,如果不了解,可以看我的另一篇文章Three.js混合 - 掘金 (juejin.cn)。
将替换为,替换为,替换为,公式就变成: 。可以看到,这里就是手动的在shader中实现blend。
所以,这句代码可以这样解释:对于一个采样后的颜色texel来说,我们指定一个alpha当作它的透明度,与“背景”outputColor进行混合。
总之,LuminosityHighPassShader通过给采样后的颜色指定一个alpha,来确定这个颜色是否显示;并且还会与给定地“背景颜色”进行混合。