【GAMES101】作业3 Pipeline and Shading

一.作业描述

在这里插入图片描述

二.作业解析

在这里插入图片描述
在这里插入图片描述

1. blinn-phong模型

理论参考:Blinn-phong 光照模型
Blinn-phong 光照模型公式为:

漫反射光

漫反射光是物体表面向四面八方反射相同强度的能量,所以:无论从哪个视角看同一个点,都应该呈现出相同的颜色
这一部分考虑了三个影响因素:
1.物体表面着色点能够接受多少光能量?
2.光源发出的光抵达物体表面时衰减了多少?
3.物体表面着色点反射多少能量光给摄像机接收?

(Lambert’s cosine law)朗伯特余弦定律表明:物体表面接收到光的能量与法线方向、光照方向两者夹角的余弦值成正比关系
在这里插入图片描述

可以根据这个定律去计算物体的每一个着色点能够接收多少的光能量:
c o s θ = l ⋅ n cosθ = l · ncosθ=l⋅n
在这里插入图片描述
接下来我们需要知道:从光源到物体表面发射的能量光是多少?

忽略其它能量损失,假设有一个点光源,发射出来的能量光在单位球上的能量表示为 I,同一个点上的能量随着光不断向外传播而逐渐减少, 根据能量守恒定律,每一个球面上的总能量都是相同的
(感觉跟大物里面的高斯定理有点相似之处)
在这里插入图片描述
在这里插入图片描述
最后还需要计算:物体表面着色点反射多少能量光给摄像机接收?
每一个着色点都有不同的光吸收率,这与该着色点的颜色相关。
在这里插入图片描述

高光

计算机图形学中,我们定义向量R是镜面反射方向,当观察方向V与R越接近时,高光部分越明显。
在这里插入图片描述

我们可以通过计算R和V的接近程度来确定看到的高光强度,但是:在计算机中计算反射向量R的计算量非常大。

所以,Blinn-Phong光照模型提出使用另一种计算方法:「半程向量」
在这里插入图片描述

通过上图可以发现,物体表面一点的法线方向正好是光源方向l 和镜面反射方向R 的角平分向量。

同样,我们知道视角V和光源l,可以很容易的计算出它们的角平分向量h,而该向量就称为半程向量。

在这里插入图片描述

环境光

环境光是照射在其它物体上的光先反射到观察物体上,通过观察物体表面再反射至视角中。
在 Blinn-Phong 模型中,举出了一个非常大胆的假设:物体表面接收到的各种环境光都是相同强度的。
由于环境光来自四面八方的物体反射,所以物体表面反射环境光的方向也是四面八方的:
环境光与光源的角度无关,与观察角度也无关,所以它是一个常数。
计算环境光的公式非常简单,只需要得到物体表面的环境光吸收率和环境光的强度即可:
在这里插入图片描述

漫反射+环境光+高光 叠加

在这里插入图片描述
关于详细的代码,我参考了好几篇博客,有的光线和pdf中的效果不同(如下左图是渲染出来的效果,右图是pdf的期望效果,明显右图效果更丝滑一点),总结了一下原因就是半程向量归一化的问题。
在这里插入图片描述在这里插入图片描述

        Eigen::Vector3f light_dir=light.position-point;
        Eigen::Vector3f view_dir=(eye_pos-point).normalized();

        //ambient
        Eigen::Vector3f La=ka.cwiseProduct(amb_light_intensity);
        //diffuse
        float r2=light_dir.dot(light_dir); //此处不能用已经归一化后的lightdir计算
        Eigen::Vector3f Ld=kd.cwiseProduct(light.intensity/r2);
        Ld*=std::max(0.0f,normal.normalized().dot(light_dir.normalized()));
        //specular
        Eigen::Vector3f h=(light_dir.normalized()+view_dir).normalized(); //这里计算lightdir和viewdir需要先归一化再相加
        Eigen::Vector3f Ls=ks.cwiseProduct(light.intensity/r2);
        Ls*=pow(std::max(0.0f,normal.normalized().dot(h)),p);

        result_color+=(La+Ld+Ls);

phong和texture的shader部分代码如下:

//phong shader
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};
    Eigen::Vector3f eye_pos{0, 0, 10};

    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};
    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_dir=light.position-point;
        Eigen::Vector3f view_dir=(eye_pos-point).normalized();

        //ambient
        Eigen::Vector3f La=ka.cwiseProduct(amb_light_intensity);
        //diffuse
        float r2=light_dir.dot(light_dir);
        Eigen::Vector3f Ld=kd.cwiseProduct(light.intensity/r2);
        Ld*=std::max(0.0f,normal.normalized().dot(light_dir.normalized()));
        //specular
        Eigen::Vector3f h=(light_dir.normalized()+view_dir).normalized();
        Eigen::Vector3f Ls=ks.cwiseProduct(light.intensity/r2);
        Ls*=pow(std::max(0.0f,normal.normalized().dot(h)),p);

        result_color+=(La+Ld+Ls);

    }

    return result_color * 255.f;
}


