当前位置: 首页 > news >正文

TinyRenderer学习笔记--Lesson 3、4

Lesson 3 zbuffer

无论怎样,生活中的显示器基本上都是平面,是一个2D的场景,而我们的模型却是3D的,是有深度的,实际上我们看见的都只是离我们的眼睛最近的那一个平面,一个不透明的3D物体的内部和背面是我们无法观测到的。对应到计算机里面,我们就需要知道一个物体哪个平面离我们的虚拟摄像机最近,但是这往往是很难办到的,如下:

image-20220814145444624

显然,我们无法判断绿色三角形和粉色三角形哪个离平面更近,这时就不能以三角形平面为单位来绘制,需要以像素为单位来绘制。

void triangle(Vec3f* pts, float* zbuffer, TGAImage& image, TGAColor color)
{
    //定义包围盒
    Vec2f bboxmin(std::numeric_limits<float>::max(), std::numeric_limits<float>::max());
    Vec2f bboxmax(-std::numeric_limits<float>::max(), -std::numeric_limits<float>::max());
    Vec2f clamp(image.get_width() - 1, image.get_height() - 1);
    //找到包围盒
    for (int i = 0; i < 3; ++i)
    {
        for (int j = 0; j < 2; ++j)
        {
            bboxmin[j] = std::max(0.f, std::min(bboxmin[j], pts[i][j]));
            bboxmax[j] = std::min(clamp[j], std::max(bboxmax[j],pts[i][j]));
        }
    }
    Vec3f p;
    for (p.x = bboxmin.x; p.x < bboxmax.x; p.x++)
    {
        for (p.y = bboxmin.y; p.y < bboxmax.y; p.y++)
        {
            //找到重心坐标并判断判断是否在三角形内
            Vec3f bc_screen = barycentric(pts[0], pts[1], pts[2], p);
            if (bc_screen.x < 0 || bc_screen.y < 0 || bc_screen.z < 0) continue;
            p.z = 0;
            //通过重心坐标计算深度值
            for (int i = 0; i < 3; i++) p.z += pts[i][2] * bc_screen[i];
            if (zbuffer[int(p.x + p.y * width)] < p.z)
            {
                //更新深度值
                zbuffer[int(p.x + p.y * width)] = p.z;
                image.set(p.x, p.y, color);
            }
        }
    }

}

.........
    
    Vec3f light_dir(0, 0, -1);
    //深度缓冲区,并赋值
    float* zbuffer = new float[width * height];
    for (int i = width * height; i--; zbuffer[i] = -std::numeric_limits<float>::max());

    for (int i = 0; i < model->nfaces(); i++) {
        std::vector<int> face = model->face(i);
        Vec3f pts[3];
        Vec3f world_coords[3];

        for (int j = 0; j < 3; ++j)
        {
            Vec3f v = model->vert(face[j]);
            pts[j] = world2screen(model->vert(face[j]));
            world_coords[j] = v;
        }
        Vec3f n = cross((world_coords[2] - world_coords[0]),(world_coords[1] - world_coords[0]));
        n.normalize();
        float intensity = n * light_dir;//光照强度=法向量*光照方向   即法向量和光照方向重合时,亮度最高
        //强度小于0,说明平面朝向为内  即背面裁剪
        if (intensity > 0) {
            triangle(pts,zbuffer, image, TGAColor(intensity * 255, intensity * 255, intensity * 255, 255));
        }
    } 

在我们之前上一节实现的平面着色上增加深度值的检测,会让我们的图象看起来更加立体

image-20220814150141677

下面将要给我们的模型加上贴图,让渲染的模型看起来更加真实。这里我们采用重心坐标插值的办法来进行纹理贴

图,首先我们需要知道某个三角形顶点上的UV值,然后通过插值的办法计算出三角形内部某个点的UV值,OBJ文

件里已经保存了顶点的纹理坐标和纹理信息,只需要进行一次插值计算即可。关键代码如下:

Vec3f p;
for (p.x = bboxmin.x; p.x < bboxmax.x; p.x++)
{
    for (p.y = bboxmin.y; p.y < bboxmax.y; p.y++)
    {
        //找到重心坐标并判断是否在三角形内
        Vec3f bc_screen = barycentric(pts[0], pts[1], pts[2], p);
        if (bc_screen.x < 0 || bc_screen.y < 0 || bc_screen.z < 0) continue;
        p.z = 0;
        //重心坐标插值计算UV值
        Vec2i uvp = uv[0] * bc_screen.x + uv[1] * bc_screen.y + uv[2] * bc_screen.z;
        //通过重心坐标计算深度值
        for (int i = 0; i < 3; i++) p.z += pts[i][2] * bc_screen[i];
        if (zbuffer[int(p.x + p.y * width)] < p.z)
        {
            //更新深度值
            zbuffer[int(p.x + p.y * width)] = p.z;
            TGAColor color = model->diffuse(uvp); //找到对应纹理
            image.set(p.x, p.y, TGAColor(color.r * intensity, color.g * intensity, color.b * intensity, 255));
        }
    }
}
    if (intensity > 0) {
        Vec2i uv[3];
        for (int k = 0; k < 3; k++) {
            uv[k] = model->uv(i, k);//获取三个顶点的UV值
        }
        triangle(pts,zbuffer,uv, image,intensity);
    }

注意,这里要对model.h和geometry.h及.cpp文件进行修改,详情参考

效果如下:

image-20220830094648272

Lesson 4 透视投影

投影大致可以分为透视投影和正交投影

image-20220819092155185

透视投影的最直观的效果就是近大远小。

对于透视投影的计算,我们需要进行简单的探讨,这里就不在讨论,强烈建议大家查看文章

文章中对缩放矩阵,平移矩阵等进行了详细的探讨。

image-20220819094033667

其中 r = -1/c,

接下来就是进行编码了。代码大体上和上一节差不多,多的部分就是利用矩阵来实现透视投影。

两个功能函数,4D变3D和3D变4D,和一个视角转换函数,


Vec3f m2v(Matrix m)
{
    return Vec3f(m[0][0] / m[3][0], m[1][0] / m[3][0], m[2][0] / m[3][0]);
}

Matrix v2m(Vec3f v)
{
    Matrix m(4,1);
    m[0][0] = v.x;
    m[1][0] = v.y;
    m[2][0] = v.z;
    m[3][0] = 1.f;//添加一个1表示坐标
    return m;
}
//视图矩阵,把模型坐标的(-1,1)转换成屏幕坐标的(100,700)
//zbuffer从(-1,1)转换成0~255
Matrix viewport(int x, int y, int w, int h) {
    Matrix m = Matrix::identity(4);
    //平移
    m[0][3] = x + w / 2.f;
    m[1][3] = y + h / 2.f;
    m[2][3] = depth / 2.f;
    //缩放
    m[0][0] = w / 2.f;
    m[1][1] = h / 2.f;
    m[2][2] = depth / 2.f;
    return m;
}

对投影矩阵和视角矩阵进行初始化,这里注意投影矩阵的[3] [2]坐标

    //初始化透视投影矩阵
    Matrix Projection = Matrix::identity(4);
    //初始化视角矩阵
    Matrix ViewPort = viewport(width / 8, height / 8, width * 3 / 4, height * 3 / 4);
    //投影矩阵[3][2]=-1/c,c为相机z坐标
    Projection[3][2] = -1.f / camera.z;

有了这三个矩阵,在计算屏幕坐标的时候,直接进行乘就行,简称为MVP变换,M是模型矩阵,V是视角矩阵,P是投影矩阵。

        for (int j = 0; j < 3; j++) {
            Vec3f v = model->vert(face[j]);
            //视角矩阵*投影矩阵*坐标
            screen_coords[j] = m2v(ViewPort * Projection * v2m(v));
            world_coords[j] = v;
        }

