计算机图形学--笔记2

前面已经做了模型变换、视图变换、投影变换、视口变换和光栅化,然后物体还缺少了光照、颜色和阴影…

7 着色

着色一般指在图形或表格中利用平行线、色块来绘制明暗、颜色信息。

在图形学中,着色表示将材质应用到物体上,使物体得到不同的外观。

布林冯模型——简化的光照模型

在一个简化的场景下,只考虑一个物体,考虑物体表面某一点周围的单位面积的漫反射、高光、环境光。

在这个点(shading point)为原点的局部坐标系内,定义光线方向 l 法向量 n 观察方向 v

  • 漫反射

    假设光源往四周均匀的发射能量,初始光照强度 I

    假设接收光照的物体表面的一点,与光源的距离为 r ,那么

    假设光线垂直打到单位面积上,物体表面发亮,这一点接收的光照强度为 I/r^2

    当法向量 n 与光线方向 L 有一定夹角 θ 时,这一点就只有 cosθ 倍原先的光照强度(对比光线垂直打在物体表面)

    最终推导出 L d = k d ( I / r 2 ) m a x ( 0 , n ⃗ ⋅ l ⃗ ) \mathbf{L}_d=k_d(I/r^2)max(0,\vec{n}·\vec{l}) Ld=kd(I/r2)max(0,n l )

    漫反射系数 k_d 代表不同物体表面对漫反射光的吸收
    在这里插入图片描述

    		Eigen::Vector3f light_vec = (light.position - point);
            Eigen::Vector3f normal_eye_vec = (eye_pos - point).normalized();
            float r_2 = light_vec.dot(light_vec);
            light_vec = light_vec.normalized();
            
            auto n_dot_l = normal.dot(light_vec);
            n_dot_l = n_dot_l <0 ? 0 : n_dot_l;
            
            Eigen::Vector3f light_diffuse = kd.cwiseProduct(light.intensity/r_2)*n_dot_l;
    
  • 高光

    高光可以近似成镜面反射的光,当观察方向 v 与反射光方向 h 十分接近时,就能看到明亮的高光部分

在这里插入图片描述

借助半程向量来计算 $\mathbf{h}=\frac{\vec{v}+\vec{l}}{||\vec{v}+\vec{l}||}$

$\mathbf{L}_s=k_s(I/r^2)max(0,cos\alpha)^p = k_s(I/r^2)max(0,\vec{n}·\vec{h})^p$

最后一项 cosα 计算 p 次方,因为一旦观察方向偏离反射光方向,接收的高光应该急剧衰减

p 一般 > 100

```cpp
				auto n_dot_h = normal.dot((normal_eye_vec+light_vec).normalized());
        n_dot_h = n_dot_h <0 ? 0 : n_dot_h;
        Eigen::Vector3f light_specular = ks.cwiseProduct(light.intensity/r_2)*pow(n_dot_h,p);
```
  • 环境光

    近似看成任意方向上,都有一定光照强度的光 L a = k a I a \mathbf{L}_a=k_aI_a La=kaIa

    Eigen::Vector3f light_ambient = ka.cwiseProduct(amb_light_intensity);

最终要把光照结果累加 L = L d + L s + L a \mathbf{L}=\mathbf{L}_d+\mathbf{L}_s+\mathbf{L}_a L=Ld+Ls+La

8 着色频率

如何应用着色?把着色应用在哪些像素点上?

  • 三角形着色——一个三角形看作整体,找三角形面的法线向量应用着色

  • 顶点着色——将三角形的三个顶点的法向量,分别应用着色,每个三角形都利用顶点颜色插值计算内部点的颜色

  • 像素着色——三角形内的每一个像素的法向量,都应用着色,每个三角形都利用顶点法线插值计算内部点的法线**

顶点的法向量怎么求?——利用周围三角形面的法向量,求平均值