// 纹理shader
Eigen::Vector3f texture_fragment_shader(const fragment_shader_payload& payload)
{
	Eigen::Vector3f return_color = { 0, 0, 0 };
	if (payload.texture)
	{
		// 获取纹理坐标的颜色
		return_color = payload.texture->getColor(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 };
	Eigen::Vector3f ambient = ka * amb_light_intensity[0];

	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_dir=light.position-point;
        Eigen::Vector3f view_dir=(eye_pos-point).normalized();

        //ambient
        Eigen::Vector3f La=ka.cwiseProduct(amb_light_intensity);
        //diffuse
        float r2=light_dir.dot(light_dir); //此处不能用已经归一化后的lightdir计算
        Eigen::Vector3f Ld=kd.cwiseProduct(light.intensity/r2);
        Ld*=std::max(0.0f,normal.normalized().dot(light_dir.normalized()));
        //specular
        Eigen::Vector3f h=(light_dir.normalized()+view_dir).normalized(); //这里计算lightdir和viewdir需要先归一化再相加
        Eigen::Vector3f Ls=ks.cwiseProduct(light.intensity/r2);
        Ls*=pow(std::max(0.0f,normal.normalized().dot(h)),p);

		result_color += (La + Ld + Ls);
	}
	return result_color * 255.f;
}

2. Bump Mapping

参考:Bump Mapping
OpenGL法线贴图
凹凸贴图是纹理的一种应用,它主要用来实现类似砖块、墙体的那种凹凸不平的效果,相较于一般的纹理映射,它并不是通过纹理映射来改变材质本身的颜色,而是改变或扰动其法线的方向,而法线的方向被用在光线模型中,改变法线的方向就可以影响物体表面光照的明暗效果。

通常,每个计算都是在视图空间中完成的,但在凹凸贴图中,来自法线贴图的法线向量在切线空间中表示。因此,我们需要转换所有需要的向量。为了做到这一点,我们使用TBN矩阵:视图空间 -> 切线空间。
在这里插入图片描述
在这里插入图片描述

TNB矩阵
TBN矩阵可以将切线空间转换到世界空间,可以把切线空间的z方向和表面的法线方向对齐
TBN矩阵这三个字母分别代表tangent(切线)、bitangent(副切线)和normal向量。这是建构这个矩阵所需的向量。

已知上向量是表面的法线向量。右和前向量是切线(Tagent)和副切线(Bitangent)向量。下面的图片展示了一个表面的三个向量:
参考来源于:法线贴图
在这里插入图片描述在这里插入图片描述

计算出切线和副切线并不像法线向量那么容易。从图中可以看到法线贴图的切线和副切线与纹理坐标的两个方向对齐。我们就是用到这个特性计算每个表面的切线和副切线的。
在这里插入图片描述

在这里插入图片描述
解出上面的矩阵,就可以得到向量T,B的值。
在这里插入图片描述

要注意的是,运行过程中可能出现segment fault的问题,出现这个问题的原因往往是越界访问,问题出现在getcolor函数中,u,v坐标需要被限制再0-1范围内。

    Eigen::Vector3f getColor(float u, float v)
    {
        //u,v坐标需要被限制再0-1范围内
        if(u<0)u=0;
        if(u>1)u=1;
        if(v<0)v=0;
        if(v>1)v=1;
        auto u_img = u * width;
        auto v_img = (1 - v) * height;
        auto color = image_data.at<cv::Vec3b>(v_img, u_img);
        return Eigen::Vector3f(color[0], color[1], color[2]);
    }
//凹凸贴图shader
Eigen::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};

    float p = 150;

    Eigen::Vector3f color = payload.color; 
    Eigen::Vector3f point = payload.view_pos;
    Eigen::Vector3f normal = payload.normal;

	//kh和kn为在x方向和y方向梯度的一个权重因子
    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)

	float x = normal.x();
	float y = normal.y();
	float z = normal.z();
	Eigen::Vector3f t{ x * y / std::sqrt(x * x + z * z), std::sqrt(x * x + z * z), z*y / std::sqrt(x * x + z * z) };
	Eigen::Vector3f b = normal.cross(t);
	Eigen::Matrix3f TBN;
	TBN << t.x(), b.x(), normal.x(),
		t.y(), b.y(), normal.y(),
		t.z(), b.z(), normal.z();

	float u = payload.tex_coords.x();
	float v = payload.tex_coords.y();
	float w = payload.texture->width;
	float h = payload.texture->height;

	float dU = kh * kn * (payload.texture->getColor(u + 1.0f / w , v).norm() - payload.texture->getColor(u, v).norm());
	float dV = kh * kn * (payload.texture->getColor(u, v + 1.0f / h).norm() - payload.texture->getColor(u, v).norm());

	Eigen::Vector3f ln{ -dU,-dV,1.0f };

	normal = TBN * ln;
	// 归一化
	Eigen::Vector3f result_color = normal.normalized();
    return result_color * 255.f;

}

