深度插值的差錯原因
當投影的圖形與投影的平面不平行時,這時進行透視投影,從上圖中可以看出,投影平面上的線段時均勻的,但是在原圖形上的線段是非均勻的,這只是一個例子,但也可以看出投影會導致圖形的變形,在我們利用重心座標,進行深度插值時原空間中的重心座標會發生變形,導致我們得到的深度不是正確的,這一點在對紋理座標進行插值時尤其明顯
透視深度插值公式推導
雖然在原空間與投影平面上的三角形可能發生變形,但是它們的重心座標依然滿足一定的關係:
投影平面:
\(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);
}
}
}