如何插值计算三角形内部点的法线?——通过三角形的重心坐标来做插值

				// 通过插值计算三角形内部像素点的 z 值
                float alpha,beta,gamma;
                // 重心坐标下任意点 (x,y) 可以表示为 (α,β,γ) 且αA+βB+γC=(x,y) α+β+γ=1
                std::tie(alpha, beta, gamma) = computeBarycentric2D(x+0.5, y+0.5, t.v);
                // w_reciprocal is interpolated view space depth for the current pixel
                // v[i].w() is the vertex view space depth value z.
                // 当前像素对应视图空间下的深度值怎么算的?参考 https://zhuanlan.zhihu.com/p/144331875
                float w_reciprocal = 1.0/(alpha / v[0].w() + beta / v[1].w() + gamma / v[2].w());
                // z_interpolated is depth between zNear and zFar, used for z-buffer
                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;
                // 之前定义了 z 值越小,越靠近相机,深度缓冲需要保存离相机最近的 z 值
                if(z_interpolated<depth_buf[get_index(x,y)])
                {
                    // Interpolate the attributes:
                    Eigen::Vector3f interpolated_color = (alpha * t.color[0] / v[0].w() + beta * t.color[1] / v[1].w() + gamma * t.color[2] / v[2].w())*w_reciprocal;
                    
                    Eigen::Vector3f interpolated_normal = (alpha * t.normal[0] / v[0].w() + beta * t.normal[1] / v[1].w() + gamma * t.normal[2] / v[2].w())*w_reciprocal;
                    // tex_coords 是二维向量
                    Eigen::Vector2f interpolated_texcoords = (alpha * t.tex_coords[0] / v[0].w() + beta * t.tex_coords[1] / v[1].w() + gamma * t.tex_coords[2] / v[2].w())*w_reciprocal;

                    // 插值计算三角形内部点(要着色的点 )对应的视图空间的坐标
                    // https://games-cn.org/forums/topic/zuoye3-interpolated_shadingcoords/
                    // 用2D空间的值去插值计算3D空间中的光照点的坐标,会有误差
                    Eigen::Vector3f interpolated_shadingcoords = (alpha * view_pos[0] / v[0].w() + beta * view_pos[1] / v[1].w() + gamma * view_pos[2] / v[2].w())*w_reciprocal;

                    fragment_shader_payload payload(interpolated_color, interpolated_normal.normalized(), interpolated_texcoords, texture ? &*texture : nullptr);
                    // 在视图空间时,光线照射的点
                    payload.view_pos = interpolated_shadingcoords;

                    // Instead of passing the triangle's color directly to the frame buffer, pass the color to the shaders first to get the final color;
                    auto pixel_color = fragment_shader(payload);
                    
                    set_pixel(Vector2i(x,y),pixel_color);
                    depth_buf[get_index(x,y)] = z_interpolated;
                }
图形管线

从三维空间中物体的顶点开始,最终生成屏幕上的物体,这一个渲染过程就是图形管线。

shader 指就是渲染流水线中顶点处理、片段处理的阶段。

纹理映射

物体不同的顶点位置应该有不同的颜色,能否把这些信息保存在一张二维图片上?这就是纹理映射。

日常生活中的地球仪就是这样,一个球体贴上图片,表示地球的各个位置。

纹理坐标、UV 坐标,描述物体不同位置应该是怎样的颜色(或其他信息),三角形每个顶点对应一个 UV 坐标。

重心坐标

对于三角形内部点的纹理坐标UV、颜色、顶点法线,我们希望能在三角形的三个顶点之间平滑过渡,需要利用重心坐标来进行插值计算

