透視矯正插值
首先關注一下作業2中的插值公式,對深度插值時這裏有一個問題就是透視投影的矯正。因爲屏幕上的點的插值結果並不等於三維空間中的插值結果,我們應該先在三維空間中插完值再進行透視投影。但是有沒有辦法通過屏幕座標得到三維空間對應的插值結果呢。
網上找了一些教程都是從直線的重心座標類比過去(想推一下怎麼從三角形的重心座標表示)
我們想得到的是s和t之間的關係,這樣就可以直接通過屏幕座標的重心座標插值三維空間中的結果。問題描述爲上圖,(s和t都是直線的重心座標表示,然後A、B、C是view space(視錐)下的點)
由相似三角形可得:
同樣,分別利用screen space 以及 view space的線性插值可以得到以下幾個式子:
(4)(5)帶進(3):
再將(1)(2)代入(7):
再將(6)代入(8):
最終得到t與s關係式:
一般只用上面的式子,但下邊的比較好記我也寫了一下。
這樣的話,就可以利用屏幕空間下的係數得到正確的插值結果:
類比到三角形的重心座標插值,可以推出:
同理,針對任意屬性I(法線向量、紋理座標、View Space座標):
下面分母\({\frac{s}{Z_2}+\frac{1-s}{Z_1}}\)是\(Z_t\)(見式(9))的倒數。所以$$I_t=\frac{\frac{I_1}{Z_1}+s(\frac{I_2}{Z_2}-\frac{I_1}{Z_1})}{\frac{1}{Z_t}}$$
類比到三角形中:
其中$$Z_t=\frac{1}{\frac{\alpha}{Z_A}+\frac{\beta}{Z_B}+\frac{\gamma}{Z_C}}$$
A B C都是三維空間中的值,\(\alpha\)、\(\beta\)、\(\gamma\)是在屏幕座標下的重心座標,這裏\(I_A\)很奇怪,針對Z的時候代碼裏寫的也是屏幕座標系下的z
這樣就能夠從屏幕空間正確插值三維空間中的值了。
參考:https://zhuanlan.zhihu.com/p/144331875
請看這個:https://blog.csdn.net/Q_pril/article/details/123598746
這個公式推導部分寫的特別好,當然裏面其他部分感覺也有一些問題【也可能是我的問題】】!!!
代碼
渲染管線整個框架:讀取頂點->頂點着色->面元組裝->光柵化->片段着色->深度測試裝填到buffer中
這裏通過使用不同的片段着色器得到blinn-phong 法線 凹凸貼圖 位移貼圖的結果。
根據上面推得公式,可以寫出一個從屏幕空間的重心座標插值三維空間(一般是相機座標系下也就是view space,經過MV未經過P投影)中屬性的插值函數,原框架的插值函數也是一個近似,可以看到跟公式相比的話少除了一個Z。自己按公式實現了interpolate_3d
函數
【當然這裏看論壇裏也有人說可能是w-buffer這個概念:https://developer.aliyun.com/article/49272,有時間可以看一下】
// 這裏面的weight一般是1/Zt
static Eigen::Vector3f interpolate(float alpha, float beta, float gamma, const Eigen::Vector3f& vert1, const Eigen::Vector3f& vert2, const Eigen::Vector3f& vert3, float weight)
{
return (alpha * vert1 + beta * vert2 + gamma * vert3) / weight;
}
static Eigen::Vector2f interpolate(float alpha, float beta, float gamma, const Eigen::Vector2f& vert1, const Eigen::Vector2f& vert2, const Eigen::Vector2f& vert3, float weight)
{
auto u = (alpha * vert1[0] + beta * vert2[0] + gamma * vert3[0]);
auto v = (alpha * vert1[1] + beta * vert2[1] + gamma * vert3[1]);
u /= weight;
v /= weight;
return Eigen::Vector2f(u, v);
}
static Eigen::Vector3f interpolate_3d(float alpha, float beta, float gamma, const Eigen::Vector3f& vert1, const Eigen::Vector3f& vert2, const Eigen::Vector3f& vert3, float weight,float Za,float Zb,float Zc)
{
return (alpha * vert1 / Za + beta * vert2 / Zb + gamma * vert3 / Zc) / weight;
}
因爲在原框架中,這裏toVector4設置w維是1.f,所以其實丟失了MV之後的z值。
std::array<Vector4f, 3> Triangle::toVector4() const
{
std::array<Vector4f, 3> res;
std::transform(std::begin(v), std::end(v), res.begin(), [](auto& vec) { return Vector4f(vec.x(), vec.y(), vec.z(), 1.f); });
return res;
}
//Screen space rasterization
void rst::rasterizer::rasterize_triangle(const Triangle& t, const std::array<Eigen::Vector3f, 3>& view_pos)
{
// 計算三角形BB
auto v = t.toVector4();
float x_max=v.at(0)(0);
float x_min=v.at(0)(0);
float y_max=v.at(0)(1);
float y_min=v.at(0)(1);
for(auto vv:v){
if(vv(0)>x_max){
x_max=vv(0);
}
if(vv(0)<x_min){
x_min=vv(0);
}
if(vv(1)>y_max){
y_max=vv(1);
}
if(vv(1)<y_min){
y_min=vv(1);
}
}
// 遍歷每一個像素是不是在三角形中
for(int x=x_min;x<=x_max;x++){
for(int y=y_min;y<=y_max;y++){
// 如果在 插值深度 進行深度測試 插值顏色 法線 紋理
if(insideTriangle(x,y,t.v)){
// 插值深度
auto [alpha,beta,gamma]=computeBarycentric2D(x,y,t.v);
// 因爲在project過程中,第四維的係數是[0 0 1 0],在後續平移和縮放中也沒有對第四維的操作,w其實保留了相機座標系(view space)下的z值 但是t.toVector4()這個算法重新將w設置爲了1;所以下式Z=1
// 原框架代碼 下式Z=1
// float Z = 1.0 / (alpha / v[0].w() + beta / v[1].w() + gamma / v[2].w());
// 近似,使用二維插值近似三維
// float zp = alpha * v[0].z() / v[0].w() + beta * v[1].z() / v[1].w() + gamma * v[2].z() / v[2].w();
// float zp *= Z;
// 自我實現插值三維 這個是按照公式來的
float Z_3d = 1.0 / (alpha / view_pos[0].z() + beta / view_pos[1].z() + gamma / view_pos[2].z());
float zp=Z_3d;
int depth_index=get_index(x,y);
if(depth_buf[depth_index]<zp||depth_buf[depth_index]==std::numeric_limits<float>::infinity()){
// 渲染
Eigen::Vector3f point(x,y,zp);
// set_pixel(point,t.getColor());
// Eigen::Vector3f interpolated_color=interpolate(alpha,beta,gamma,t.color[0],t.color[1],t.color[2],1.0);
// Eigen::Vector3f interpolated_normal=interpolate(alpha,beta,gamma,t.normal[0],t.normal[1],t.normal[2],1.0);
// Eigen::Vector2f interpolated_texcoords=interpolate(alpha,beta,gamma,t.tex_coords[0],t.tex_coords[1],t.tex_coords[2],1.0);
// Eigen::Vector3f interpolated_shadingcoords=interpolate(alpha,beta,gamma,view_pos[0],view_pos[1],view_pos[2],1.0);//在這裏在相機座標系下插值需要渲染點的位置
Eigen::Vector3f interpolated_color=interpolate_3d(alpha,beta,gamma,t.color[0],t.color[1],t.color[2],1.0/Z_3d,view_pos[0].z(),view_pos[1].z(),view_pos[2].z());
Eigen::Vector3f interpolated_normal=interpolate_3d(alpha,beta,gamma,t.normal[0],t.normal[1],t.normal[2],1.0/Z_3d,view_pos[0].z(),view_pos[1].z(),view_pos[2].z());
Eigen::Vector2f interpolated_texcoords=interpolate(alpha,beta,gamma,t.tex_coords[0],t.tex_coords[1],t.tex_coords[2],1.0);
Eigen::Vector3f interpolated_shadingcoords=interpolate_3d(alpha,beta,gamma,view_pos[0],view_pos[1],view_pos[2],1.0/Z_3d,view_pos[0].z(),view_pos[1].z(),view_pos[2].z());//在相機座標系下需要渲染點的位置的插值 但是在後面的光線計算的時候使用的卻是世界座標系下的位置
fragment_shader_payload payload( interpolated_color, interpolated_normal.normalized(), interpolated_texcoords, texture ? &*texture : nullptr);
payload.view_pos = interpolated_shadingcoords;
auto pixel_color = fragment_shader(payload);
set_pixel(Eigen::Vector2i(x,y),pixel_color);
// 更新zBuffer
depth_buf[depth_index]=zp;
}
}
}
}
}
phong_fragment_shader
- 這裏給出的代碼註釋裏計算環境光寫在了遍歷每一條光線的時候,我想了想也對比了結果,環境光應該寫在遍歷外面,即沒有光線的時候也得有環境光。
- 高光記得做指數運算,半程向量計算的時候的正則化,不正則化的話和最終結果會有偏差,高光會有些發散的感覺。
- 在進行光照計算的時候,應該是在相機座標系下,因爲在之前傳進來的渲染點參數插值的是view space座標系下的結果。所以eye_pos和light的位置甚至Normal感覺都應該是view space下的才合理。
Eigen::Vector3f phong_fragment_shader(const fragment_shader_payload& payload)
{
Eigen::Vector3f ka = Eigen::Vector3f(0.005, 0.005, 0.005);
Eigen::Vector3f kd = payload.color;
Eigen::Vector3f ks = Eigen::Vector3f(0.7937, 0.7937, 0.7937);
auto l1 = light{{20, 20, 20}, {500, 500, 500}};
auto l2 = light{{-20, 20, 0}, {500, 500, 500}};
std::vector<light> lights = {l1, l2};
Eigen::Vector3f amb_light_intensity{10, 10, 10};
// 傳進來的參數是view space空間下的插值 這裏的eye_pos應該在原點
// Eigen::Vector3f eye_pos{0, 0, 10};
Eigen::Vector3f eye_pos{0, 0, 0};
float p = 150;
Eigen::Vector3f color = payload.color;
Eigen::Vector3f point = payload.view_pos;//需要渲染的點
Eigen::Vector3f normal = payload.normal;
Eigen::Vector3f result_color = {0, 0, 0};
// 環境光
Eigen::Vector3f L_a=ka.cwiseProduct(amb_light_intensity);
result_color+=L_a;
for (auto& light : lights)
{
// TODO: For each light source in the code, calculate what the *ambient*, *diffuse*, and *specular* 【每一條光源都要計算環境光??】
// components are. Then, accumulate that result on the *result_color* object.
// 每一條光線的漫反射
Eigen::Vector3f light_viewpos=light.position-point;//渲染點指向光源
float d_wight=normal.normalized().dot(light_viewpos.normalized());//光源與點法線的夾角
Eigen::Vector3f L_d(0,0,0);
if(d_wight>0){
// 計算光源到渲染點的距離
float dis=(light_viewpos).dot(light_viewpos);
L_d=kd.cwiseProduct(light.intensity/dis)*d_wight;
result_color+=L_d;
}
// 每一條光線的鏡面反射
// 半程向量
Eigen::Vector3f eyepos_viewpos=eye_pos-point;//渲染點指向視點
Eigen::Vector3f h=(eyepos_viewpos.normalized()+light_viewpos.normalized()).normalized();
float s_weight=normal.normalized().dot(h);
Eigen::Vector3f L_s(0,0,0);
if(s_weight>0){
float dis=(light_viewpos).dot(light_viewpos);
s_weight=pow(s_weight,p);
L_s=ks.cwiseProduct(light.intensity/dis)*s_weight;
result_color+=L_s;
}
}
return result_color * 255.f;
}
normal_fragment_shader
這個是作業本身實現好的
Eigen::Vector3f normal_fragment_shader(const fragment_shader_payload& payload)
{
Eigen::Vector3f return_color = (payload.normal.head<3>().normalized() + Eigen::Vector3f(1.0f, 1.0f, 1.0f)) / 2.f;
Eigen::Vector3f result;
result << return_color.x() * 255, return_color.y() * 255, return_color.z() * 255;
return result;
}
結果:
texture_fragment_shader
這裏面主要是blinn-phong模型中的kd變爲了從紋理貼圖中讀取。
Eigen::Vector3f texture_fragment_shader(const fragment_shader_payload& payload)
{
Eigen::Vector3f return_color = {0, 0, 0};
if (payload.texture)
{
// TODO: Get the texture value at the texture coordinates of the current fragment
return_color=payload.texture->getColor(payload.tex_coords.x(),payload.tex_coords.y());
// return_color=payload.texture->getColorBilinear(payload.tex_coords.x(),payload.tex_coords.y());
}
Eigen::Vector3f texture_color;
texture_color << return_color.x(), return_color.y(), return_color.z();
Eigen::Vector3f ka = Eigen::Vector3f(0.005, 0.005, 0.005);
Eigen::Vector3f kd = texture_color / 255.f;
Eigen::Vector3f ks = Eigen::Vector3f(0.7937, 0.7937, 0.7937);
auto l1 = light{{20, 20, 20}, {500, 500, 500}};
auto l2 = light{{-20, 20, 0}, {500, 500, 500}};
std::vector<light> lights = {l1, l2};
Eigen::Vector3f amb_light_intensity{10, 10, 10};
Eigen::Vector3f eye_pos{0, 0, 10};
float p = 150;
Eigen::Vector3f color = texture_color;
Eigen::Vector3f point = payload.view_pos;
Eigen::Vector3f normal = payload.normal;
Eigen::Vector3f result_color = {0, 0, 0};
// 環境光
auto L_a=ka.cwiseProduct(amb_light_intensity);
result_color+=L_a;
for (auto& light : lights)
{
// TODO: For each light source in the code, calculate what the *ambient*, *diffuse*, and *specular*
// components are. Then, accumulate that result on the *result_color* object.
auto light_dir=light.position-point;//渲染點指向光源
// 衰減因子
auto dis=light_dir.dot(light_dir);
// 漫反射
// 光和法線夾角
auto d_wight=light_dir.normalized().dot(normal.normalized());
if(d_wight>0){
auto L_d=kd.cwiseProduct(light.intensity/dis)*d_wight;
result_color+=L_d;
}
// 高光
// 眼睛和渲染點
auto eye_dir=eye_pos-point;//渲染點指向眼睛
// 注意這裏都需要先normalized再normalized
auto h=(light_dir.normalized()+eye_dir.normalized()).normalized();//半程向量
// 半程向量和法線夾角
auto s_wight=normal.normalized().dot(h);
if(s_wight>0){
s_wight=pow(s_wight,p);
auto L_s=ks.cwiseProduct(light.intensity/dis)*s_wight;
result_color+=L_s;
}
}
return result_color * 255.f;
}
運行結果:
雙線性插值
// 雙線性插值
Eigen::Vector3f getColorBilinear(float u, float v)
{
// 這是一開始我寫的,後來感覺應該是+-0.5
// 但是+-0.5是有問題的因爲最後取得是int值 效果沒有這個明顯同時和不線性插值是差不多的
int u1_img=u*width;
int u2_img=u1_img+1;
float w_u=u*width-u1_img;
int v1_img=(1-v)*height;
int v2_img=v1_img+1;
float w_v=(1-v)*height-v1_img;
// float u_img=u*width;
// float u1_img=u_img-0.5;
// if(u1_img<0){
// u1_img=0;
// }
// float u2_img=u_img+0.5;
// if(u2_img>=width){
// u2_img=width-1;
// }
// float w_u=(u_img-u1_img)/(u2_img-u1_img);
// float v_img=(1-v)*height;
// float v1_img=v_img-0.5;
// if(v1_img<0){
// v1_img=0;
// }
// float v2_img=v_img+0.5;
// if(v2_img>=height){
// v2_img=height-1;
// }
// float w_v=(v_img-v1_img)/(v2_img-v1_img);
auto color1=image_data.at<cv::Vec3b>(v1_img,u1_img);
auto color2=image_data.at<cv::Vec3b>(v1_img,u2_img);
auto color3=image_data.at<cv::Vec3b>(v2_img,u1_img);
auto color4=image_data.at<cv::Vec3b>(v2_img,u2_img);
// u方向插值
auto color_1=color1+w_u*(color2-color1);
auto color_2=color3+w_u*(color4-color3);
// v方向插值
auto color=color_1+w_v*(color_2-color_1);
return Eigen::Vector3f(color[0], color[1], color[2]);
}
觀察鼻孔,左圖爲原來直接取顏色的,中間是第一種的雙線性插值結果效果比較好,右邊是第二種+-0.5的感覺和直接取顏色的差別不大。【嘶 這裏感覺也有點問題,沒有找到文件中給出的小紋理貼圖,這麼看來驗證雙線性插值是不是不太好】
bump_fragment_shader
bump是凹凸貼圖,主要是改變法線(其實就是要渲染的顏色值),但是並不對頂點做位移。建立在blinn-phong模型下,是通過blinn-phong光照計算中改變法線的值來做到凹凸的結果。
這裏面有一個注意點是,我們輸入的normal是view space下的,然後通過紋理上記錄的對應高度微小變化求出在紋理座標系下的新法線。但是新法線並不是view space下的,確實一個變換。我們將這個變換記爲TBN。問題就變爲了TBN怎麼求,TBN是view space,正常情況下是通過三角形頂點之間的變換可以得到,但是這裏是只有一個點,感覺又進行了近似。
更詳細的內容可以參考:https://zhuanlan.zhihu.com/p/144357517
因爲這裏求不出文章https://zhuanlan.zhihu.com/p/144357517中的[e1,e2]【只有一個點】,所以代碼裏直接這一項爲[1,1]。
igen::Vector3f bump_fragment_shader(const fragment_shader_payload& payload)
{
Eigen::Vector3f ka = Eigen::Vector3f(0.005, 0.005, 0.005);
Eigen::Vector3f kd = payload.color;
Eigen::Vector3f ks = Eigen::Vector3f(0.7937, 0.7937, 0.7937);
auto l1 = light{{20, 20, 20}, {500, 500, 500}};
auto l2 = light{{-20, 20, 0}, {500, 500, 500}};
std::vector<light> lights = {l1, l2};
Eigen::Vector3f amb_light_intensity{10, 10, 10};
// Eigen::Vector3f eye_pos{0, 0, 10};
Eigen::Vector3f eye_pos{0, 0, 0};
float p = 150;
Eigen::Vector3f color = payload.color;
Eigen::Vector3f point = payload.view_pos;
Eigen::Vector3f normal = payload.normal;
float kh = 0.2, kn = 0.1;
// TODO: Implement bump mapping here
// Let n = normal = (x, y, z)
// Vector t = (x*y/sqrt(x*x+z*z),sqrt(x*x+z*z),z*y/sqrt(x*x+z*z))
// Vector b = n cross product t
// Matrix TBN = [t b n]
// dU = kh * kn * (h(u+1/w,v)-h(u,v))
// dV = kh * kn * (h(u,v+1/h)-h(u,v))
// Vector ln = (-dU, -dV, 1)
// Normal n = normalize(TBN * ln)
// bump是凹凸貼圖,改變法線但是不改變頂點位置
// 我們的法線是Viewspace下的
float x=normal.x();
float y=normal.y();
float z=normal.z();
// 近似
Eigen::Vector3f t(x*y/sqrt(x*x+z*z),sqrt(x*x+z*z),z*y/sqrt(x*x+z*z));
Eigen::Vector3f b=normal.cross(t);
Eigen::Matrix3f TBN=Eigen::Matrix3f::Identity();
TBN.block(0,0,3,1)=t;
TBN.block(0,1,3,1)=b;
TBN.block(0,2,3,1)=normal;
auto u=payload.tex_coords.x();
auto v=payload.tex_coords.y();
auto w=payload.texture->width;
auto h=payload.texture->height;
// 此時紋理的值認爲記錄了一個相對高度,然後求解(dp/du,dp/dv)
auto dU = kh * kn * (payload.texture->getColor(u+1.0/w,v).norm()-payload.texture->getColor(u,v).norm());
auto dV = kh * kn * (payload.texture->getColor(u,v+1.0/h).norm()-payload.texture->getColor(u,v).norm());
// 切線變爲法線,下面是求高度變化後的法線。
Eigen::Vector3f ln=Eigen::Vector3f(-dU,-dV,1.0f).normalized();
// 上面這個ln法線是在切線空間,也就是[u,v,1]這個空間下的。TBN定義了切線空間和相機(世界)座標系之間的變換。
normal=(TBN*ln).normalized();
Eigen::Vector3f result_color = {0, 0, 0};
result_color = normal;
return result_color * 255.f;
}
結果:
displacement_fragment_shader
位移貼圖是改變法線的同時,也改變頂點的位置。
Eigen::Vector3f displacement_fragment_shader(const fragment_shader_payload& payload)
{
Eigen::Vector3f ka = Eigen::Vector3f(0.005, 0.005, 0.005);
Eigen::Vector3f kd = payload.color;
Eigen::Vector3f ks = Eigen::Vector3f(0.7937, 0.7937, 0.7937);
auto l1 = light{{20, 20, 20}, {500, 500, 500}};
auto l2 = light{{-20, 20, 0}, {500, 500, 500}};
std::vector<light> lights = {l1, l2};
Eigen::Vector3f amb_light_intensity{10, 10, 10};
// Eigen::Vector3f eye_pos{0, 0, 10};
Eigen::Vector3f eye_pos{0, 0, 0};
float p = 150;
Eigen::Vector3f color = payload.color;
Eigen::Vector3f point = payload.view_pos;
Eigen::Vector3f normal = payload.normal;
float kh = 0.2, kn = 0.1;
// TODO: Implement displacement mapping here
// Let n = normal = (x, y, z)
// Vector t = (x*y/sqrt(x*x+z*z),sqrt(x*x+z*z),z*y/sqrt(x*x+z*z))
// Vector b = n cross product t
// Matrix TBN = [t b n]
// dU = kh * kn * (h(u+1/w,v)-h(u,v))
// dV = kh * kn * (h(u,v+1/h)-h(u,v))
// Vector ln = (-dU, -dV, 1)
// Position p = p + kn * n * h(u,v)
// Normal n = normalize(TBN * ln)
// displace是位移貼圖,改變法線的同時改變頂點位置
float x=normal.x();
float y=normal.y();
float z=normal.z();
Eigen::Vector3f t(x*y/sqrt(x*x+z*z),sqrt(x*x+z*z),z*y/sqrt(x*x+z*z));
Eigen::Vector3f b=normal.cross(t);
Eigen::Matrix3f TBN=Eigen::Matrix3f::Identity();
TBN.block(0,0,3,1)=t;
TBN.block(0,1,3,1)=b;
TBN.block(0,2,3,1)=normal;
auto u=payload.tex_coords.x();
auto v=payload.tex_coords.y();
auto w=payload.texture->width;
auto h=payload.texture->height;
// 沿紋理的微小移動
auto dU = kh * kn * (payload.texture->getColor(u+1.0/w,v).norm()-payload.texture->getColor(u,v).norm());
auto dV = kh * kn * (payload.texture->getColor(u,v+1.0/h).norm()-payload.texture->getColor(u,v).norm());
Eigen::Vector3f ln(-dU,-dV,1.0f);
// 頂點移動相對的高度
point=point+kn*normal*payload.texture->getColor(u,v).norm();
// 更新法線
normal=(TBN*ln).normalized();
Eigen::Vector3f result_color = {0, 0, 0};
//環境光
auto L_a=ka.cwiseProduct(amb_light_intensity);
result_color+=L_a;
for (auto& light : lights)
{
// TODO: For each light source in the code, calculate what the *ambient*, *diffuse*, and *specular*
// components are. Then, accumulate that result on the *result_color* object.
// 漫反射
auto light_dir=light.position-point;//點指向光源,此時點的位置已經發生變化
// 衰變因子
float dis=light_dir.dot(light_dir);
float d_wight=light_dir.normalized().dot(normal.normalized());
if(d_wight>0){
auto L_d=kd.cwiseProduct(light.intensity/dis)*d_wight;
result_color+=L_d;
}
// 鏡面反射
auto eye_dir=eye_pos-point;
auto h=(eye_dir.normalized()+light_dir.normalized()).normalized();
auto s_weight=normal.normalized().dot(h);
if(s_weight>0){
s_weight=pow(s_weight,p);
auto L_s=ks.cwiseProduct(light.intensity/dis)*s_weight;
result_color+=L_s;
}
}
return result_color * 255.f;
}
結果:
總結
這裏面有很多近似的地方,結果出來是有差別的,但是差別並不大。重點還是關注流程和框架吧。