文章

OpenGL复习(Gamma、模糊、抗锯齿)

Gamma伽马校正(ga ma jiao zheng)

  • 物理亮度即光子数量,光子数量翻倍时是真实的物理亮度翻倍,线性亮度
  • 人眼感知,对暗部变化更敏感(暗部只需要较少的光子数增幅,就会感到明显的变化)
  • CRT显示器正好符合人眼感知,非线性亮度即\^2.2
  • 1
  • 图中,横轴光子数量,纵轴颜色/亮度值,它们都以归一化表示,中间是线性的物理亮度,下方\^2.2,上方\^1/2.2 ~= 0.45
  • 观察发现,\^2.2比\^1的整体要暗,但0和1颜色是相等的
  • Gamma校正:应用Gamma倒数(1/2.2),线性颜色\^1 -> 显示器\^2.2 -> Gamma\^1/2.2 颜色变回最初设置的线性颜色
  • 在opengl中使用Gamma校正:
    • 内建的sRGB帧缓冲:glEnable(GL_FRAMEBUFFER_SRGB);(注意:我们应在最后的颜色计算应用校正,否则将使用不正确的颜色值)
    • 手动在每个fragment shader中校正,更加简单的方式新增一个后期处理shader,仅在quad上校正一次
  • RGB纹理:不用任何转换,线性颜色^1 -> 显示器^2.2 -> Gamma^1/2.2
  • sRGB纹理:在sRGB颜色空间(gamma2.2)中制作的纹理
    • 不使用校正时没有问题:纹理在sRGB空间创作和显示,这是统一的
    • 但应用了gamma后,纹理颜色就会很亮,因为我们在sRGB空间制作纹理,会通常将它的颜色设置的更亮,以便达到预期颜色(比如0.5的颜色值),经过gamma后它的颜色变为了线性颜色,会超出预期颜色
  • 为了解决这个问题,有几种方案:
    • 在线性空间工作:线性颜色\^1 -> 显示器\^2.2 -> Gamma\^1/2.2 颜色变回最初设置的线性颜色
    • 每次使用前\^2.2,opengl中提供了内置的纹理格式GL_SRGB和GL_SRGB_ALPHA,它们会自动校正为线性空间
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <iostream>
#include <cmath>

// Linear RGB 转换为 sRGB
double linearToSrgb(double linear) {
    double s;
    if (linear <= 0.0031308) {
        s = linear * 12.92;
    } else {
        s = 1.055 * std::pow(linear, 1.0 / 2.4) - 0.055;
    }
    return s;
}

// sRGB 转换为 Linear RGB
double srgbToLinear(double s) {
    double linear;
    if (s <= 0.04045) {
        linear = s / 12.92;
    } else {
        linear = std::pow((s + 0.055) / 1.055, 2.4);
    }
    return linear;
}

int main() {
    // 示例:将 Linear 值 0.5 转换为 sRGB 0.735
    double linearValue = 0.5;
    double srgbValue = linearToSrgb(linearValue);
    std::cout << "Linear value " << linearValue << " converts to sRGB value " << srgbValue << std::endl;

    // 示例:将 sRGB 值 0.735 转换为 Linear 0.5
    srgbValue = 0.735;
    linearValue = srgbToLinear(srgbValue);
    std::cout << "sRGB value " << srgbValue << " converts to Linear value " << linearValue << std::endl;

    return 0;
}

展开

  • rgb空间和srgb空间转换算法