A、B、C为三角形顶点,三角形组成的平面内的任意点的位置(x,y),可以都可以表示为 (α,β,γ) ,(1/3,1/3,1/3) 恰好就是重心
{ ( x , y ) = α A + β B + γ C α + β + γ = 1 ( α > 0 , β > 0 , γ > 0 时,该点在三角形内部 ) \begin{cases} (x,y)=\alpha{A}+\beta{B}+\gamma{C}\\ \alpha+\beta+\gamma=1\quad(\alpha>0,\beta>0,\gamma>0时,该点在三角形内部) \end{cases} {(x,y)=αA+βB+γCα+β+γ=1(α>0,β>0,γ>0时,该点在三角形内部)
重心坐标可以通过面积比计算出来,将三个顶点和重心连线组成三个小三角形,∠A 对应的小三角形面积就是 S A S_A SA

α = S A S A + S B + S C β = S B S A + S B + S C γ = S C S A + S B + S C \alpha=\frac{S_A}{S_A+S_B+S_C}\\ \beta=\frac{S_B}{S_A+S_B+S_C}\\ \gamma=\frac{S_C}{S_A+S_B+S_C} α=SA+SB+SCSAβ=SA+SB+SCSBγ=SA+SB+SCSC
因此,三角形内部任意点的任何属性,都可以通过三个顶点对应的属性进行插值得到 V = α V A + β V B + γ V C V=\alpha{V}_A+\beta{V}_B+\gamma{V}_C V=αVA+βVB+γVC

重心坐标的注意事项——在投影后,三角形内任意点的坐标可能发生改变。所以要做插值,应该在三维空间物体投影前做计算。误差矫正

纹理贴图

原本我们将屏幕上的三角形顶点坐标映射到纹理坐标UV,现在利用重心坐标做插值计算,又能求出三角形内任意点的UV坐标。

用二维图形表示这些信息,就是纹理贴图,一个坐(u,v)标下的纹理像素可以对应物体光栅化之后(x,y)位置上一点的颜色。

在纹理贴图上,根据顶点对应的UV坐标,就找到了顶点的颜色信息(例如布林冯模型中的漫反射系数Kd)等。

问题1

纹理太小,生成图像太大,那么图像的多个像素计算UV坐标时,就可能产生小数,四舍五入后,多个像素映射到同一个UV坐标的纹理像素上,图像就变模糊了

双线性插值

为了解决纹理太小的问题,通过双线性插值(点查询),计算出像素对应的UV坐标周围的纹理像素之间的过渡颜色

在这里插入图片描述

红点是像素点,黑点是纹理贴图的纹理像素

在水平方向的两对点做插值,得到一对新的点(与红点在一条垂直线上),这一对点再做一次插值就能得到红点对应的颜色,并且是综合了周围四个黑点得出的颜色

问题2

纹理太大,在透视投影后生成的图像中,远处的像素点能覆盖到纹理贴图的多个纹素,产生摩尔纹,近处出现锯齿。

解决办法

使用超采样——一个像素点采样多次——改善了摩尔纹和锯齿,但还是效果一般

问题就在于,我们使用一个像素点(采样频率低)去表示多个纹素的信息(信号频率高),产生走样。那么,能否不采样?给出像素点,马上能找到对应UV坐标附近的多个纹素的平均值?(范围查询)

Mipmap 快速、近似、正方形范围查询——生成多个层级的不同尺寸的纹理贴图,用来对应不同情况(远处、近处)时,UV坐标的纹素

在这里插入图片描述

更好的办法是使用各向异性过滤,原本的 Mipmap 是正方形的,各向异性过滤就是横纵方向多了不同长宽比的矩形纹理贴图

纹理贴图的应用
  • 光照贴图——用纹理贴图模拟环境光对物体的效果,想象成房间中的一个光滑小球可以反射四周的景象,但是如果把球体展开,两极位置会产生扭曲,所以改用立方体,六个面去反射四周的景象。

  • 凹凸贴图——用纹理贴图定义物体表面不同位置的相对高度,顶点影响法向量,从而影响着色效果。凹凸贴图实际未改变物体表面的几何形状,只是视觉上产生表面凹凸不平的效果。在物体的边缘、凸起阴影处可能会露馅。

    如何计算改变后的法向量?通过差分法计算切线,再计算法向量

    因为是在局部坐标系下(切线空间)计算出的法向量,要应用到光照中的话,需要进行坐标转换,TBN 矩阵就是用来做转换的,TBN 矩阵推导较为复杂。

在这里插入图片描述

        Eigen::Vector3f n = normal;
        Eigen::Vector3f t;
        t<<n.x()*n.y()/std::sqrt(n.x()*n.x()+n.z()*n.z()),
            std::sqrt(n.x()*n.x()+n.z()*n.z()),
            n.z()*n.y()/std::sqrt(n.x()*n.x()+n.z()*n.z());
        
        Eigen::Vector3f b = n.cross(t);
    
        Eigen::Matrix3f TBN;
        TBN<<t,b,n;
        // 等价于 payload.tex_coords[0]
        float u = payload.tex_coords(0);
        float v = payload.tex_coords(1);
        float w = payload.texture->width;
        float h = payload.texture->height;
    
        // dU = kh * kn * (h(u+1/w,v)-h(u,v))
        float dU = kh * kn * (payload.texture->getColor(u+1.f/w,v).norm() - payload.texture->getColor(u,v).norm());
        // dV = kh * kn * (h(u,v+1/h)-h(u,v))
        float dV = kh * kn * (payload.texture->getColor(u,v+1.f/h).norm() - payload.texture->getColor(u,v).norm());
    
        Eigen::Vector3f ln;
        ln<<-dU, -dV, 1;
        // 新的法向量
        n = (TBN * ln).normalized();
    
        Eigen::Vector3f result_color = {0, 0, 0};
        result_color = n;
  • 法线贴图——直接存储表面法线(切线空间),而不是存储相对高度
    // https://www.icode9.com/content-4-1153285.html
    // 使用法线贴图(normal map)来直接存储表面法线,经过简单映射即可从颜色信息转换为法线信息
    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;
    }
  • 位移贴图——真的移动顶点,从而改变顶点法线,不容易露馅。

    跟 bump 凹凸贴图十分类似,但是有实际移动顶点

    ......
    		// 实际移动顶点
        point = point + kn * n * payload.texture->getColor(u,v).norm();
        n = (TBN * ln).normalized();
    ......
    
  • 3D纹理——通过噪声函数定义空间中任意点对应的信息

  • 阴影纹理——通过纹理来保存预先计算好的环境光遮蔽产生的阴影信息

作业 3 插值计算法向量颜色等、布林冯模型的shader、凹凸贴图、位移贴图

TBN矩阵的推导暂时没读懂,TBN矩阵作用,就是切线空间下的法线如何变换到光照时的视图空间下使用

10 几何

如何描述不同形态的几何?

隐式表示
  • 用函数描述几何体上的点满足的数量关系,例如球的函数,不方便观察,但方便判断几何体内的点

  • 通过基础物体的组合、交集、并集等几何运算来表示几何

  • 通过距离函数表示

  • 通过水平集表示 (等高线)

  • 分形几何

显式表示
  • 点云——直接给出所有点或通过参数映射给出所有点,将 2D 空间中的点映射到 3D 空间中。方便观察,但不方便判断几何体内的点。
  • 多边形面——用三角形或四边形描述复杂的形状
  • 曲线——参数映射到UV坐标
  • 曲面——参数映射
贝塞尔曲线

看成是一个点从起点,运动到终点,在任意 t 时刻,通过控制点组成的线段之间,进行多次插值,求出曲线经过的点

在这里插入图片描述

在仿射变换后,通过多次插值,重新绘制的贝塞尔曲线,和原先的贝塞尔曲线保持一致

逐段贝塞尔曲线——使用多个三次贝塞尔曲线连接成一个曲线

曲面

通过 4X4 的控制点描述曲面,映射到UV坐标(U时刻得到四横排的曲线上的点,V时刻就是这四个点为控制点的竖列的曲线上的点)

几何处理——曲面操作
  • 曲面细分
  • 曲面简化
  • 曲面规则化
曲面细分
法一:loop细分

适用于三角形面

把一个三角形连接三边中点,分成四个小三角形。

如何计算新生成的三角形的顶点?答案是取周围几个顶点位置的加权平均数。

如何计算旧三角形的顶点?

法二:Catmull-Clark细分

适用于一般情况,三角形面和多边形面混合

取每个面的中心点和边的中点连线

曲面简化

边坍缩

通过二次误差度量,找出价值最低的顶点进行坍缩,使用优先队列的数据结构,在坍缩完一个顶点后更新剩余顶点的价值

作业4绘制曲线 de Casteljau算法

在这里插入图片描述

// de Casteljau算法下,曲线上t处的点的位置
cv::Point2f recursive_bezier(const std::vector<cv::Point2f> &control_points, float t) 
{
    // Implement de Casteljau's algorithm
    if(control_points.size()==1)
    // 当序列只包含一个点,返回该点
    {
        return control_points[0];
    }
    else
    // 否则,继续迭代执行当前函数
    {
        // 构造新控制点的容器
        std::vector<cv::Point2f> new_control_points;
        // t:(1-t) 按比例分割线段,找到分割点
        for(int i=0;i<control_points.size()-1;i++)
        {
            new_control_points.emplace_back((1-t)*control_points[i]+t*control_points[i+1]);
        }
        // 根据新控制点的容器,计算曲线上t处点的位置
        return recursive_bezier(new_control_points,t);
    }
}
// 根据算法计算曲线上的点
void bezier(const std::vector<cv::Point2f> &control_points, cv::Mat &window) 
{
    // Iterate through all t = 0 to t = 1 with small steps, and call de Casteljau's 
    // t从 [0,1] 开始,每次取一小段时间,计算曲线上的点
    // recursive Bezier algorithm.
    for (double t = 0.0; t <= 1.0; t += 0.001) 
    {
        auto point = recursive_bezier(control_points,t);
        // 反走样的情况:曲线的点周围有多个像素,并不一定恰好落在某个像素中央,因此考虑与周围 3X3 像素中心的距离,来决定周围像素的颜色,看起来就会更平滑
        for(int x=-1;x<2;x++)
        {
            for(int y=-1;y<2;y++)
            {
                float p_x = std::floor(point.x)+0.5f*x;
                float p_y = std::floor(point.y)+0.5f*y;
                // (p_x,p_y) 像素中心与曲线某一点的距离为 [0,3/1.41] 1.41 就是根号二 根据勾股定理求得斜边距离
                float distance = std::sqrt(std::pow(p_x-point.x,2)+std::pow(p_y-point.y,2));                
                // 映射到颜色 [255,0] 上,远离曲线某一点的像素中心,应该不着色
                float color = (1-distance/(3/1.41))*255;
                if(window.at<cv::Vec3b>(p_y, p_x)[1]<color)
                {
                    window.at<cv::Vec3b>(p_y, p_x)[1]=color;
                }
            }
        }
        
        // 简化的情况:曲线的点对应屏幕上的一个像素,会走样产生锯齿
        // window.at<cv::Vec3b>(point.y, point.x)[1] = 255;
    }
}
阴影映射

在这里插入图片描述

硬阴影——只考虑点光源

软阴影——认为光源是有大小的,所以考虑本影和半影

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值