games101-2 透視深度插值矯正與抗鋸齒分析

dyccyber發表於2023-12-03

深度插值的差錯原因

img
當投影的圖形與投影的平面不平行時,這時進行透視投影,從上圖中可以看出,投影平面上的線段時均勻的,但是在原圖形上的線段是非均勻的,這只是一個例子,但也可以看出投影會導致圖形的變形,在我們利用重心座標,進行深度插值時原空間中的重心座標會發生變形,導致我們得到的深度不是正確的,這一點在對紋理座標進行插值時尤其明顯

透視深度插值公式推導

雖然在原空間與投影平面上的三角形可能發生變形,但是它們的重心座標依然滿足一定的關係:
投影平面:
\(1 = \alpha^{'} +\beta^{'} +\gamma^{'}\)

原空間:
\(1 = \alpha +\beta +\gamma\)

現在我們只有投影平面上三角形的bounding box中一個個畫素點,我們想要得到這個畫素點真實的深度值,假設一個畫素點真實的深度值為\(Z\),三角形三個頂點真實的深度值分別為\(Z_{a},Z_{b},Z_{c}\),我們對第一個式子進行恆等變形:

$\frac{Z}{Z} = \frac{Z_{a}}{Z_{a}}\alpha^{'} + \frac{Z_{b}}{Z_{b}}\beta^{'} + \frac{Z_{c}}{Z_{c}}\gamma^{'} $

進一步變換得到:
\(Z = (\frac{Z}{Z_{a}}\alpha^{'})Z_{a} + (\frac{Z}{Z_{b}}\beta^{'})Z_{b} + (\frac{Z}{Z_{c}}\gamma^{'})Z_{c}\)

我們對照原空間的深度重心插值公式:
\(Z = \alpha Z_{a} + \beta Z_{b} + \gamma Z_{c}\)

可以得到:
\(\alpha = \frac{Z}{Z_{a}}\alpha^{'}\)
\(\beta = \frac{Z}{Z_{b}}\beta^{'}\)
\(\gamma = \frac{Z}{Z_{c}}\gamma^{'}\)

我們再代入之前的第二個式子:
\(1 = \frac{Z}{Z_{a}}\alpha^{'} + \frac{Z}{Z_{b}}\beta^{'} + \frac{Z}{Z_{c}}\gamma^{'}\)

兩邊同時除以\(Z\):
$\frac{1}{Z} = \frac{1}{Z_{a}}\alpha^{'} + \frac{1}{Z_{b}}\beta^{'} + \frac{1}{Z_{c}}\gamma^{'} $

我們可以進一步考慮更一般的情況,對任意屬性(uv座標顏色法線等)使用重心座標進行插值:
\(I = \alpha I_{a} + \beta I_{b} + \gamma I_{c}\)

\(I = Z(\alpha^{'}\frac{I_{a}}{Z_{a}} + \beta^{'}\frac{I_{b}}{Z_{b}} + \gamma^{'}\frac{I_{c}}{Z_{c}} )\)

games101中的錯誤

有了上述理論基礎,我們再來看看games101中的實現:

auto[alpha, beta, gamma] = computeBarycentric2D(x, y, t.v);
float w_reciprocal = 1.0/(alpha / v[0].w() + beta / v[1].w() + gamma / v[2].w());
float z_interpolated = alpha * v[0].z() / v[0].w() + beta * v[1].z() / v[1].w() + gamma * v[2].z() / v[2].w();

注意在前面:

auto v = t.toVector4();

games101將一個三維向量擴充為四維向量,理論上一個畫素點的座標應該是(x,y,z,w),其中x,y代表投影的xy座標,z代表壓縮之後的z值,一般在[-1,1]或者[0,1]或者[n,f]之間,w一般用於儲存原空間真實的深度值,但是上述擴充預設將w設定為1,w儲存的不是真實的深度值,因此:

float w_reciprocal = 1.0/(alpha / v[0].w() + beta / v[1].w() + gamma / v[2].w());

這一步使用的深度值是錯誤的

假如是正確的,其實這一步得到的w_reciprocal已經是正確的深度矯正值,也不需要在後面再求z值

但是最終結果我們也沒有發現明顯的錯誤,可以認為即使使用錯誤的深度值,對最終結果也影響不大

msaa與ssaa簡要定義

MSAA:多重取樣抗鋸齒是一種選擇性的抗鋸齒技術,它在渲染影像時對特定部分進行多次取樣。通常,它會對幾何邊緣周圍進行多次取樣,以減少鋸齒狀邊緣的出現。
SSAA:超級取樣抗鋸齒是一種全域性的抗鋸齒技術,它透過在整個影像上進行更高解析度的取樣,然後縮放到目標解析度,從而減少鋸齒和增強影像的質量。

games101中ssaa的實現

ssaa實現的是更高解析度的取樣,為了實現這一點我們需要為每個取樣點都維護深度表與顏色表,在對每個取樣點進行覆蓋檢測以及深度檢測之後,將取樣點的顏色進行平均,設定為畫素點顏色:

for(int x=min_x; x<=max_x; x++) {
        for(int y=min_y; y<=max_y; y++) {
            int eid = get_index(x,y)*4;
            for(int k = 0; k < 4; k++){//遍歷畫素的每個樣本
                if(insideTriangle(x+a[k], y+a[k+1], v.data())){
                   //計算重心座標
                   auto[alpha, beta, gamma] = computeBarycentric2D(x+a[k],y+a[k+1], t.v);
                   //矯正深度插值
                   float w_reciprocal = 1.0/(alpha / v[0].w() + beta / v[1].w() + gamma / v[2].w());
                   float z_interpolated = alpha * v[0].z() / v[0].w() + beta * v[1].z() / v[1].w() + gamma * v[2].z() / v[2].w();
                   z_interpolated *= w_reciprocal;
                   //如果此時深度值大於當前儲存深度值,說明被遮擋了,不做處理
                   if (depth_sample[eid + k] < z_interpolated) {
                       continue;
                   }
                   //反之,更新當前深度值,對取樣點進行著色
                    depth_sample[eid + k] = z_interpolated;
                    frame_sample[eid + k] = t.getColor();
                }
            }
            Eigen::Vector3f p;
            p << x, y, 1;
            //平均四個取樣點的顏色,簡單的線性混合
            Eigen::Vector3f color = (frame_sample[eid] + frame_sample[eid + 1] + frame_sample[eid + 2] + frame_sample[eid + 3])/4;
            set_pixel(p, color);
        }
    }

games101中msaa的實現

msaa與ssaa類似,也是對四個取樣點的顏色進行混合,也需要對取樣點進行覆蓋以及深度檢測,不過不同的時,msaa會記錄深度的變化,只有在深度發生變化,認為檢測到邊緣的時候,才會進行shading,並且不需要維護顏色表,減少了時間以及空間開銷:

for(int x=min_x; x<=max_x; x++) {
        for(int y=min_y; y<=max_y; y++) {
            //使用msaa方法,統計畫素覆蓋率
            int eid = get_index(x,y)*4;
            //統計畫素的覆蓋率與深度變化
            float count_coverage = 0,count_depth = 0;
            for(int k = 0; k < 4; k++){//遍歷畫素的每個樣本
                if(insideTriangle(x+a[k], y+a[k+1], v.data())){
                   auto[alpha, beta, gamma] = computeBarycentric2D(x+a[k],y+a[k+1], t.v);
                   float w_reciprocal = 1.0/(alpha / v[0].w() + beta / v[1].w() + gamma / v[2].w());
                   float z_interpolated = alpha * v[0].z() / v[0].w() + beta * v[1].z() / v[1].w() + gamma * v[2].z() / v[2].w();
                   z_interpolated *= w_reciprocal;
                   //如果該取樣點在三角形內,增加覆蓋率的計數
                   count_coverage++;
                    if (depth_buf[eid + k] < z_interpolated) {
                       continue;
                    }
                    //如果該取樣點的深度發生了變化,說明該畫素分佈在邊緣,需要進行抗鋸齒
                    count_depth++;
                    depth_buf[eid + k] = z_interpolated;
                }
            }
            //如果該畫素在邊緣,需要進行抗鋸齒
            if(count_depth > 0){
                int ind = get_index(x,y);
                Eigen::Vector3f p;
                p << x, y, 1;
                //混合顏色
                Eigen::Vector3f color = (count_coverage / 4)*t.getColor() +(1 - count_coverage/4)*frame_buf[ind];
                set_pixel(p, color);
            }
        }
    }