3. Displacement Mapping

Normal maps的一个应用是displacement mapping,在这个应用中,细节的增加不再是通过虚拟光照得到的,而是真正的改变模型的vertices。使用displacement mapping时,vertex沿着它的法线向量平移,该法线的值由displacement map指定

根据从displacement map中采样的结果,对一个vertex移位时,要么沿着法线向内,要么向外。使用如下的公式执行向外移位:
Position = Position0 + (Normal * Scale * DisplacementMagnitude)

并且displacement在此基础上还要考虑光照因素。

代码如下所示:

//displacement 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};

    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)
	float x = normal.x();
	float y = normal.y();
	float z = normal.z();
	Eigen::Vector3f t{ x * y / std::sqrt(x * x + z * z), std::sqrt(x * x + z * z), z*y / std::sqrt(x * x + z * z) };
	Eigen::Vector3f b = normal.cross(t);
	Eigen::Matrix3f TBN;
	TBN << t.x(), b.x(), normal.x(),
		t.y(), b.y(), normal.y(),
		t.z(), b.z(), normal.z();

	float u = payload.tex_coords.x();
	float v = payload.tex_coords.y();
	float w = payload.texture->width;
	float h = payload.texture->height;

	float dU = kh * kn * (payload.texture->getColor(u + 1.0f / w , v).norm() - payload.texture->getColor(u, v).norm());
	float dV = kh * kn * (payload.texture->getColor(u, v + 1.0f / h).norm() - payload.texture->getColor(u, v).norm());

	Eigen::Vector3f ln{ -dU,-dV,1.0f };
	
	//vertex移位
    point+=(kn*normal*payload.texture->getColor(u,v).norm());

	normal = (TBN * ln).normalized();

	Eigen::Vector3f result_color = {0,0,0};

    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_dir=light.position-point;
        Eigen::Vector3f view_dir=(eye_pos-point).normalized();

        //ambient
        Eigen::Vector3f La=ka.cwiseProduct(amb_light_intensity);
        //diffuse
        float r2=light_dir.dot(light_dir);
        Eigen::Vector3f Ld=kd.cwiseProduct(light.intensity/r2);
        Ld*=std::max(0.0f,normal.normalized().dot(light_dir.normalized()));
        //specular
        Eigen::Vector3f h=(light_dir.normalized()+view_dir).normalized();
        Eigen::Vector3f Ls=ks.cwiseProduct(light.intensity/r2);
        Ls*=pow(std::max(0.0f,normal.normalized().dot(h)),p);

        result_color+=(La+Ld+Ls);

    }

    return result_color * 255.f;
}

4.双线性插值

如下图所示,双线性插值可以分别从两个方向上求出插值的结果。
opencv坐标的高和uv坐标和高是相反的,即opencv图像坐标的y坐标是逐渐增大,而对应的uv坐标下的v坐标是逐渐减小,所以计算中心点的邻域点时,也要注意v_min实际上是在图像中中心点的上面,而v_max是在图像中中心点的下面。请添加图片描述

    Eigen::Vector3f getColorBilinear(float u, float v){
        if(u<0)u=0;
        if(u>1)u=1;
        if(v<0)v=0;
        if(v>1)v=1;
        auto u_img = u * width;
        auto v_img = (1 - v) * height;
        float u_min=std::floor(u_img);
        float u_max=std::min((float)width,std::ceil(u_img));
        float v_min=std::floor(v_img);
        float v_max=std::min((float)height,std::ceil(v_img));

        auto p11 = image_data.at<cv::Vec3b>(v_min, u_min);
        auto p12 = image_data.at<cv::Vec3b>(v_min, u_max);
        auto p21 = image_data.at<cv::Vec3b>(v_max, u_min);
        auto p22 = image_data.at<cv::Vec3b>(v_max, u_max);

        float u_rate=(u_img-u_min)/(u_max-u_min);
        float v_rate=(v_img-v_max)/(v_min-v_max);

        auto c1=u_rate*p12+(1-u_rate)*p11;
        auto c2=u_rate*p22+(1-u_rate)*p21;
        auto color=v_rate*c1+(1-v_rate)*c2;

        return Eigen::Vector3f(color[0], color[1], color[2]);
    }

进行双线性插值前后效果对比如下。如果双线性插值的函数写的有问题,渲染出来的小牛身上就会出现摩尔纹= = 。
在这里插入图片描述在这里插入图片描述

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值