高斯模糊

  • 使用滤波函数,生成卷积核对应的权重
  • 卷积核的范围越大,图像就越模糊
  • 均值滤波函数:1/width*height,width以及height分别为卷积核的宽与高,因此每个权重值都相等
  • 1
  • 高斯滤波函数:高斯模糊基于高斯曲线(基于正太分布),中间的值达到最大化,随着距离的增加,值不断减少,如上权重值
  • 两步高斯模糊(优化)
    • 想象对于32×32的卷积核,要对每个像素做1024次采样计算,计算量非常庞大
    • 由于高斯模糊符合正太分布,它可以拆分成一维卷积操作,数学原理,总共只需要32+32次计算
    • 在水平方向做32次,再垂直方向做32次
    • 使用两个帧缓冲,每个帧缓冲有一个颜色纹理附件( 一个帧缓冲一个附件,卷积要一个纹理读,一个纹理写,不能同时读写同一个纹理 / 一个帧缓冲,2个颜色纹理附件,这种方式技术上不太可行,同一个帧缓冲的颜色纹理附件没法一个读一个写)
    • 权重值在代码怎么计算的
      • 没有计算,直接使用的特定数值
    • 文章中为什么amount = 10
      • 每2次是一次水平+一次垂直,10次相当于做了5次的高斯模糊,模糊次数越多越模糊

