
GPUImageColorBalanceFilter 源码详解
GPUImageColorBalanceFilter 是基于 Android 平台 GPUImage 框架实现的色彩平衡滤镜,核心通过 GLSL 着色器在 GPU 端完成图像阴影、中间调、高光区域的色彩偏移调整,同时支持"保留亮度"模式,兼顾色彩调整与亮度一致性。
本文将逐行解析代码结构、核心逻辑,并说明实际使用方式。
代码整体结构梳理
该类分为以下核心模块:
- 包声明与依赖导入
- 类定义与核心 GLSL 片段着色器(色彩平衡的核心计算逻辑)
- 着色器 Uniform 变量位置缓存(shadowsLocation、midtonesLocation 等)
- 滤镜参数成员变量(阴影/中间调/高光偏移值、亮度保留开关)
- 构造方法(初始化默认参数)
- 生命周期方法(onInit/onInitialized:初始化着色器变量、设置默认参数)
- Setter 方法(更新滤镜参数并同步到 GPU)
代码逐段解析(含逐行注释)
1. 包声明与导入类
java
// 声明当前类所属的包路径,遵循 GPUImage 框架的滤镜包规范
package jp.co.cyberagent.android.gpuimage.filter;
// 导入 Android 原生 OpenGL ES 2.0 核心类,用于操作 GPU 着色器变量
import android.opengl.GLES20;
含义:
- 包声明遵循 GPUImage 框架的目录结构,确保滤镜能被框架识别;
- 导入
GLES20类,用于与 OpenGL ES 2.0 交互(如获取着色器 Uniform 变量位置、设置变量值)。
2. 类定义与核心成员变量
java
/**
* Created by edward_chiang on 13/10/16.
*/
// 继承 GPUImageFilter(GPUImage 框架的基础滤镜类),实现色彩平衡滤镜
public class GPUImageColorBalanceFilter extends GPUImageFilter {
// 片段着色器字符串:核心色彩平衡计算逻辑(GLSL 代码)
public static final String GPU_IMAGE_COLOR_BALANCE_FRAGMENT_SHADER = "" +
// 纹理坐标(从顶点着色器传递过来)
"varying highp vec2 textureCoordinate;\n" +
// 输入纹理(待处理的图像)
"uniform sampler2D inputImageTexture;\n" +
// 阴影区域色彩偏移值(RGB 三维向量)
"uniform lowp vec3 shadowsShift;\n" +
// 中间调区域色彩偏移值(RGB 三维向量)
"uniform lowp vec3 midtonesShift;\n" +
// 高光区域色彩偏移值(RGB 三维向量)
"uniform lowp vec3 highlightsShift;\n" +
// 是否保留亮度(int 型:0=不保留,1=保留)
"uniform int preserveLuminosity;\n" +
// ========== 核心函数 1:RGB 转 HSL 颜色空间 ==========
"lowp vec3 RGBToHSL(lowp vec3 color)\n" +
"{\n" +
"lowp vec3 hsl; // 定义 HSL 向量(H:色相, S:饱和度, L:亮度)\n" +
"lowp float fmin = min(min(color.r, color.g), color.b); // 取 RGB 最小值(计算亮度用)\n" +
"lowp float fmax = max(max(color.r, color.g), color.b); // 取 RGB 最大值(计算亮度/饱和度用)\n" +
"lowp float delta = fmax - fmin; // RGB 差值(计算饱和度用)\n" +
"hsl.z = (fmax + fmin) / 2.0; // 计算亮度 L(0=黑,1=白)\n" +
"if (delta == 0.0) // 差值为0 → 灰度色(无饱和度)\n" +
"{\n" +
" hsl.x = 0.0; // 色相 H 设为0(灰度无色相)\n" +
" hsl.y = 0.0; // 饱和度 S 设为0\n" +
"}\n" +
"else // 非灰度色 → 计算色相/饱和度\n" +
"{\n" +
" // 计算饱和度 S:亮度<0.5 时,S=delta/(max+min);否则 S=delta/(2-max-min)\n" +
" if (hsl.z < 0.5)\n" +
" hsl.y = delta / (fmax + fmin); \n" +
" else\n" +
" hsl.y = delta / (2.0 - fmax - fmin); \n" +
"\n" +
" // 计算色相偏移量(用于最终色相 H 的计算)\n" +
" lowp float deltaR = (((fmax - color.r) / 6.0) + (delta / 2.0)) / delta;\n" +
" lowp float deltaG = (((fmax - color.g) / 6.0) + (delta / 2.0)) / delta;\n" +
" lowp float deltaB = (((fmax - color.b) / 6.0) + (delta / 2.0)) / delta;\n" +
"\n" +
" // 根据 RGB 最大值所在通道,计算色相 H(范围 0~1)\n" +
" if (color.r == fmax )\n" +
" hsl.x = deltaB - deltaG; \n" +
" else if (color.g == fmax)\n" +
" hsl.x = (1.0 / 3.0) + deltaR - deltaB; \n" +
" else if (color.b == fmax)\n" +
" hsl.x = (2.0 / 3.0) + deltaG - deltaR; \n" +
" // 修正色相范围(确保 H 在 0~1 之间)\n" +
" if (hsl.x < 0.0)\n" +
" hsl.x += 1.0; \n" +
" else if (hsl.x > 1.0)\n" +
" hsl.x -= 1.0; \n" +
"}\n" +
"\n" +
"return hsl; // 返回 HSL 颜色值\n" +
"}\n" +
// ========== 核心函数 2:色相转 RGB 分量 ==========
"lowp float HueToRGB(lowp float f1, lowp float f2, lowp float hue)\n" +
"{\n" +
" // 修正色相范围(0~1)\n" +
" if (hue < 0.0)\n" +
" hue += 1.0;\n" +
" else if (hue > 1.0)\n" +
" hue -= 1.0;\n" +
" lowp float res; // 存储转换后的 RGB 分量值\n" +
" // 根据色相区间,计算 RGB 分量\n" +
" if ((6.0 * hue) < 1.0)\n" +
" res = f1 + (f2 - f1) * 6.0 * hue;\n" +
" else if ((2.0 * hue) < 1.0)\n" +
" res = f2;\n" +
" else if ((3.0 * hue) < 2.0)\n" +
" res = f1 + (f2 - f1) * ((2.0 / 3.0) - hue) * 6.0;\n" +
" else\n" +
" res = f1;\n" +
" return res; // 返回 RGB 分量值\n" +
"}\n" +
// ========== 核心函数 3:HSL 转 RGB 颜色空间 ==========
"lowp vec3 HSLToRGB(lowp vec3 hsl)\n" +
"{\n" +
" lowp vec3 rgb; // 存储转换后的 RGB 颜色值\n" +
" if (hsl.y == 0.0) // 饱和度为0 → 灰度色(RGB 等于亮度)\n" +
" rgb = vec3(hsl.z); \n" +
" else\n" +
" {\n" +
" lowp float f2; // 辅助变量(计算 RGB 用)\n" +
" // 根据亮度计算 f2\n" +
" if (hsl.z < 0.5)\n" +
" f2 = hsl.z * (1.0 + hsl.y);\n" +
" else\n" +
" f2 = (hsl.z + hsl.y) - (hsl.y * hsl.z);\n" +
" lowp float f1 = 2.0 * hsl.z - f2; // 辅助变量\n" +
" // 分别计算 R/G/B 分量(基于色相偏移)\n" +
" rgb.r = HueToRGB(f1, f2, hsl.x + (1.0/3.0));\n" +
" rgb.g = HueToRGB(f1, f2, hsl.x);\n" +
" rgb.b= HueToRGB(f1, f2, hsl.x - (1.0/3.0));\n" +
" }\n" +
" return rgb; // 返回 RGB 颜色值\n" +
"}\n" +
// ========== 核心函数 4:RGB 转亮度 L ==========
"lowp float RGBToL(lowp vec3 color)\n" +
"{\n" +
" lowp float fmin = min(min(color.r, color.g), color.b); // RGB 最小值\n" +
" lowp float fmax = max(max(color.r, color.g), color.b); // RGB 最大值\n" +
" return (fmax + fmin) / 2.0; // 返回亮度 L\n" +
"}\n" +
// ========== 主函数:片元着色器入口 ==========
"void main()\n" +
"{\n" +
" // 采样当前纹理坐标的像素颜色(RGBA)\n" +
" lowp vec4 textureColor = texture2D(inputImageTexture, textureCoordinate);\n" +
" // 获取像素的 RGB 亮度(注:原代码注释的 alternative way 是直接用 RGB,实际取的是原像素 RGB)\n" +
" lowp vec3 lightness = textureColor.rgb;\n" +
" // 定义色彩平衡计算的辅助常量\n" +
" const lowp float a = 0.25;\n" +
" const lowp float b = 0.333;\n" +
" const lowp float scale = 0.7;\n" +
" // 计算阴影区域的色彩偏移:基于亮度区间筛选阴影像素,并乘以偏移值\n" +
" lowp vec3 shadows = shadowsShift * (clamp((lightness - b) / -a + 0.5, 0.0, 1.0) * scale);\n" +
" // 计算中间调区域的色彩偏移:筛选中间调像素,乘以偏移值\n" +
" lowp vec3 midtones = midtonesShift * (clamp((lightness - b) / a + 0.5, 0.0, 1.0) *\n" +
" clamp((lightness + b - 1.0) / -a + 0.5, 0.0, 1.0) * scale);\n" +
" // 计算高光区域的色彩偏移:筛选高光像素,乘以偏移值\n" +
" lowp vec3 highlights = highlightsShift * (clamp((lightness + b - 1.0) / a + 0.5, 0.0, 1.0) * scale);\n" +
" // 合并阴影/中间调/高光的色彩偏移,得到新颜色\n" +
" mediump vec3 newColor = textureColor.rgb + shadows + midtones + highlights;\n" +
" // 修正颜色范围(确保 RGB 在 0~1 之间,避免过曝/过暗)\n" +
" newColor = clamp(newColor, 0.0, 1.0);\n " +
" // 判断是否保留原亮度\n" +
" if (preserveLuminosity != 0) {\n " +
" // 新颜色转 HSL(获取调整后的色相/饱和度)\n" +
" lowp vec3 newHSL = RGBToHSL(newColor);\n" +
" // 原颜色的亮度\n" +
" lowp float oldLum = RGBToL(textureColor.rgb);\n" +
" // 保留原亮度,将新色相/饱和度转回 RGB\n" +
" textureColor.rgb = HSLToRGB(vec3(newHSL.x, newHSL.y, oldLum));\n" +
" // 输出最终颜色(保留原 Alpha 通道)\n" +
" gl_FragColor = textureColor;\n" +
" } else {\n" +
" // 不保留亮度,直接输出新颜色(保留原 Alpha 通道)\n" +
" gl_FragColor = vec4(newColor.rgb, textureColor.w);\n" +
" }\n" +
"}\n";
// ========== Java 层成员变量:缓存着色器 Uniform 变量位置 ==========
// 阴影偏移变量的 GPU 位置
private int shadowsLocation;
// 中间调偏移变量的 GPU 位置
private int midtonesLocation;
// 高光偏移变量的 GPU 位置
private int highlightsLocation;
// 亮度保留开关变量的 GPU 位置
private int preserveLuminosityLocation;
// ========== 滤镜参数:默认值 ==========
// 阴影偏移值(RGB 三维数组,默认无偏移)
private float[] showdows; // 注:变量名拼写错误(应为 shadows),属于代码小瑕疵
// 中间调偏移值(默认无偏移)
private float[] midtones;
// 高光偏移值(默认无偏移)
private float[] highlights;
// 是否保留亮度(默认开启)
private boolean preserveLuminosity;
核心含义:
- 着色器字符串 :是滤镜的核心,通过 GLSL 实现:
- 定义输入纹理、色彩偏移变量、亮度保留开关;
- 实现 RGB↔HSL 颜色空间转换(用于"保留亮度"功能);
- 主函数中根据像素亮度区分阴影/中间调/高光区域,叠加对应色彩偏移;
- 可选保留原亮度(仅调整色相/饱和度,不改变亮度)。
- Java 层成员变量 :
- 缓存 Uniform 变量的 GPU 位置(避免重复查询,提升性能);
- 存储滤镜参数(默认无色彩偏移,开启亮度保留)。
3. 构造方法
java
// 构造方法:初始化滤镜默认参数
public GPUImageColorBalanceFilter() {
// 调用父类构造方法:顶点着色器用默认(NO_FILTER_VERTEX_SHADER),片段着色器用自定义的色彩平衡着色器
super(NO_FILTER_VERTEX_SHADER, GPU_IMAGE_COLOR_BALANCE_FRAGMENT_SHADER);
// 初始化阴影偏移:RGB 均为 0(无偏移)
this.showdows = new float[]{0.0f, 0.0f, 0.0f};
// 初始化中间调偏移:无偏移
this.midtones = new float[]{0.0f, 0.0f, 0.0f};
// 初始化高光偏移:无偏移
this.highlights = new float[]{0.0f, 0.0f, 0.0f};
// 默认保留亮度
this.preserveLuminosity = true;
}
含义:
- 父类
GPUImageFilter的构造方法需要传入顶点着色器和片段着色器:- 顶点着色器用框架默认的
NO_FILTER_VERTEX_SHADER(仅处理纹理坐标变换); - 片段着色器用自定义的色彩平衡着色器。
- 顶点着色器用框架默认的
- 初始化所有参数为默认值,确保滤镜创建后可直接使用。
4. 初始化生命周期方法
java
// 滤镜初始化时调用:获取着色器 Uniform 变量的 GPU 位置
@Override
public void onInit() {
super.onInit(); // 调用父类初始化逻辑
// 获取 "shadowsShift" 变量的 GPU 位置
shadowsLocation = GLES20.glGetUniformLocation(getProgram(), "shadowsShift");
// 获取 "midtonesShift" 变量的 GPU 位置
midtonesLocation = GLES20.glGetUniformLocation(getProgram(), "midtonesShift");
// 获取 "highlightsShift" 变量的 GPU 位置
highlightsLocation = GLES20.glGetUniformLocation(getProgram(), "highlightsShift");
// 获取 "preserveLuminosity" 变量的 GPU 位置
preserveLuminosityLocation = GLES20.glGetUniformLocation(getProgram(), "preserveLuminosity");
}
// 滤镜初始化完成后调用:设置默认参数到 GPU
@Override
public void onInitialized() {
super.onInitialized(); // 调用父类逻辑
// 设置中间调偏移值到 GPU
setMidtones(midtones);
// 设置阴影偏移值到 GPU(变量名拼写错误不影响功能)
setShowdows(showdows);
// 设置高光偏移值到 GPU
setHighlights(highlights);
// 设置亮度保留开关到 GPU
setPreserveLuminosity(preserveLuminosity);
}
含义:
onInit():滤镜初始化时,通过GLES20.glGetUniformLocation获取着色器中 Uniform 变量的 GPU 内存地址,缓存到成员变量中(避免每次绘制都查询,提升性能);onInitialized():初始化完成后,将默认参数同步到 GPU,确保滤镜生效。
5. Setter 方法(更新滤镜参数)
java
// 设置阴影偏移值,并同步到 GPU
public void setShowdows(float[] showdows) {
this.showdows = showdows; // 更新本地变量
// 将三维浮点数组设置到 GPU 的 shadowsShift 变量
setFloatVec3(shadowsLocation, showdows);
}
// 设置中间调偏移值,并同步到 GPU
public void setMidtones(float[] midtones) {
this.midtones = midtones; // 更新本地变量
setFloatVec3(midtonesLocation, midtones); // 同步到 GPU
}
// 设置高光偏移值,并同步到 GPU
public void setHighlights(float[] highlights) {
this.highlights = highlights; // 更新本地变量
setFloatVec3(highlightsLocation, highlights); // 同步到 GPU
}
// 设置是否保留亮度,并同步到 GPU
public void setPreserveLuminosity(boolean preserveLuminosity) {
this.preserveLuminosity = preserveLuminosity; // 更新本地变量
// 将布尔值转为 int(1/0),设置到 GPU 的 preserveLuminosity 变量
setInteger(preserveLuminosityLocation, preserveLuminosity ? 1 : 0);
}
}
含义:
- 每个 Setter 方法分为两步:更新本地参数 → 调用 GPUImage 框架的工具方法(
setFloatVec3/setInteger)将参数同步到 GPU; setFloatVec3:用于设置三维浮点数组(如 RGB 偏移);setInteger:用于设置亮度保留开关(GLSL 中用 int 接收布尔逻辑)。
着色器深度解析
该着色器的设计核心分为两部分:一是实现RGB↔HSL颜色空间互转 (为"保留亮度"功能提供基础);二是通过亮度区间筛选实现分区域色彩偏移,让阴影、中间调、高光的调色相互独立,最终实现专业级的色彩平衡效果。
下面将按**「全局变量声明」→「核心工具函数」→「主函数执行逻辑」**三个核心模块逐段解析,附逐行注释+原理说明,同时讲清代码中的关键设计细节和数学逻辑。
先明确核心基础
- 执行规则 :片段着色器为图像的每个像素执行一次main函数,GPU并行计算,效率远高于CPU端调色;
- 精度限定符 :代码中
highp/lowp/mediump是GLSL的精度修饰符,为移动端GPU做性能优化:highp(高精度):用于纹理坐标textureCoordinate,需精准定位像素,避免偏移;lowp(低精度):用于颜色、偏移值等计算,低精度足够满足视觉效果,节省GPU带宽和计算资源;mediump(中精度):用于中间计算结果(如newColor),平衡精度和性能;
- 颜色值范围 :所有RGB/HSL值均为0.0~1.0的归一化值 (如纯红为
vec3(1.0,0.0,0.0),黑色为vec3(0.0))。
完整逐行注释+模块解析
模块1:全局变量声明区
定义着色器的输入参数、可调配置、跨函数变量,是Java层与GPU层的参数交互入口,也是着色器的基础依赖。
c
// 从顶点着色器传递的纹理坐标(经光栅化线性插值,每个像素唯一)
varying highp vec2 textureCoordinate;
// 2D纹理采样器,关联待处理的输入图像纹理ID(如相机帧、图片)
uniform sampler2D inputImageTexture;
// 阴影区域的RGB色彩偏移值(lowp vec3:r/g/b各0~1,Java层可动态调整)
uniform lowp vec3 shadowsShift;
// 中间调区域的RGB色彩偏移值(核心可调参数)
uniform lowp vec3 midtonesShift;
// 高光区域的RGB色彩偏移值(核心可调参数)
uniform lowp vec3 highlightsShift;
// 亮度保留开关(int:1=开启,0=关闭,Java层可配置)
uniform int preserveLuminosity;
核心作用:
- 接收顶点着色器的纹理坐标,实现像素精准采样;
- 定义3个可动态调整的色彩偏移参数,分别控制阴影/中间调/高光的调色方向和强度;
- 定义亮度保留开关,实现"仅调色彩不调亮度"的专业调色效果。
模块2:核心工具函数区
实现RGB↔HSL颜色空间互转 和快速亮度计算,是"保留亮度"功能的底层支撑,也是色彩平衡的关键辅助。
函数1:RGBToHSL ------ RGB转HSL颜色空间
将RGB归一化值转换为HSL(色相H/饱和度S/亮度L),HSL空间中亮度L与色相H、饱和度S相互独立,这是"保留亮度"的核心原理(只需替换H/S,保留原L即可)。
c
// 入参:待转换的RGB颜色(vec3);返回值:转换后的HSL颜色(vec3,x=H,y=S,z=L)
lowp vec3 RGBToHSL(lowp vec3 color)
{
lowp vec3 hsl; // 定义HSL结果向量,x=色相(0~1), y=饱和度(0~1), z=亮度(0~1)
lowp float fmin = min(min(color.r, color.g), color.b); // 取RGB三通道最小值(计算亮度/饱和度用)
lowp float fmax = max(max(color.r, color.g), color.b); // 取RGB三通道最大值(计算亮度/饱和度用)
lowp float delta = fmax - fmin; // RGB最大值-最小值,反映色彩的鲜艳程度(计算饱和度用)
hsl.z = (fmax + fmin) / 2.0; // 计算亮度L:RGB最大/最小值的平均值,0=纯黑,1=纯白
if (delta == 0.0) // delta=0 → RGB三通道值相等,是灰度色(无色彩信息)
{
hsl.x = 0.0; // 灰度色无色相,设为0
hsl.y = 0.0; // 灰度色饱和度为0
}
else // delta≠0 → 有色彩信息,计算色相H和饱和度S
{
// 计算饱和度S:分亮度L<0.5和L≥0.5两种情况,保证S始终在0~1之间
if (hsl.z < 0.5)
hsl.y = delta / (fmax + fmin); // 低亮度时,饱和度=差值/(最大值+最小值)
else
hsl.y = delta / (2.0 - fmax - fmin); // 高亮度时,饱和度=差值/(2-最大值-最小值)
// 计算色相偏移量:为后续按RGB最大值通道计算色相做准备
lowp float deltaR = (((fmax - color.r) / 6.0) + (delta / 2.0)) / delta;
lowp float deltaG = (((fmax - color.g) / 6.0) + (delta / 2.0)) / delta;
lowp float deltaB = (((fmax - color.b) / 6.0) + (delta / 2.0)) / delta;
// 根据RGB最大值所在通道,计算基础色相H(色相环上R/G/B各差1/3)
if (color.r == fmax )
hsl.x = deltaB - deltaG; // 最大值为红,基础色相从红通道计算
else if (color.g == fmax)
hsl.x = (1.0 / 3.0) + deltaR - deltaB; // 最大值为绿,基础色相+1/3
else if (color.b == fmax)
hsl.x = (2.0 / 3.0) + deltaG - deltaR; // 最大值为蓝,基础色相+2/3
// 修正色相范围:确保H始终在0~1之间(超出则循环,如H=1.2→0.2,H=-0.1→0.9)
if (hsl.x < 0.0)
hsl.x += 1.0;
else if (hsl.x > 1.0)
hsl.x -= 1.0;
}
return hsl; // 返回转换后的HSL颜色
}
关键原理:
- 亮度L:采用简易亮度计算法(最大/最小值平均),而非人眼感知的亮度(如YCrCb的Y),兼顾计算效率和视觉效果;
- 饱和度S:分亮度计算是为了避免高亮度时饱和度溢出,保证调色后色彩自然;
- 色相H:将RGB映射到0~1的色相环,R(0/1)、G(1/3)、B(2/3)为三个基准点,实现RGB到色相的线性转换。
函数2:HueToRGB ------ 色相值转单个RGB分量
HSL转RGB的辅助函数,将色相值映射为对应的R/G/B单通道值,是HSLToRGB的核心子步骤。
c
// 入参:f1/f2(亮度相关辅助值)、hue(待转换的色相值);返回值:对应的RGB单通道值(0~1)
lowp float HueToRGB(lowp float f1, lowp float f2, lowp float hue)
{
// 先修正色相范围,确保在0~1之间
if (hue < 0.0)
hue += 1.0;
else if (hue > 1.0)
hue -= 1.0;
lowp float res; // 存储转换后的RGB分量结果
// 根据色相所在区间,线性映射为RGB分量值(色相环分6个区间,对应RGB的渐变)
if ((6.0 * hue) < 1.0)
res = f1 + (f2 - f1) * 6.0 * hue;
else if ((2.0 * hue) < 1.0)
res = f2;
else if ((3.0 * hue) < 2.0)
res = f1 + (f2 - f1) * ((2.0 / 3.0) - hue) * 6.0;
else
res = f1;
return res; // 返回单个RGB分量值
}
核心作用 :
将抽象的色相值(0~1)转换为具体的RGB单通道值,解决了"HSL中色相如何映射为RGB三通道"的核心问题,保证转换后的色彩无失真。
函数3:HSLToRGB ------ HSL转RGB颜色空间
将HSL值还原为RGB归一化值,与RGBToHSL互为逆操作,用于"保留亮度"时,将「新色相+新饱和度+原亮度」的组合还原为RGB颜色。
c
// 入参:待转换的HSL颜色(vec3);返回值:转换后的RGB颜色(vec3)
lowp vec3 HSLToRGB(lowp vec3 hsl)
{
lowp vec3 rgb; // 定义RGB结果向量
if (hsl.y == 0.0)
rgb = vec3(hsl.z); // 饱和度为0 → 灰度色,RGB三通道值均等于亮度L
else
{
lowp float f2; // 亮度相关辅助值,用于计算RGB分量
// 根据亮度L计算f2,与RGBToHSL的饱和度计算逻辑对应
if (hsl.z < 0.5)
f2 = hsl.z * (1.0 + hsl.y);
else
f2 = (hsl.z + hsl.y) - (hsl.y * hsl.z);
lowp float f1 = 2.0 * hsl.z - f2; // 辅助值,与f2配合实现色相到RGB的映射
// 分别计算R/G/B三通道值:每个通道在色相环上偏移1/3(120°),实现RGB色彩的完整覆盖
rgb.r = HueToRGB(f1, f2, hsl.x + (1.0/3.0));
rgb.g = HueToRGB(f1, f2, hsl.x);
rgb.b= HueToRGB(f1, f2, hsl.x - (1.0/3.0));
}
return rgb; // 返回转换后的RGB颜色
}
关键设计 :
R/G/B三通道分别偏移1/3色相值,对应色相环上R(0)、G(1/3)、B(2/3)的基准位置,确保任意色相值都能准确还原为对应的RGB色彩。
函数4:RGBToL ------ 快速计算RGB亮度
提取RGB颜色的亮度L,与RGBToHSL中的亮度计算逻辑完全一致,用于"保留亮度"时快速获取原图像素的亮度值,无需重复执行完整的RGBToHSL转换,提升计算效率。
// 入参:RGB颜色(vec3);返回值:亮度L(0~1)
lowp float RGBToL(lowp vec3 color)
{
lowp float fmin = min(min(color.r, color.g), color.b); // 取RGB最小值
lowp float fmax = max(max(color.r, color.g), color.b); // 取RGB最大值
return (fmax + fmin) / 2.0; // 返回亮度L(与RGBToHSL的L计算一致)
}
模块3:主函数main ------ 色彩平衡核心执行逻辑
着色器的入口函数,每个像素执行一次,通过「像素采样→分区域权重计算→色彩偏移叠加→亮度保留处理→颜色输出」的步骤,实现最终的色彩平衡效果,是整个着色器的核心。
c
void main()
{
// 步骤1:采样当前像素的RGBA颜色(根据纹理坐标从输入纹理中获取)
lowp vec4 textureColor = texture2D(inputImageTexture, textureCoordinate);
// 步骤2:定义亮度区域筛选的基础值(注释为备选方案:用RGBToL计算单亮度值,实际用RGB三通道值)
//lowp vec3 lightness = RGBToL(textureColor.rgb);
lowp vec3 lightness = textureColor.rgb;
// 步骤3:定义色彩平衡的核心常量(固定值,控制区域筛选范围和偏移强度)
const lowp float a = 0.25; // 区域范围系数:控制阴影/高光/中间调的亮度区间宽度
const lowp float b = 0.333; // 区域中心系数:定位阴影/高光的亮度区间中心
const lowp float scale = 0.7; // 偏移强度系数:限制色彩偏移的最大值,避免调色过度、色彩失真
// 步骤4:分区域计算色彩偏移权重 + 叠加偏移值
// 4.1 阴影区域:仅对低亮度像素叠加shadowsShift偏移,高亮度像素权重为0
lowp vec3 shadows = shadowsShift * (clamp((lightness - b) / -a + 0.5, 0.0, 1.0) * scale);
// 4.2 中间调区域:仅对中等亮度像素叠加midtonesShift偏移,阴影/高光像素权重为0(两个clamp乘积实现区间筛选)
lowp vec3 midtones = midtonesShift * (clamp((lightness - b) / a + 0.5, 0.0, 1.0) *
clamp((lightness + b - 1.0) / -a + 0.5, 0.0, 1.0) * scale);
// 4.3 高光区域:仅对高亮度像素叠加highlightsShift偏移,低亮度像素权重为0
lowp vec3 highlights = highlightsShift * (clamp((lightness + b - 1.0) / a + 0.5, 0.0, 1.0) * scale);
// 步骤5:叠加所有区域的色彩偏移,得到初步调整后的新颜色
mediump vec3 newColor = textureColor.rgb + shadows + midtones + highlights;
// 颜色溢出保护:确保RGB值始终在0~1之间(超出则截断为0或1,避免过曝/过暗)
newColor = clamp(newColor, 0.0, 1.0);
// 步骤6:亮度保留处理(核心分支逻辑)
if (preserveLuminosity != 0) { // 开启亮度保留(preserveLuminosity=1)
lowp vec3 newHSL = RGBToHSL(newColor); // 将新颜色转换为HSL,提取新的色相H和饱和度S
lowp float oldLum = RGBToL(textureColor.rgb); // 获取原图像素的亮度L(保留原亮度)
// 用「新H + 新S + 原L」重新转换为RGB,实现"仅调色彩,不调亮度"
textureColor.rgb = HSLToRGB(vec3(newHSL.x, newHSL.y, oldLum));
gl_FragColor = textureColor; // 输出最终颜色(保留原Alpha通道)
} else { // 关闭亮度保留(preserveLuminosity=0)
// 直接输出调整后的新颜色,保留原Alpha通道(textureColor.w为Alpha值)
gl_FragColor = vec4(newColor.rgb, textureColor.w);
}
}
主函数关键细节深度解析
主函数的核心难点是分区域亮度筛选的数学逻辑 和lightness的特殊设计,下面逐一讲清:
细节1:lightness = textureColor.rgb的设计巧思
代码中并未使用注释中的RGBToL(textureColor.rgb)(单亮度值),而是直接将lightness赋值为RGB三通道值(vec3),这是该着色器的核心设计亮点:
- 若用单亮度值,所有颜色通道会共享同一个区域权重(如红色暗部和绿色暗部的阴影偏移权重相同);
- 用RGB三通道值作为
lightness,每个颜色通道会独立计算区域权重 (如红色暗部的阴影偏移权重≠绿色暗部的权重),实现通道级的精细色彩平衡,调色效果更细腻、更贴合专业调色工具的逻辑。
细节2:分区域亮度筛选的数学逻辑(核心)
通过clamp函数结合线性公式,实现阴影/中间调/高光的自动区间筛选 ,核心原理是:仅当像素亮度在目标区间时,计算出的权重为0~1的非0值,否则为0,最终实现"仅目标区域叠加色彩偏移"。
以阴影区域为例,公式为:
glsl
clamp((lightness - b) / -a + 0.5, 0.0, 1.0)
- 当像素为低亮度 (如
lightness=0.1):lightness - b = -0.233,除以-a(-0.25)得0.932,加0.5得1.432→clamp后为1.0,阴影权重为1,完全叠加shadowsShift; - 当像素为中/高亮度 (如
lightness=0.5):lightness - b = 0.167,除以-a得-0.668,加0.5得-0.168→clamp后为0.0,阴影权重为0,不叠加shadowsShift。
高光区域 公式逻辑与阴影相反,仅高亮度像素会得到非0权重;中间调区域 通过两个clamp的乘积 实现筛选------只有中等亮度像素能同时满足两个clamp的非0条件,最终得到非0权重,完美实现三个区域的互斥独立调色。
细节3:scale=0.7的作用
scale是偏移强度限制系数,将色彩偏移的最大权重限制为0.7,避免因偏移值过大导致调色过度(如纯红叠加红色偏移后变成过曝的红),保证调色后的色彩自然、无失真。
细节4:颜色溢出保护(clamp(newColor, 0.0, 1.0))
色彩偏移叠加后,RGB值可能超出01的归一化范围(如`1.2`或`-0.1`),GPU会将超出范围的值直接截断,导致**色彩失真、过曝/过暗**,通过`clamp`函数强制将值限制在01之间,从根源避免该问题。
核心亮点与应用场景
核心亮点
- 分区域独立调色:阴影、中间调、高光三个区域互斥独立,可分别调整RGB偏移,实现专业级色彩平衡;
- 通道级精细调色:lightness采用RGB三通道值,每个颜色通道独立计算区域权重,调色效果更细腻;
- 亮度保留功能:基于RGB↔HSL互转,实现"仅调色相/饱和度,不改变亮度",避免调色后图像过曝/过暗;
- GPU并行加速:所有计算在GPU端完成,效率远高于CPU,支持相机实时预览、短视频实时调色等高性能场景;
- 色彩保护机制:通过scale系数和clamp函数,避免调色过度和颜色溢出,保证色彩自然;
- 参数可动态配置:阴影/中间调/高光偏移值、亮度保留开关均可由Java层动态调整,适配不同调色需求。
典型应用场景
- 图片编辑:如修图软件中的"色彩平衡"功能,调整人像/风景的阴影偏冷、高光偏暖等;
- 相机实时滤镜:直播、拍照时的实时色彩调整,实现个性化调色;
- 短视频后期:短视频的批量色彩平衡处理,提升画面质感;
- 影视级调色:基础色彩校正,如修复画面偏色、调整明暗区域的色彩氛围。
代码小细节说明
代码中有一处注释备选方案,无功能影响,仅为设计思路参考:
- 注释中的
lowp vec3 lightness = RGBToL(textureColor.rgb);是单亮度值方案,调色效率更高,但效果较粗糙; - 实际使用的
lowp vec3 lightness = textureColor.rgb;是通道级方案,调色效果更细腻,是框架的最终选择。
该着色器是GPUImage框架中色彩调整类滤镜的经典实现,其分区域调色、亮度保留的设计思路,也被广泛应用于其他图像编辑框架(如Glide、FFmpeg)的色彩平衡功能中。
实际使用示例
1. 依赖准备
确保项目中引入 GPUImage 框架(如通过 Gradle):
gradle
dependencies {
implementation 'jp.co.cyberagent.android:gpuimage:2.1.0'
}
2. 代码使用示例
java
// 1. 创建 GPUImage 实例(关联 SurfaceView/TextureView 用于显示)
GPUImage gpuImage = new GPUImage(context);
gpuImage.setGLSurfaceView((GLSurfaceView) findViewById(R.id.gpu_surface_view));
// 2. 加载待处理的图像(Bitmap 或 Uri)
Bitmap sourceBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.test_image);
gpuImage.setImage(sourceBitmap);
// 3. 创建色彩平衡滤镜实例
GPUImageColorBalanceFilter colorBalanceFilter = new GPUImageColorBalanceFilter();
// 4. 设置滤镜参数(核心步骤)
// 示例:调整阴影偏红、中间调偏绿、高光偏蓝,关闭亮度保留
float[] shadowsShift = new float[]{0.2f, 0.0f, 0.0f}; // 阴影红通道 +0.2
float[] midtonesShift = new float[]{0.0f, 0.2f, 0.0f}; // 中间调绿通道 +0.2
float[] highlightsShift = new float[]{0.0f, 0.0f, 0.2f}; // 高光蓝通道 +0.2
colorBalanceFilter.setShowdows(shadowsShift); // 设置阴影偏移(注意变量名拼写)
colorBalanceFilter.setMidtones(midtonesShift); // 设置中间调偏移
colorBalanceFilter.setHighlights(highlightsShift); // 设置高光偏移
colorBalanceFilter.setPreserveLuminosity(false); // 关闭亮度保留
// 5. 应用滤镜到 GPUImage
gpuImage.setFilter(colorBalanceFilter);
// 6. (可选)获取处理后的 Bitmap
Bitmap filteredBitmap = gpuImage.getBitmapWithFilterApplied();
3. 参数调整说明
- 色彩偏移值范围 :建议在
-1.0 ~ 1.0之间(超出范围会被clamp修正为 0~1); - 保留亮度(preserveLuminosity) :
true:仅调整色相/饱和度,图像整体亮度不变(适合人像、风景调色,避免过曝/过暗);false:直接叠加色彩偏移,亮度会随偏移值变化(适合创意调色);
- 阴影/中间调/高光区分 :通过亮度区间(由常量
a=0.25、b=0.333控制),无需手动指定区间,算法自动筛选。
总结
- 色彩平衡调整:分阴影、中间调、高光三个区域独立调整 RGB 偏移,精准控制图像不同亮度区域的色彩;
- 亮度保留:基于 RGB↔HSL 颜色空间转换,确保调色后亮度与原图一致,提升调色自然度;
- GPU 加速:所有计算在 GPU 端完成,相比 CPU 处理更高效,适合实时预览(如相机滤镜)。