就在这里应用,和原来的代码的主要不同之处也就在这。最后结果如下

image-20220830150425580

下面是深度图:

image-20220830150504984

现在通过学习,已经学会了

三角形的栅格化及背面剔除 (通过实现不同光照来实现背部剔除)

zbuffer深度缓冲区

纹理贴图 (利用重心坐标插值)

透视投影 (MVP变换)

之处也就在这。最后结果如下

相关文章:

  • (附源码)springboot太原学院贫困生申请管理系统 毕业设计 101517
  • Hive的独立安装
  • Smobiler 窗体
  • Android用户切换系统语言后,回到App,App重新加载导致的一些问题[android:configChanges=“layoutDirection“]
  • Django部署深度学习项目-1
  • JS-sort
  • Callable接口(类似于Runnable)
  • CentOS环境下安装Nacos
  • 金仓数据库 KingbaseES 插件参考手册 S (2)
  • 营销软文的结尾怎样写?营销软文结尾怎样去设计?
  • 2022河南萌新联赛第(七)场:南阳理工学院 B 龍
  • 我做了几年的Android应用层开发,为什么还要去学习安卓系统知识?
  • [暑假]Vue框架里面 一些属性和配置项的作用
  • 【unity记录】导入标准资源包(Standard Assets)
  • SoringBoot特点
  • [ JavaScript ] 数据结构与算法 —— 链表
  • [微信小程序] 使用ES6特性Class后出现编译异常
  • 【React系列】如何构建React应用程序
  • httpie使用详解
  • iOS 系统授权开发
  • java第三方包学习之lombok
  • Java应用性能调优
  • Netty 4.1 源代码学习:线程模型
  • node-sass 安装卡在 node scripts/install.js 解决办法
  • WebSocket使用
  • 不上全站https的网站你们就等着被恶心死吧
  • 从PHP迁移至Golang - 基础篇
  • 关于使用markdown的方法(引自CSDN教程)
  • 前端_面试
  • 数组的操作
  • 小程序01:wepy框架整合iview webapp UI
  • CMake 入门1/5:基于阿里云 ECS搭建体验环境
  • 通过调用文摘列表API获取文摘
  • ​LeetCode解法汇总1276. 不浪费原料的汉堡制作方案
  • ​软考-高级-信息系统项目管理师教程 第四版【第23章-组织通用管理-思维导图】​
  • # Maven错误Error executing Maven
  • #在 README.md 中生成项目目录结构
  • (react踩过的坑)Antd Select(设置了labelInValue)在FormItem中initialValue的问题
  • (分享)一个图片添加水印的小demo的页面,可自定义样式
  • (黑马出品_高级篇_01)SpringCloud+RabbitMQ+Docker+Redis+搜索+分布式
  • (剑指Offer)面试题41:和为s的连续正数序列
  • (生成器)yield与(迭代器)generator
  • (转) SpringBoot:使用spring-boot-devtools进行热部署以及不生效的问题解决
  • (转)用.Net的File控件上传文件的解决方案
  • .NET 除了用 Task 之外,如何自己写一个可以 await 的对象?
  • .NET 中 GetProcess 相关方法的性能
  • .net 中viewstate的原理和使用
  • :中兴通讯为何成功
  • [ CTF ] WriteUp- 2022年第三届“网鼎杯”网络安全大赛(白虎组)
  • [Android]通过PhoneLookup读取所有电话号码
  • [Angular] 笔记 20:NgContent
  • [AX]AX2012开发新特性-禁止表或者表字段
  • [BZOJ 3680]吊打XXX(模拟退火)
  • [C和指针].(美)Kenneth.A.Reek(ED2000.COM)pdf
  • [Docker]四.Docker部署nodejs项目,部署Mysql,部署Redis,部署Mongodb