抗锯齿/反走样

  • 形成原因:像素分辨率和栅格化的底层实现
  • 先模糊再采样(要注意顺序,反过来达不到理想效果),模糊通常用卷积实现,在信号学中又叫滤波,但这种方式较难实现,并且计算量巨大
  • 提高采样点数量
    • 片元:图元被分解位一系列的小方形区域,这个区域<=一个像素的区域,可以存储数据
    • 假设为每个像素新增4个采样点,每个图元对应1组片元,假设最坏情况,有n个三角形图元完全重叠,每个图元覆盖m个像素,从后往前绘制
    • SSAA
      • 光栅化阶段:每个采样点和原采样点一样经历所有相同阶段,覆盖判断(生成4*m*n个片元),fs(对每个片元着色),深度测试(对每个片元测试)(其他阶段忽略不考虑)
      • 降采样:将采样点颜色平均值作为像素颜色
      • 时间:当没有应用SSAA,每个阶段是m*n, 总共是m*n*3,应用SSAA后,变为4*m*n*3
      • 空间:在depth_buffer和color_buffer数组大小扩充4倍
    • MSAA
      • alt text
      • 光栅化阶段:对每个采样点执行覆盖判断,深度测试(其他阶段忽略不考虑)
      • fs阶段:对于每个像素,所有同层级(对应相同图元,比如一个图元对某像素生成了4个片元,只做一次计算)的片元仅着色一次,将着色结果拷贝给其他同层级片元,这样fs阶段只需要m*n次
      • 降采样:将采样点颜色平均值作为像素颜色
      • 时间:4m*n*2 + m*n,但考虑fs最耗费性能,会较大优化性能
      • 空间:在depth_buffer和color_buffer数组大小扩充到4倍
    • SSAA vs. MSAA
      • MSAA比SSAA开销低很多,通过拷贝颜色而不是计算颜色
      • MSAA比SSAA效果准确度差,MSAA根据中心采样点计算的颜色值拷贝过来,SSAA根据具体采样点位置计算
  • 后期处理方式
    • FXAA
      • 不同于上面的方式,它通过后处理方式,检测边缘,对其颜色混合处理
      • 优点:集成比较方便,只需要新增一个阶段来实现抗锯齿
      • 缺点:画面会略微有些模糊,高频(颜色变化剧烈)的地方会不稳定,并且在不断改变视角时,有可能会导致一些闪烁
      • 原理:
        • Quality版本(注重质量):

          alt text

          • 排除内部点:计算 当前处理的像素点 和 周围像素点 的亮度对比值,当对比度值超过一定阈值,被判定为边界,需要进行接下来的抗锯齿处理

            1
            2
            3
            4
            
            float MaxLuma = max(N, E, W, S, M);
            float MinLuma = min(N, E, W, S, M);
            float Contrast =  MaxLuma - MinLuma;
            if(Contrast >= _Threshold)
            

            展开

            • 确定水平和垂直方向上5个像素点的亮度差(MaxLuma - MinLuma),如果为了结果更精确可以计算对角总共9个像素点,3X3 像素块
            • 亮度值使用亮度公式计算 L = 0.213 * R + 0.715 * G + 0.072 * B,其中绿色对亮度贡献最大
          • 计算混合系数:计算 当前处理的像素点 和 周围像素点的平均亮度 的差值

            1
            2
            3
            
            float Filter = 2 * (N + E + S + W) + NE + NW + SE + SW;
            Filter = Filter / 12;//加权颜色混合值
            float PixelBlend = abs(Filter -  M);//差值,混合系数
            

            展开

            • 权重卷积核:距离越远权重越小
            • 变化越剧烈,混合系数越大
          • 确定混合方向:

            1
            2
            3
            4
            5
            6
            7
            8
            9
            10
            
            //确定混合方向
            float Vertical = abs(N + S - 2 * M) * 2+ abs(NE + SE - 2 * E) + abs(NW + SW - 2 * W);//水平亮度变化幅度
            float Horizontal = abs(E + W - 2 * M) * 2 + abs(NE + NW - 2 * N) + abs(SE + SW - 2 * S);//垂直亮度变化幅度
            bool IsVertical = Vertical > Horizontal;
            //确定步长大小
            float2 PixelStep = IsVertical ? float2(0, _MainTex_TexelSize.y) : float2(_MainTex_TexelSize.x, 0);
            //确定步长方向
            float Positive = abs((IsVertical ? N : E) - M);
            float Negative = abs((IsVertical ? S : W) - M);
            if(Positive < Negative) PixelStep = -PixelStep;
            

            展开

            • 锯齿边界通常不会是刚好水平或者垂直的,我们要寻找一个最接近的方向
            • 如果水平方向的亮度变化较大,锯齿边界就是垂直的,沿水平方向进行混合;如果垂直方向的亮度变化较大,锯齿边界是水平的,按垂直方向进行混合
            • 确定步长方向,取变化剧烈的那个方向
            • 约定:当在垂直方向时,我们约定向上为正,向下为负。在水平方向时,向右为正,向左为负
          • 混合:

            1
            
            float4 Result = tex2D(_MainTex, UV + PixelStep * PixelBlend);
            

            展开

            • 当前处理的像素点颜色,为当前纹理坐标 + 混合方向 * 混合系数,的偏移坐标采样
        • 混合系数优化:

          1
          2
          3
          4
          5
          6
          7
          8
          9
          10
          11
          12
          13
          14
          15
          16
          17
          18
          19
          20
          21
          22
          23
          24
          25
          26
          27
          28
          29
          30
          31
          32
          33
          34
          35
          36
          37
          38
          39
          40
          41
          42
          43
          44
          
            //阈值
            float Positive = abs((IsHorizontal ? N : E) - M);
            float Negative = abs((IsHorizontal ? S : W) - M);
            float Gradient, OppositeLuminance;//当前像素对应的梯度值,混合方向的像素
            if(Positive > Negative) {
                Gradient = Positive;
                OppositeLuminance = IsHorizontal ? N : E;
            } else {
                PixelStep = -PixelStep;
                Gradient = Negative;
                OppositeLuminance = IsHorizontal ? S : W;
            }
            float EdgeLuminance = (M + OppositeLuminance) * 0.5f;//M和混合方向像素的平均值
            float GradientThreshold = EdgeLuminance * 0.25f;//再乘0.25
          
            //相对方向
            float2 EdgeStep = IsVertical ? float2(_MainTex_TexelSize.x, 0) : float2(0, _MainTex_TexelSize.y);
          
            //计算边界距离
            int i;
            for(i = 1; i <= _SearchSteps; ++i) {//搜索步长
                PLuminanceDelta = Luminance(tex2D(_MainTex, UVEdge + i * EdgeStep)) - EdgeLuminance;//边界采样(两侧像素平均值) - 中心的两侧像素平均值,即平均值的差
                if(abs(PLuminanceDelta) > GradientThreshold) {//abs(平均值差值) > 阈值
                    PDistance = i * (IsVertical ? EdgeStep.x : EdgeStep.y);//获得边界距离
                    break;
                }
            }
            if(i == _SearchSteps + 1) {//如果超出搜索步长
                PDistance = EdgeStep * _SearchSteps;//限制到边界
            }
            //沿着相反方向搜索,计算NDistance
            for(i = 1; i <= _SearchSteps; ++i) {
              //NDistance……
            }
          
            //计算混合系数
            if (PDistance < NDistance) {
              EdgeBlend = 0.5f - PDistance / (PDistance + NDistance);//归一化到0——1,反转到0.5—— -0.5
            }else{
              EdgeBlend = 0.5f - NDistance / (PDistance + NDistance);
            }
          
            //计算最终混合系数
            float FinalBlend = max(PixelBlend, EdgeBlend);
          

          展开

          • 计算边界距离:
            • 沿着混合方向的相对方向的两侧搜索(沿着锯齿边界方向)
            • 方法一:
              • 梯度值:当前处理的像素和混合方向的像素的亮度差值
              • 阈值:当前梯度值 * 0.25作为阈值 +
              • 比较:如果abs(梯度值差值) > 当前梯度值 + 阈值 abs(梯度值差值) < 当前梯度值 - 阈值,则认为是到达了锯齿的边界
            • 方法二:
              • 阈值:当前平均亮度值 * 0.25作为阈值 + 当前梯度值
              • 比较优化:比较时可以不用分别对两个像素采样平均,可以利用线性过滤,在边界处采样,既两侧像素的平均亮度值
          • 计算混合系数:根据正负边界距离动态调整混合系数,左右边界差距越大,混合系数越大
          • 计算最终混合系数:和之前PixelBlend取最大值
        • Console版本(注重速度):

          alt text

          1
          2
          3
          4
          5
          6
          7
          8
          9
          10
          11
          12
          13
          14
          15
          16
          17
          18
          19
          20
          21
          22
          23
          24
          25
          26
          27
          28
          29
          
          //亮度对比值
          float MaxLuma = max(NW, NE, SW, SE);
          float MinLuma = min(NW, NE, SW, SE);
          float Contrast =  max(MaxLuma, M) -  min(MinLuma, M);
          if(Contrast >= _Threshold)
          
          //计算混合方向
          Dir.x = -((NW + NE) - (SW + SE));//水平亮度
          Dir.y = ((NE + SE) - (NW + SW));//垂直亮度
          Dir = normalize(Dir);//构成向量,获取方向
          
          //沿着混合方向采样平均
          float2 Dir1 = Dir * _MainTex_TexelSize.xy;
          float4 N1 = tex2D(_MainTex, UV + Dir1);
          float4 P1 = tex2D(_MainTex, UV - Dir1);
          float4 Result = (N1 + P1) * 0.5f;
          
          //优化
          float DirAbsMinTimesC = min(abs(Dir.x), abs(Dir.y));//越水平,结果越小
          float2 Dir2 = clamp(Dir1 / DirAbsMinTimesC, -2, 2) * 2;//分母越小,值越大
          
          float2 Dir2 = Dir2 * _MainTex_TexelSize.xy;
          float4 N2 = tex2D(_MainTex, UV + Dir2);
          float4 P2 = tex2D(_MainTex, UV - Dir2);
          float4 Result2 = Result * 0.5f + (N2 + P2) * 0.25f;//和上次结果加权混合
          
          if(Luminance(Result2.xyz) > MinLuma && Luminance(Result2.xyz) < MaxLuma) {//检测是否在亮度范围中
              Result = Result2;
          }
          

          展开

          • 采样点:M像素的四个顶点位置,是圆点所有覆盖像素的亮度平均值
          • 排除内部点:……同上
          • 计算混合方向和系数
          • 计算像素结果:沿着混合方向采样平均
          • 优化混合系数:
            • 上述对于水平/垂直锯齿不友好
            • 对Dir两个分量的最小值的倒数对Dir1缩放,获得Dir2
            • 如果切向方向越接近水平或者垂直方向,那么采样的距离缩放值就越大
            • 和上次结果加权混合
            • 检测是否在亮度范围中,否则丢弃
本文由作者按照 CC BY 4.0 进行授权
本页总访问量