透视矫正插值
首先关注一下作业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;
}
结果:
总结
这里面有很多近似的地方,结果出来是有差别的,但是差别并不大。重点还是关注流程和框架吧。